├── .editorconfig
├── .github
└── ISSUE_TEMPLATE.md
├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── Makefile
├── PyPi.rst
├── README.md
├── h8mail
├── __init__.py
├── __main__.py
├── requirements.txt
└── utils
│ ├── __init__.py
│ ├── breachcompilation.py
│ ├── chase.py
│ ├── classes.py
│ ├── colors.py
│ ├── gen_config.py
│ ├── helpers.py
│ ├── intelx.py
│ ├── intelx_helpers.py
│ ├── localgzipsearch.py
│ ├── localsearch.py
│ ├── print_json.py
│ ├── print_results.py
│ ├── run.py
│ ├── summary.py
│ ├── url.py
│ └── version.py
├── setup.py
└── tests
├── __init__.py
├── test_email.txt
└── test_h8mail.py
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | indent_style = space
7 | indent_size = 4
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 | charset = utf-8
11 | end_of_line = lf
12 |
13 | [*.bat]
14 | indent_style = tab
15 | end_of_line = crlf
16 |
17 | [LICENSE]
18 | insert_final_newline = false
19 |
20 | [Makefile]
21 | indent_style = tab
22 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | *Issues not respecting the issue template will be closed without being read, thank you.*
2 |
3 | ### Checkbox
4 |
5 | - [ ] I have thoroughly checked the answer is not in the [wiki](https://github.com/khast3x/h8mail/wiki).
6 |
7 | ### Env
8 |
9 | * h8mail version:
10 | * Python version:
11 | * Operating System:
12 |
13 | ### Description
14 |
15 | Describe what you were trying to get done.
16 | Tell us what happened, what went wrong, and what you expected to happen.
17 |
18 | ### What I Did
19 |
20 | ```
21 | Paste the command(s) you ran and the output.
22 | If there was a crash, please include the traceback here.
23 | ```
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # h8mail
10 |
11 | my_config*
12 |
13 | # Distribution / packaging
14 | .Python
15 | env/
16 | build/
17 | develop-eggs/
18 | dist/
19 | downloads/
20 | eggs/
21 | .eggs/
22 | lib/
23 | lib64/
24 | parts/
25 | sdist/
26 | var/
27 | wheels/
28 | *.egg-info/
29 | .installed.cfg
30 | *.egg
31 |
32 | # PyInstaller
33 | # Usually these files are written by a python script from a template
34 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
35 | *.manifest
36 | *.spec
37 |
38 | # Installer logs
39 | pip-log.txt
40 | pip-delete-this-directory.txt
41 |
42 | # Unit test / coverage reports
43 | htmlcov/
44 | .tox/
45 | .coverage
46 | .coverage.*
47 | .cache
48 | nosetests.xml
49 | coverage.xml
50 | *.cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 |
62 | # Flask stuff:
63 | instance/
64 | .webassets-cache
65 |
66 | # Scrapy stuff:
67 | .scrapy
68 |
69 | # Sphinx documentation
70 | docs/_build/
71 |
72 | # PyBuilder
73 | target/
74 |
75 | # Jupyter Notebook
76 | .ipynb_checkpoints
77 |
78 | # pyenv
79 | .python-version
80 |
81 | # celery beat schedule file
82 | celerybeat-schedule
83 |
84 | # SageMath parsed files
85 | *.sage.py
86 |
87 | # dotenv
88 | .env
89 |
90 | # virtualenv
91 | .venv
92 | venv/
93 | ENV/
94 |
95 | # Spyder project settings
96 | .spyderproject
97 | .spyproject
98 |
99 | # Rope project settings
100 | .ropeproject
101 |
102 | # mkdocs documentation
103 | /site
104 |
105 | # mypy
106 | .mypy_cache/
107 | .vscode/settings.json
108 | h8mail_config.ini
109 | h8mail_keys.ini
110 | h8mail_config.ini
111 | keysbackup.ini
112 | .vscode/launch.json
113 | file1.bin
114 | *.txt
115 | test.json
116 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # Config file for automatic testing at travis-ci.org
2 | # This file will be regenerated if you run travis_pypi_setup.py
3 |
4 | language: python
5 | python:
6 | - 3.6
7 |
8 |
9 | # Command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors
10 | # install: pip install -r requirements.txt
11 |
12 | install: pip install -e .
13 |
14 | # Command to run tests, e.g. python setup.py test
15 | script: python setup.py test
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # How to contribute to h8mail
2 |
3 | *First of all, thank you for considering contributing :purple_heart:*
4 | *h8mail is very much a script turned into a tool. As such, the project is still evolving in regards to contribution guidelines.*
5 |
6 | *If this is your first contribution, I highly suggest you check [this repo](https://github.com/firstcontributions/first-contributions) first.*
7 |
8 | ----
9 |
10 |
11 | ### The basics
12 |
13 | * h8mail should only depend on the `requests` package
14 |
15 | * If adding a new feature or refactoring, consider opening an issue or sending me an email beforehand to ensure things go smoothly
16 |
17 | * Comment your functions as docstrings
18 |
19 | * Pull requests should be merged into the latest development branch, not in the main branch
20 | * If there is no on-going development branch, there should still be the current version's dev branch
21 | * If unsure, open an issue and you'll be pointed to the right branch, or a seperate branch can be created for you
22 |
23 | * Make sure your code passes the included tests. You can run them by using `python3 -m unittest` in h8mail's top level directory
24 |
25 | * Code should be formatted using [Python Black](https://github.com/psf/black). Most IDE's can run `black` directly. You can also launch it [from CLI](https://github.com/psf/black#installation-and-usage)
26 |
27 | ----
28 |
29 | ### Ways to contribute
30 |
31 | * Bug reports using issues
32 | * Bug fixes *(please open issues for typos)*
33 | * Code refactoring *(please open issue beforehand if > 2 lines)*
34 | * Code optimisation
35 | * New features
36 | * New API integrations
37 |
38 | ----
39 |
40 | ### Code of conduct
41 |
42 | h8mail follows [Django's code of conduct](https://www.djangoproject.com/conduct/).
43 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3-alpine
2 |
3 | RUN apk add --update --no-cache git bash
4 | WORKDIR h8mail
5 | RUN pip3 install requests
6 | COPY . .
7 | RUN ["python", "setup.py", "install"]
8 | ENTRYPOINT ["h8mail"]
9 | CMD ["-h"]
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 |
3 | BSD License
4 |
5 | Copyright (c) 2019, khast3x
6 | All rights reserved.
7 |
8 | Redistribution and use in source and binary forms, with or without modification,
9 | are permitted provided that the following conditions are met:
10 |
11 | * Redistributions of source code must retain the above copyright notice, this
12 | list of conditions and the following disclaimer.
13 |
14 | * Redistributions in binary form must reproduce the above copyright notice, this
15 | list of conditions and the following disclaimer in the documentation and/or
16 | other materials provided with the distribution.
17 |
18 | * Neither the name of the copyright holder nor the names of its
19 | contributors may be used to endorse or promote products derived from this
20 | software without specific prior written permission.
21 |
22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
23 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
24 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
25 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
26 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
29 | OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
30 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
31 | OF THE POSSIBILITY OF SUCH DAMAGE.
32 |
33 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: clean clean-test clean-pyc clean-build docs help
2 | .DEFAULT_GOAL := help
3 |
4 | define BROWSER_PYSCRIPT
5 | import os, webbrowser, sys
6 |
7 | try:
8 | from urllib import pathname2url
9 | except:
10 | from urllib.request import pathname2url
11 |
12 | webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1])))
13 | endef
14 | export BROWSER_PYSCRIPT
15 |
16 | define PRINT_HELP_PYSCRIPT
17 | import re, sys
18 |
19 | for line in sys.stdin:
20 | match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line)
21 | if match:
22 | target, help = match.groups()
23 | print("%-20s %s" % (target, help))
24 | endef
25 | export PRINT_HELP_PYSCRIPT
26 |
27 | BROWSER := 3 -c "$$BROWSER_PYSCRIPT"
28 |
29 | help:
30 | @python3 -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST)
31 |
32 | clean: clean-build clean-pyc clean-test ## remove all build, test, coverage and Python artifacts
33 |
34 | clean-build: ## remove build artifacts
35 | rm -fr build/
36 | rm -fr dist/
37 | rm -fr .eggs/
38 | find . -name '*.egg-info' -exec rm -fr {} +
39 | find . -name '*.egg' -exec rm -f {} +
40 |
41 | clean-pyc: ## remove Python file artifacts
42 | find . -name '*.pyc' -exec rm -f {} +
43 | find . -name '*.pyo' -exec rm -f {} +
44 | find . -name '*~' -exec rm -f {} +
45 | find . -name '__pycache__' -exec rm -fr {} +
46 |
47 | clean-test: ## remove test and coverage artifacts
48 | rm -fr .tox/
49 | rm -f .coverage
50 | rm -fr htmlcov/
51 | rm -fr .pytest_cache
52 |
53 | lint: ## check style with flake8
54 | black h8mail tests
55 |
56 | test: ## run tests quickly with the default Python
57 | python3 setup.py test
58 |
59 |
60 | coverage: ## check code coverage quickly with the default Python
61 | coverage run --source h8mail setup.py test
62 | coverage report -m
63 | coverage html
64 | $(BROWSER) htmlcov/index.html
65 |
66 | release: dist ## package and upload a release
67 | twine upload dist/*
68 |
69 | dist: clean ## builds source and wheel package
70 | python3 setup.py sdist
71 | python3 setup.py bdist_wheel
72 | ls -l dist
73 |
74 | install: clean ## install the package to the active Python's site-packages
75 | python3 setup.py install
76 |
--------------------------------------------------------------------------------
/PyPi.rst:
--------------------------------------------------------------------------------
1 | ======
2 | h8mail
3 | ======
4 |
5 | .. image:: https://i.postimg.cc/LXR6Jq8Y/logo-transparent.png
6 | ::target: https://github.com/khast3x/h8mail
7 |
8 | .. image:: https://img.shields.io/pypi/v/h8mail.svg
9 | :target: https://pypi.python.org/pypi/h8mail
10 |
11 | .. image:: https://img.shields.io/travis/khast3x/h8mail.svg
12 | :target: https://travis-ci.org/khast3x/h8mail
13 |
14 |
15 |
16 |
17 |
18 |
19 | Email OSINT and password breach hunting. Use h8mail to find passwords through different breach and reconnaissance services, or the infamous Breached Compilation torrent
20 |
21 |
22 | * Free software: BSD license
23 |
24 |
25 | Features
26 | --------
27 |
28 | * Email pattern matching (reg exp), useful for reading from other tool outputs
29 | * Loosey patterns for local searchs ("john.smith", "evilcorp")
30 | * Painless install. Available through `pip`, only requires `requests`
31 | * Small and fast Alpine Dockerfile available
32 | * CLI or Bulk file-reading for targeting
33 | * Output to CSV file
34 | * Compatible with the "Breach Compilation" torrent scripts
35 | * Search .txt and .gz files locally using multiprocessing
36 | * Compatible with "Collection#1"
37 | * Get related emails
38 | * Chase and target related emails in ongoing search
39 | * Supports premium lookup services for advanced users
40 | * Regroup breach results for all targets and methods
41 | * Delicious colors
42 |
43 | Credits
44 | -------
45 |
46 | This package was created with Cookiecutter_ and the `audreyr/cookiecutter-pypackage`_ project template.
47 |
48 | .. _Cookiecutter: https://github.com/audreyr/cookiecutter
49 | .. _`audreyr/cookiecutter-pypackage`: https://github.com/audreyr/cookiecutter-pypackage
50 |
51 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | [](https://pypi.org/project/h8mail/) [](https://badge.fury.io/py/h8mail)
6 | [](https://pypi.org/project/h8mail/) [](https://pepy.tech/project/h8mail) [](https://travis-ci.org/khast3x/h8mail)
7 | [](https://hub.docker.com/r/kh4st3x00/h8mail)
8 | **h8mail** is an email OSINT and breach hunting tool using [different breach and reconnaissance services](#apis), or local breaches such as Troy Hunt's "Collection1" and the infamous "Breach Compilation" torrent.
9 |
10 | ----
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | ----
20 |
21 |
22 | ## :book: Table of Content
23 |
24 | - [Table of Content](#book-Table-of-Content)
25 | - [Features](#tangerine-Features)
26 | - [APIs](#APIs)
27 | - [Usage](#tangerine-Usage)
28 | - [Usage examples](#tangerine-Usage-examples)
29 | - [Thanks & Credits](#tangerine-Thanks--Credits)
30 | - [Related open source projects](#tangerine-Related-open-source-projects)
31 |
32 |
33 | ----
34 |
35 |
36 | ## :tangerine: Features
37 |
38 | * :mag_right: Email pattern matching (reg exp), useful for reading from other tool outputs
39 | * :earth_africa: Pass URLs to directly find and target emails in pages
40 | * :dizzy: Loosey patterns for local searchs ("john.smith", "evilcorp")
41 | * :package: Painless install. Available through `pip`, only requires `requests`
42 | * :white_check_mark: Bulk file-reading for targeting
43 | * :memo: Output to CSV file or JSON
44 | * :muscle: Compatible with the "Breach Compilation" torrent scripts
45 | * :house: Search cleartext and compressed .gz files locally using multiprocessing
46 | * :cyclone: Compatible with "Collection#1"
47 | * :fire: Get related emails
48 | * :dragon_face: Chase related emails by adding them to the ongoing search
49 | * :crown: Supports premium lookup services for advanced users
50 | * :factory: Custom query premium APIs. Supports username, hash, ip, domain and password and more
51 | * :books: Regroup breach results for all targets and methods
52 | * :eyes: Includes option to hide passwords for demonstrations
53 | * :rainbow: Delicious colors
54 |
55 | ---
56 |
57 | ### :package: `pip3 install h8mail`
58 |
59 | -----
60 |
61 |
62 | #### APIs
63 | | Service | Functions | Status |
64 | |-|-|-|
65 | | [HaveIBeenPwned(v3)](https://haveibeenpwned.com/) | Number of email breaches | :white_check_mark: :key: |
66 | | [HaveIBeenPwned Pastes(v3)](https://haveibeenpwned.com/Pastes) | URLs of text files mentioning targets | :white_check_mark: :key: |
67 | | [Hunter.io](https://hunter.io/) - Public | Number of related emails | :white_check_mark: |
68 | | [Hunter.io](https://hunter.io/) - Service (free tier) | Cleartext related emails, Chasing | :white_check_mark: :key: |
69 | | [Snusbase](https://api.snusbase.com/admin/purchase) - Service | Cleartext passwords, hashs and salts, usernames, IPs - Fast :zap: | :white_check_mark: :key: |
70 | | [Leak-Lookup](https://leak-lookup.com/) - Public | Number of search-able breach results | :white_check_mark: (:key:) |
71 | | [Leak-Lookup](https://leak-lookup.com/) - Service | Cleartext passwords, hashs and salts, usernames, IPs, domain | :white_check_mark: :key: |
72 | | [Emailrep.io](https://emailrep.io/) - Service (free) | Last seen in breaches, social media profiles | :white_check_mark: :key: |
73 | | [scylla.so](https://scylla.so/) - Service (free) | Cleartext passwords, hashs and salts, usernames, IPs, domain | :construction: |
74 | | [Dehashed.com](https://dehashed.com/) - Service | Cleartext passwords, hashs and salts, usernames, IPs, domain | :white_check_mark: :key: |
75 | | [IntelX.io](https://intelx.io/signup) - Service (free trial) | Cleartext passwords, hashs and salts, usernames, IPs, domain, Bitcoin Wallets, IBAN | :white_check_mark: :key: |
76 | | :new: [Breachdirectory.org](https://breachdirectory.org) - Service (free) | Cleartext passwords, hashs and salts, usernames, domain | :construction: :key: |
77 |
78 | *:key: - API key required*
79 |
80 |
81 |
82 |
83 | -----
84 |
85 | ## :tangerine: Usage
86 |
87 | ```bash
88 | usage: h8mail [-h] [-t USER_TARGETS [USER_TARGETS ...]]
89 | [-u USER_URLS [USER_URLS ...]] [-q USER_QUERY] [--loose]
90 | [-c CONFIG_FILE [CONFIG_FILE ...]] [-o OUTPUT_FILE]
91 | [-j OUTPUT_JSON] [-bc BC_PATH] [-sk]
92 | [-k CLI_APIKEYS [CLI_APIKEYS ...]]
93 | [-lb LOCAL_BREACH_SRC [LOCAL_BREACH_SRC ...]]
94 | [-gz LOCAL_GZIP_SRC [LOCAL_GZIP_SRC ...]] [-sf]
95 | [-ch [CHASE_LIMIT]] [--power-chase] [--hide] [--debug]
96 | [--gen-config]
97 |
98 | Email information and password lookup tool
99 |
100 | optional arguments:
101 | -h, --help show this help message and exit
102 | -t USER_TARGETS [USER_TARGETS ...], --targets USER_TARGETS [USER_TARGETS ...]
103 | Either string inputs or files. Supports email pattern
104 | matching from input or file, filepath globing and
105 | multiple arguments
106 | -u USER_URLS [USER_URLS ...], --url USER_URLS [USER_URLS ...]
107 | Either string inputs or files. Supports URL pattern
108 | matching from input or file, filepath globing and
109 | multiple arguments. Parse URLs page for emails.
110 | Requires http:// or https:// in URL.
111 | -q USER_QUERY, --custom-query USER_QUERY
112 | Perform a custom query. Supports username, password,
113 | ip, hash, domain. Performs an implicit "loose" search
114 | when searching locally
115 | --loose Allow loose search by disabling email pattern
116 | recognition. Use spaces as pattern seperators
117 | -c CONFIG_FILE [CONFIG_FILE ...], --config CONFIG_FILE [CONFIG_FILE ...]
118 | Configuration file for API keys. Accepts keys from
119 | Snusbase, WeLeakInfo, Leak-Lookup, HaveIBeenPwned,
120 | Emailrep, Dehashed and hunterio
121 | -o OUTPUT_FILE, --output OUTPUT_FILE
122 | File to write CSV output
123 | -j OUTPUT_JSON, --json OUTPUT_JSON
124 | File to write JSON output
125 | -bc BC_PATH, --breachcomp BC_PATH
126 | Path to the breachcompilation torrent folder. Uses the
127 | query.sh script included in the torrent
128 | -sk, --skip-defaults Skips Scylla and HunterIO check. Ideal for local scans
129 | -k CLI_APIKEYS [CLI_APIKEYS ...], --apikey CLI_APIKEYS [CLI_APIKEYS ...]
130 | Pass config options. Supported format: "K=V,K=V"
131 | -lb LOCAL_BREACH_SRC [LOCAL_BREACH_SRC ...], --local-breach LOCAL_BREACH_SRC [LOCAL_BREACH_SRC ...]
132 | Local cleartext breaches to scan for targets. Uses
133 | multiprocesses, one separate process per file, on
134 | separate worker pool by arguments. Supports file or
135 | folder as input, and filepath globing
136 | -gz LOCAL_GZIP_SRC [LOCAL_GZIP_SRC ...], --gzip LOCAL_GZIP_SRC [LOCAL_GZIP_SRC ...]
137 | Local tar.gz (gzip) compressed breaches to scans for
138 | targets. Uses multiprocesses, one separate process per
139 | file. Supports file or folder as input, and filepath
140 | globing. Looks for 'gz' in filename
141 | -sf, --single-file If breach contains big cleartext or tar.gz files, set
142 | this flag to view the progress bar. Disables
143 | concurrent file searching for stability
144 | -ch [CHASE_LIMIT], --chase [CHASE_LIMIT]
145 | Add related emails from hunter.io to ongoing target
146 | list. Define number of emails per target to chase.
147 | Requires hunter.io private API key if used without
148 | power-chase
149 | --power-chase Add related emails from ALL API services to ongoing
150 | target list. Use with --chase
151 | --hide Only shows the first 4 characters of found passwords
152 | to output. Ideal for demonstrations
153 | --debug Print request debug information
154 | --gen-config, -g Generates a configuration file template in the current
155 | working directory & exits. Will overwrite existing
156 | h8mail_config.ini file
157 |
158 | ```
159 |
160 | -----
161 |
162 | ## :tangerine: Usage examples
163 |
164 | ###### Query for a single target
165 |
166 | ```bash
167 | $ h8mail -t target@example.com
168 | ```
169 |
170 | ###### Query for list of targets, indicate config file for API keys, output to `pwned_targets.csv`
171 | ```bash
172 | $ h8mail -t targets.txt -c config.ini -o pwned_targets.csv
173 | ```
174 |
175 | ###### Query a list of targets against local copy of the Breach Compilation, pass API key for [Snusbase](https://snusbase.com/) from the command line
176 | ```bash
177 | $ h8mail -t targets.txt -bc ../Downloads/BreachCompilation/ -k "snusbase_token=$snusbase_token"
178 | ```
179 |
180 | ###### Query without making API calls against local copy of the Breach Compilation
181 | ```bash
182 | $ h8mail -t targets.txt -bc ../Downloads/BreachCompilation/ -sk
183 | ```
184 |
185 | ###### Search every .gz file for targets found in targets.txt locally, skip default checks
186 |
187 | ```bash
188 | $ h8mail -t targets.txt -gz /tmp/Collection1/ -sk
189 | ```
190 |
191 | ###### Check a cleartext dump for target. Add the next 10 related emails to targets to check. Read keys from CLI
192 |
193 | ```bash
194 | $ h8mail -t admin@evilcorp.com -lb /tmp/4k_Combo.txt -ch 10 -k "hunterio=ABCDE123"
195 | ```
196 | ###### Query username. Read keys from CLI
197 |
198 | ```bash
199 | $ h8mail -t JSmith89 -q username -k "dehashed_email=user@email.com" "dehashed_key=ABCDE123"
200 | ```
201 |
202 | ###### Query IP. Chase all related targets. Read keys from CLI
203 |
204 |
205 | ```bash
206 | $ h8mail -t 42.202.0.42 -q ip -c h8mail_config_priv.ini -ch 2 --power-chase
207 | ```
208 |
209 | ###### Fetch URL content (CLI + file). Target all found emails
210 |
211 |
212 | ```bash
213 | $ h8mail -u "https://pastebin.com/raw/kQ6WNKqY" "list_of_urls.txt"
214 | ```
215 |
216 |
217 | -----
218 |
219 | ## :tangerine: Thanks & Credits
220 |
221 | * [Snusbase](https://snusbase.com/) for being developer friendly
222 | * [kodykinzie](https://twitter.com/kodykinzie) for making a nice [introduction and walkthrough article](https://null-byte.wonderhowto.com/how-to/exploit-recycled-credentials-with-h8mail-break-into-user-accounts-0188600/) and [video](https://www.youtube.com/watch?v=z8G_vBBHtfA) on installing and using h8mail
223 | * [Leak-Lookup](https://leak-lookup.com/) for being developer friendly
224 | * [Dehashed](https://dehashed.com/) for being developer friendly
225 | * h8mail's Pypi integration is strongly based on the work of audreyr's [CookieCutter PyPackage](https://github.com/audreyr/cookiecutter-pypackage)
226 | * Logo generated using Hatchful by Shopify
227 | * [Jake Creps](https://twitter.com/jakecreps) for his [h8mail v2 introduction](https://jakecreps.com/2019/06/21/h8mail/)
228 | * [Alejandro Caceres](https://twitter.com/_hyp3ri0n) for making scylla.so available. Be sure to [support](https://www.buymeacoffee.com/Eiw47ImnT) him if you can
229 | * [IntelX](https://intelx.io) for being developer friendly
230 | * [Breachdirectory.tk](https://breachdirectory.tk) for being developer friendly
231 |
232 | :purple_heart: **h8mail can be found in:**
233 | * [BlackArch Linux](https://blackarch.org/recon.html)
234 | * [Tsurugi DFIR VM](https://tsurugi-linux.org/)
235 | * [CSI Linux](https://csilinux.com)
236 | * [Trace Labs OSINT VM](https://www.tracelabs.org/trace-labs-osint-vm/)
237 |
238 |
239 | -----
240 |
241 | ## :tangerine: Related open source projects
242 | * [WhatBreach](https://github.com/Ekultek/WhatBreach) by Ekultek
243 | * [HashBuster](https://github.com/s0md3v/Hash-Buster) by s0md3v
244 | * [BaseQuery](https://github.com/g666gle/BaseQuery) by g666gle
245 | * [LeakLooker](https://github.com/woj-ciech/LeakLooker) by woj-ciech
246 | * [buster](https://github.com/sham00n/buster) by sham00n
247 | * [Scavenger](https://github.com/rndinfosecguy/Scavenger) by ndinfosecguy
248 | * [pwndb](https://github.com/davidtavarez/pwndb) by davidtavarez
249 |
250 |
251 | -----
252 |
253 | ## :tangerine: Notes
254 |
255 | * Service providers that wish being integrated can send me an email at `k at khast3x dot club` (PGP friendly)
256 | * h8mail is maintained on my free time. Feedback and war stories are welcomed.
257 | * Licence is BSD 3 clause
258 | * My code is [signed](https://help.github.com/en/articles/signing-commits) with my [Keybase](https://keybase.io/ktx) PGP key. You can get it using:
259 | ```bash
260 | # curl + gpg pro tip: import ktx's keys
261 | curl https://keybase.io/ktx/pgp_keys.asc | gpg --import
262 |
263 | # the Keybase app can push to gpg keychain, too
264 | keybase pgp pull ktx
265 | ```
266 | ___
267 |
268 | *If you wish to stay updated on this project:*
269 |
270 |
271 |
272 |
273 |
274 |
275 |
--------------------------------------------------------------------------------
/h8mail/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """Top-level package for h8mail."""
4 |
5 | from h8mail.utils.version import __version__
6 |
7 | __author__ = """khast3x"""
8 | __email__ = "k@kast3x.club"
9 |
--------------------------------------------------------------------------------
/h8mail/__main__.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from .utils.run import main
3 |
4 | if __name__ == "__main__":
5 | # Check major and minor python version
6 | if sys.version_info[0] < 3:
7 | sys.stdout.write(
8 | "\n/!\\ h8mail requires Python 3.6+ /!\\\nTry running h8mail with python3 if on older systems\n\neg: python --version\neg: python3 h8mail v --help\n\n"
9 | )
10 | sys.exit(1)
11 | if sys.version_info[1] < 6:
12 | sys.stdout.write(
13 | "\n/!\\ h8mail requires Python 3.6+ /!\\\nTry running h8mail with python3 if on older systems\n\neg: python --version\neg: python3 h8mail --help\n\n"
14 | )
15 | sys.exit(1)
16 | main()
17 | print("Done")
18 |
--------------------------------------------------------------------------------
/h8mail/requirements.txt:
--------------------------------------------------------------------------------
1 | requests
2 |
--------------------------------------------------------------------------------
/h8mail/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/khast3x/h8mail/ee31c8ff6d148580047ae5048fea2234decb5932/h8mail/utils/__init__.py
--------------------------------------------------------------------------------
/h8mail/utils/breachcompilation.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from .colors import colors as c
3 | import os
4 | import re
5 | import stat
6 | from string import printable
7 | from .localsearch import local_search
8 |
9 | def check_shell():
10 | shell = os.environ['SHELL']
11 | print("SHELL IS " + shell)
12 | if "bash" not in shell:
13 | c.info_news("If you're having issues or not results, be sure to launch h8mail using bash for this operation.")
14 | c.info_news("OSX users should read this https://khast3x.club/posts/2021-02-17-h8mail-with-COMB/#targeting-emails\n")
15 |
16 |
17 | def clean_targets(targets):
18 | """
19 | This function is necessary since local_search performs a loose search.
20 | We'll double check results to ensure ONLY target email is present is results
21 | """
22 | for t in targets:
23 | cleaned_data = []
24 | for d in t.data:
25 | if d:
26 | found_email = re.split("[;:]",d[1])[0]
27 | if found_email == t.target:
28 | # We rebuild the expected data array ["BC_PASS", "password"]
29 | new_data = [d[0], re.split("[;:]",d[1])[-1]]
30 | cleaned_data.append(new_data)
31 | else:
32 | c.info_news("Removing " + d[1] + " (cleaning function)")
33 | t.data = cleaned_data
34 |
35 | return targets
36 |
37 | def breachcomp_check(targets, breachcomp_path):
38 | breachcomp_path = os.path.join(breachcomp_path, "data")
39 | for t in targets:
40 | if len(t.target):
41 | for i in range(len(t.target)):
42 | if t.target[i].isalnum():
43 | next_dir_to_test = os.path.join(breachcomp_path, t.target[i])
44 | else:
45 | next_dir_to_test = os.path.join(breachcomp_path, "symbols")
46 | if os.path.isdir(next_dir_to_test):
47 | breachcomp_path = next_dir_to_test
48 | else:
49 | if os.path.isfile(next_dir_to_test):
50 | found_list = local_search([next_dir_to_test], [t.target])
51 | for f in found_list:
52 | t.pwned += 1
53 | t.data.append(("BC_PASS", f.content.strip()))
54 | else:
55 | c.bad_news(next_dir_to_test + " is neither a file or directory")
56 |
57 | break
58 |
59 | targets = clean_targets(targets)
60 | return targets
61 |
62 | def old_breachcomp_check(targets, breachcomp_path):
63 | # https://gist.github.com/scottlinux/9a3b11257ac575e4f71de811322ce6b3
64 | try:
65 | import subprocess
66 |
67 | check_shell()
68 | query_bin = os.path.join(breachcomp_path, "query.sh")
69 | st = os.stat(query_bin)
70 | os.chmod(query_bin, st.st_mode | stat.S_IEXEC)
71 | for t in targets:
72 | c.info_news(f"Looking up {t.target} in BreachCompilation")
73 | procfd = subprocess.run([query_bin, t.target], stdout=subprocess.PIPE)
74 | try:
75 | output = procfd.stdout.decode("cp437")
76 | except Exception as e:
77 | c.bad_news(f"Could not decode bytes for {t.target} results")
78 | output = procfd.stdout
79 | # print(output[:85], "[...]")
80 | print(output)
81 | continue
82 | if len(output) != 0:
83 | split_output = output.split("\n")
84 | for line in split_output:
85 | if ":" in line:
86 | t.pwned += 1
87 | t.data.append(("BC_PASS", re.split("[;:]",line)[-1]))
88 | c.good_news(
89 | f"Found BreachedCompilation entry {line}"
90 | )
91 | return targets
92 | except Exception as e:
93 | c.bad_news("Breach compilation")
94 | print(e)
95 | return targets
96 |
--------------------------------------------------------------------------------
/h8mail/utils/chase.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | from .colors import colors as c
5 | import re
6 |
7 | def chase(target, user_args):
8 | """
9 | Takes the current target & returns adequate chase list
10 | to add to the current target list
11 | """
12 | new_targets = []
13 | chase_counter = 0
14 | if user_args.debug:
15 | print(c.fg.red, "\nCHASING DEBUG-----------")
16 | print(f"Hunting targets from {target.target}")
17 | print(f"Recursive Chase Stop is at {user_args.chase_limit}" + c.reset)
18 | if user_args.chase_limit > 0:
19 | for d in target.data:
20 | if len(d) != 2:
21 | continue
22 |
23 | if user_args.power_chase:
24 | if "RELATED" in d[0] or "EMAIL" in d[0]:
25 | c.good_news(
26 | "Chasing {new_target} as new target".format(new_target=d[1])
27 | )
28 | new_targets.append(d[1])
29 | else: # in case there is an email as a username
30 | e = re.findall(r"[\w\.-]+@[\w\.-]+", d[1])
31 | if e:
32 | for email in e:
33 | c.good_news(
34 | "Chasing {new_target} as new target (found as pattern)".format(
35 | new_target=d[1]
36 | )
37 | )
38 | new_targets.append(d[1])
39 |
40 | else:
41 | if "HUNTER_RELATED" in d[0]:
42 | c.good_news(
43 | "Chasing {new_target} as new target".format(new_target=d[1])
44 | )
45 | new_targets.append(d[1])
46 | return new_targets
47 |
--------------------------------------------------------------------------------
/h8mail/utils/classes.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | from .intelx import intelx as i
3 | from time import sleep
4 | from .colors import colors as c
5 | import requests
6 | import json
7 | import socket
8 | import sys
9 | import platform
10 | from .version import __version__
11 |
12 |
13 | class local_breach_target:
14 | """
15 | Class is called when performing local file search.
16 | This class is meant to store found data, to be later appended to the existing target objects.
17 | local_to_targets() tranforms to target object
18 | Used by both cleartext and gzip search
19 | """
20 |
21 | def __init__(self, target_data, fp, ln, content):
22 | self.target = target_data
23 | self.filepath = fp
24 | self.line = ln
25 | self.content = content
26 |
27 | def dump(self):
28 | print(f"Email: {self.target}")
29 | print(f"Path: {self.filepath}")
30 | print(f"Line: {self.line}")
31 | print(f"Content: {self.content}")
32 | print()
33 |
34 |
35 | class target:
36 | """
37 | Main class used to create and follow breach data.
38 | Found data is stored in self.data. Each method increments self.pwned when data is found.
39 | """
40 |
41 | def __init__(self, target_data, debug=False):
42 | self.headers = {
43 | "User-Agent": "h8mail-v.{h8ver}-OSINT-and-Education-Tool (PythonVersion={pyver}; Platform={platfrm})".format(
44 | h8ver=__version__,
45 | pyver=sys.version.split(" ")[0],
46 | platfrm=platform.platform().split("-")[0],
47 | )
48 | }
49 | self.target = target_data
50 | self.pwned = 0
51 | self.data = [()]
52 | self.debug = debug
53 | if debug:
54 | print(
55 | c.fg.red,
56 | f"DEBUG: Created target object for {self.target}",
57 | c.reset,
58 | )
59 |
60 | def not_exists(self, pattern):
61 | for d in self.data:
62 | if len(d) >= 2:
63 | if d[1] == pattern:
64 | return False
65 | return True
66 |
67 | def make_request(
68 | self,
69 | url,
70 | meth="GET",
71 | timeout=20,
72 | redirs=True,
73 | data=None,
74 | params=None,
75 | verify=True,
76 | auth=None,
77 | ):
78 | try:
79 | response = requests.request(
80 | url=url,
81 | headers=self.headers,
82 | method=meth,
83 | timeout=timeout,
84 | allow_redirects=redirs,
85 | data=data,
86 | params=params,
87 | verify=verify,
88 | auth=auth,
89 | )
90 | # response = requests.request(url="http://127.0.0.1:8000", headers=self.headers, method=meth, timeout=timeout, allow_redirects=redirs, data=data, params=params)
91 | if self.debug:
92 | c.debug_news("DEBUG: Sent the following---------------------")
93 | print(self.headers)
94 | print(url, meth, data, params)
95 | c.debug_news("DEBUG: Received the following---------------------")
96 | c.debug_news(response.url)
97 | c.debug_news("DEBUG: RESPONSE HEADER---------------------")
98 | print(
99 | "\n".join(
100 | f"{k}: {v}" for k, v in response.headers.items()
101 | )
102 | )
103 | c.debug_news("DEBUG: RESPONSE BODY---------------------")
104 | print(response.content)
105 | # print(response)
106 | except Exception as ex:
107 | c.bad_news("Request could not be made for " + self.target)
108 | print(url)
109 | print(ex)
110 | print(response)
111 | return response
112 |
113 | # New HIBP API
114 | def get_hibp3(self, api_key):
115 | try:
116 | c.info_news("[" + self.target + "]>[hibp]")
117 | sleep(1.3)
118 | url = f"https://haveibeenpwned.com/api/v3/breachedaccount/{self.target}"
119 | self.headers.update({"hibp-api-key": api_key})
120 | response = self.make_request(url)
121 | if response.status_code not in [200, 404]:
122 | c.bad_news("Could not contact HIBP v3 for " + self.target)
123 | print(response.status_code)
124 | return
125 |
126 | if response.status_code == 200:
127 | data = response.json()
128 | for d in data: # Returned type is a dict of Name : Service
129 | for _, ser in d.items():
130 | self.data.append(("HIBP3", ser))
131 | self.pwned += 1
132 |
133 | c.good_news(
134 | "Found {num} breaches for {target} using HIBP v3".format(
135 | num=len(self.data) - 1, target=self.target
136 | )
137 | )
138 | self.get_hibp3_pastes()
139 | self.headers.popitem()
140 | elif response.status_code == 404:
141 | c.info_news(
142 | f"No breaches found for {self.target} using HIBP v3"
143 | )
144 |
145 | else:
146 | c.bad_news(
147 | f"HIBP v3: got API response code {response.status_code} for {self.target}"
148 | )
149 |
150 | except Exception as e:
151 | c.bad_news("haveibeenpwned v3: " + self.target)
152 | print(e)
153 |
154 | # New HIBP API
155 | def get_hibp3_pastes(self):
156 | try:
157 | c.info_news("[" + self.target + "]>[hibp-paste]")
158 | sleep(1.3)
159 | url = f"https://haveibeenpwned.com/api/v3/pasteaccount/{self.target}"
160 |
161 | response = self.make_request(url)
162 | if response.status_code not in [200, 404]:
163 | c.bad_news("Could not contact HIBP PASTE for " + self.target)
164 | print(response.status_code)
165 | print(response)
166 | return
167 |
168 | if response.status_code == 200:
169 |
170 | data = response.json()
171 | for d in data: # Returned type is a dict of Name : Service
172 | self.pwned += 1
173 | if "Pastebin" in d["Source"]:
174 | self.data.append(
175 | ("HIBP3_PASTE", "https://pastebin.com/" + d["Id"])
176 | )
177 | else:
178 | self.data.append(("HIBP3_PASTE", d["Id"]))
179 |
180 | c.good_news(
181 | "Found {num} pastes for {target} using HIBP v3 Pastes".format(
182 | num=len(data), target=self.target
183 | )
184 | )
185 |
186 | elif response.status_code == 404:
187 | c.info_news(
188 | f"No pastes found for {self.target} using HIBP v3 PASTE"
189 | )
190 | else:
191 | c.bad_news(
192 | f"HIBP v3 PASTE: got API response code {response.status_code} for {self.target}"
193 | )
194 |
195 | except Exception as ex:
196 | c.bad_news("HIBP v3 PASTE error: " + self.target)
197 | print(ex)
198 |
199 | def get_intelx(self, api_keys):
200 | try:
201 | intel_files = []
202 | intelx = i(key=api_keys["intelx_key"], ua="h8mail-v.{h8ver}-OSINT-and-Education-Tool (PythonVersion={pyver}; Platform={platfrm})".format(
203 | h8ver=__version__,
204 | pyver=sys.version.split(" ")[0],
205 | platfrm=platform.platform().split("-")[0],
206 | ))
207 | from .intelx_helpers import intelx_getsearch
208 | from .localsearch import local_search
209 | from os import remove, fspath
210 |
211 | maxfile = 10
212 | if api_keys["intelx_maxfile"]:
213 | maxfile = int(api_keys["intelx_maxfile"])
214 | search = intelx_getsearch(self.target, intelx, maxfile)
215 | if self.debug:
216 | import json
217 |
218 | print(json.dumps(search, indent=4))
219 |
220 | for record in search["records"]:
221 | filename = record["systemid"].strip() + ".txt"
222 | intel_files.append(filename)
223 | if record["media"] != 24:
224 | c.info_news(
225 | "Skipping {name}, not text ({type})".format(
226 | type=record["mediah"], name=record["name"]
227 | )
228 | )
229 | continue
230 | c.good_news(
231 | "["
232 | + self.target
233 | + "]>[intelx.io] Fetching "
234 | + record["name"]
235 | + " as file "
236 | + filename
237 | + " ("
238 | + "{:,.0f}".format(record["size"] / float(1 << 20))
239 | + " MB)"
240 | )
241 | intelx.FILE_READ(record["systemid"], 0, record["bucket"], filename)
242 | found_list = local_search([filename], [self.target])
243 | for f in found_list:
244 | self.pwned += 1
245 | self.data.append(
246 | (
247 | "INTELX.IO",
248 | "{name} | Line: {line} - {content}".format(
249 | name=record["name"].strip(),
250 | line=f.line,
251 | content=" ".join(f.content.split()),
252 | ),
253 | )
254 | )
255 | # print(contents) # Contains search data
256 | for file in intel_files:
257 | try:
258 | if self.debug:
259 | c.info_news(
260 | "["
261 | + self.target
262 | + f"]>[intelx.io] [DEBUG] Keeping {file}"
263 | )
264 | else:
265 | c.info_news(
266 | "["
267 | + self.target
268 | + f"]>[intelx.io] Removing {file}"
269 | )
270 | remove(file)
271 | except Exception as ex:
272 | c.bad_news("intelx.io cleanup error: " + self.target)
273 | print(ex)
274 |
275 | except Exception as ex:
276 | c.bad_news("intelx.io error: " + self.target)
277 | print(ex)
278 |
279 | def get_emailrepio(self, api_key=""):
280 | try:
281 | sleep(0.5)
282 | if len(api_key) != 0:
283 | self.headers.update({"Key": api_key})
284 | c.info_news("[" + self.target + "]>[emailrep.io+key]")
285 | else:
286 | c.info_news("[" + self.target + "]>[emailrep.io]")
287 | url = f"https://emailrep.io/{self.target}"
288 | response = self.make_request(url)
289 | if response.status_code not in [200, 404, 429]:
290 | c.bad_news("Could not contact emailrep for " + self.target)
291 | print(response.status_code)
292 | print(response)
293 | return
294 |
295 | if response.status_code == 429:
296 | c.info_news(
297 | "[warning] Is your emailrep key working? Get a free API key here: https://bit.ly/3b1e7Pw"
298 | )
299 | elif response.status_code == 404:
300 | c.info_news(
301 | f"No data found for {self.target} using emailrep.io"
302 | )
303 | elif response.status_code == 200:
304 | data = response.json()
305 |
306 | self.data.append(
307 | (
308 | "EMAILREP_INFO",
309 | "Reputation: {rep} | Deliverable: {deli}".format(
310 | rep=data["reputation"].capitalize(),
311 | deli=data["details"]["deliverable"],
312 | ),
313 | )
314 | )
315 |
316 | if data["details"]["credentials_leaked"] is True:
317 | self.pwned += int(data["references"]) # or inc num references
318 | if data["references"] == 1:
319 | self.data.append(
320 | (
321 | "EMAILREP_LEAKS",
322 | f"{data['references']} leaked credential",
323 | )
324 | )
325 | else:
326 | self.data.append(
327 | (
328 | "EMAILREP_LEAKS",
329 | "{} leaked credentials".format(data["references"]),
330 | )
331 | )
332 | c.good_news(
333 | "Found {num} breaches for {target} using emailrep.io".format(
334 | num=data["references"], target=self.target
335 | )
336 | )
337 | if len(data["details"]["profiles"]) != 0:
338 | for profile in data["details"]["profiles"]:
339 | self.data.append(("EMAILREP_SOCIAL", profile.capitalize()))
340 | c.good_news(
341 | "Found {num} social profiles linked to {target} using emailrep.io".format(
342 | num=len(data["details"]["profiles"]), target=self.target
343 | )
344 | )
345 | if "never" in data["details"]["last_seen"]:
346 | return
347 | self.data.append(("EMAILREP_1ST_SN", data["details"]["first_seen"]))
348 | c.good_news(
349 | "{target} was first seen on the {data}".format(
350 | data=data["details"]["first_seen"], target=self.target
351 | )
352 | )
353 | self.data.append(("EMAILREP_LASTSN", data["details"]["last_seen"]))
354 | c.good_news(
355 | "{target} was last seen on the {data}".format(
356 | data=data["details"]["last_seen"], target=self.target
357 | )
358 | )
359 | else:
360 | c.bad_news(
361 | "emailrep.io: got API response code {code} for {target}".format(
362 | code=response.status_code, target=self.target
363 | )
364 | )
365 | if len(api_key) != 0:
366 | self.headers.popitem()
367 | except Exception as ex:
368 | c.bad_news("emailrep.io error: " + self.target)
369 | print(ex)
370 |
371 | def get_scylla(self, user_query="email"):
372 | try:
373 | c.info_news("[" + self.target + "]>[scylla.so]")
374 | sleep(0.5)
375 | self.headers.update({"Accept": "application/json"})
376 | if user_query == "email":
377 | uri_scylla = 'email: "' + self.target + '"'
378 | elif user_query == "password":
379 | uri_scylla = 'password: "' + self.target + '"'
380 | elif user_query == "username":
381 | uri_scylla = 'name: "' + self.target + '"'
382 | elif user_query == "ip":
383 | uri_scylla = 'ip: "' + self.target + '"'
384 | elif user_query == "hash":
385 | uri_scylla = 'passhash: "' + self.target + '"'
386 | elif user_query == "domain":
387 | uri_scylla = 'email: "*@' + self.target + '"'
388 | url = "https://scylla.so/search?q={}".format(
389 | requests.utils.requote_uri(uri_scylla)
390 | )
391 |
392 | # https://github.com/khast3x/h8mail/issues/64
393 | response = self.make_request(
394 | url,
395 | verify=False,
396 | # auth=requests.auth.HTTPBasicAuth("sammy", "BasicPassword!"),
397 | )
398 | self.headers.popitem()
399 |
400 | if response.status_code not in [200, 404]:
401 | c.bad_news("Could not contact scylla.so for " + self.target)
402 | print(response.status_code)
403 | print(response)
404 | return
405 | data = response.json()
406 | total = 0
407 | for d in data:
408 | for field, k in d["fields"].items():
409 | if k is not None:
410 | total += 1
411 | c.good_news(
412 | "Found {num} entries for {target} using scylla.so ".format(
413 | num=total, target=self.target
414 | )
415 | )
416 | for d in data:
417 | for field, k in d["fields"].items():
418 | if "name" in field and k is not None:
419 | self.data.append(("SCYLLA_USERNAME", k))
420 | self.pwned += 1
421 | elif (
422 | "email" in field and k is not None and user_query != "email"
423 | ):
424 | self.data.append(("SCYLLA_EMAIL", k))
425 | self.pwned += 1
426 | elif "password" in field and k is not None:
427 | self.data.append(("SCYLLA_PASSWORD", k))
428 | self.pwned += 1
429 | elif "passhash" in field and k is not None:
430 | self.data.append(("SCYLLA_HASH", k))
431 | self.pwned += 1
432 | elif "passsalt" in field and k is not None:
433 | self.data.append(("SCYLLA_HASHSALT", k))
434 | self.pwned += 1
435 | elif "ip" in field and k is not None:
436 | self.data.append(("SCYLLA_LASTIP", k))
437 | self.pwned += 1
438 | if "domain" in field and k is not None:
439 | self.data.append(("SCYLLA_SOURCE", k))
440 | self.pwned += 1
441 | else:
442 | self.data.append(("SCYLLA_SOURCE", "N/A"))
443 | except Exception as ex:
444 | c.bad_news("scylla.so error: " + self.target)
445 | print(ex)
446 |
447 | def get_hunterio_public(self):
448 | try:
449 | c.info_news("[" + self.target + "]>[hunter.io public]")
450 | target_domain = self.target.split("@")[1]
451 | url = f"https://api.hunter.io/v2/email-count?domain={target_domain}"
452 | req = self.make_request(url)
453 | response = req.json()
454 | if response["data"]["total"] != 0:
455 | self.data.append(("HUNTER_PUB", response["data"]["total"]))
456 | c.good_news(
457 | "Found {num} related emails for {target} using hunter.io (public)".format(
458 | num=response["data"]["total"], target=self.target
459 | )
460 | )
461 | except Exception as ex:
462 | c.bad_news("hunter.io (public API) error: " + self.target)
463 | print(ex)
464 |
465 | def get_hunterio_private(self, api_key):
466 | try:
467 | c.info_news("[" + self.target + "]>[hunter.io private]")
468 | target_domain = self.target.split("@")[1]
469 | url = f"https://api.hunter.io/v2/domain-search?domain={target_domain}&api_key={api_key}"
470 | req = self.make_request(url)
471 | response = req.json()
472 | b_counter = 0
473 | for e in response["data"]["emails"]:
474 | self.data.append(("HUNTER_RELATED", e["value"]))
475 | b_counter += 1
476 | if self.pwned != 0:
477 | self.pwned += 1
478 | c.good_news(
479 | "Found {num} related emails for {target} using hunter.io (private)".format(
480 | num=b_counter, target=self.target
481 | )
482 | )
483 | except Exception as ex:
484 | c.bad_news(
485 | f"hunter.io (private API) error for {self.target}:"
486 | )
487 | print(ex)
488 |
489 | def get_snusbase(self, api_url, api_key, user_query):
490 | try:
491 | if user_query == "ip":
492 | user_query = "lastip"
493 | if user_query == "domain":
494 | payload = {"type": "email", "term": "%@" + self.target, "wildcard": "true"}
495 | # elif user_query == "hash": If we want hash to search for password instead of reverse searching emails from the hash
496 | # payload = {"hash": self.target}
497 | # api_url = "https://api.snusbase.com/v3/hash"
498 | else:
499 | payload = {"type": user_query, "term": self.target}
500 | c.info_news("[" + self.target + "]>[snusbase]")
501 | url = api_url
502 | self.headers.update({"authorization": api_key})
503 | # payload = {"type": user_query, "term": self.target}
504 | req = self.make_request(url, meth="POST", data=payload)
505 | self.headers.popitem()
506 | response = req.json()
507 | if "error" in response:
508 | c.bad_news("[snusbase]> " + response["error"])
509 | c.bad_news("[snusbase]> " + response["reason"])
510 | return 1
511 | if "size" in response:
512 | c.good_news(
513 | "Found {num} entries for {target} using Snusbase".format(
514 | num=response["size"], target=self.target
515 | )
516 | )
517 | for result in response["results"]:
518 | if "email" in result and self.not_exists(result["email"]):
519 | self.data.append(("SNUS_RELATED", result["email"].strip()))
520 | if "username" in result:
521 | self.data.append(("SNUS_USERNAME", result["username"]))
522 | self.pwned += 1
523 | if "password" in result:
524 | self.data.append(("SNUS_PASSWORD", result["password"]))
525 | self.pwned += 1
526 | if "hash" in result:
527 | if "salt" in result:
528 | self.data.append(
529 | (
530 | "SNUS_HASH_SALT",
531 | result["hash"].strip() + " : " + result["salt"].strip(),
532 | )
533 | )
534 | self.pwned += 1
535 | else:
536 | self.data.append(("SNUS_HASH", result["hash"]))
537 | self.pwned += 1
538 | if "lastip" in result:
539 | self.data.append(("SNUS_LASTIP", result["lastip"]))
540 | self.pwned += 1
541 | if "name" in result:
542 | self.data.append(("SNUS_NAME", result["name"]))
543 | self.pwned += 1
544 | if "db" in result and self.not_exists(result["db"]):
545 | self.data.append(("SNUS_SOURCE", result["db"]))
546 | else:
547 | self.data.append(("SNUS_SOURCE", "N/A"))
548 | except Exception as ex:
549 | c.bad_news(f"Snusbase error with {self.target}")
550 | print(ex)
551 |
552 | def get_leaklookup_pub(self, api_key):
553 | try:
554 | c.info_news("[" + self.target + "]>[leaklookup public]")
555 | url = "https://leak-lookup.com/api/search"
556 | payload = {"key": api_key, "type": "email_address", "query": self.target}
557 | req = self.make_request(url, meth="POST", data=payload, timeout=20)
558 | response = req.json()
559 | if "false" in response["error"] and len(response["message"]) != 0:
560 | c.good_news(
561 | "Found {num} entries for {target} using LeakLookup (public)".format(
562 | num=len(response["message"]), target=self.target
563 | )
564 | )
565 | for result in response["message"]:
566 | self.pwned += 1
567 | self.data.append(("LEAKLOOKUP_PUB", result))
568 | if "false" in response["error"] and len(response["message"]) == 0:
569 | c.info_news(
570 | f"No breaches found for {self.target} using Leak-lookup (public)"
571 | )
572 |
573 | except Exception as ex:
574 | c.bad_news(
575 | f"Leak-lookup error with {self.target} (public)"
576 | )
577 | print(ex)
578 |
579 | def get_leaklookup_priv(self, api_key, user_query):
580 | try:
581 | if user_query == "ip":
582 | user_query = "ipadress"
583 | if user_query in ["hash"]:
584 | c.bad_news(
585 | f"Leaklookup does not support {user_query} search (yet)"
586 | )
587 | return
588 | c.info_news("[" + self.target + "]>[leaklookup private]")
589 | url = "https://leak-lookup.com/api/search"
590 | payload = {"key": api_key, "type": user_query, "query": self.target}
591 | req = self.make_request(url, meth="POST", data=payload, timeout=60)
592 | response = req.json()
593 | if "false" in response["error"] and len(response["message"]) != 0:
594 | b_counter = 0
595 | for db, data in response["message"].items():
596 | for d in data:
597 | if "username" in d.keys():
598 | self.pwned += 1
599 | self.data.append(("LKLP_USERNAME", d["username"]))
600 | if "email_address" in d.keys() and self.not_exists(
601 | d["email_address"]
602 | ):
603 | self.data.append(
604 | ("LKLP_RELATED", d["email_address"].strip())
605 | )
606 | if "password" in d.keys():
607 | self.pwned += 1
608 | self.data.append(("LKLP_PASSWORD", d["password"]))
609 | b_counter += 1
610 | if "hash" in d.keys():
611 | self.pwned += 1
612 | self.data.append(("LKLP_HASH", d["password"]))
613 | b_counter += 1
614 | if "ipaddress" in d.keys():
615 | self.pwned += 1
616 | self.data.append(("LKLP_LASTIP", d["ipaddress"]))
617 | for tag in [
618 | "address",
619 | "address1",
620 | "address2",
621 | "country",
622 | "zip",
623 | "zipcode",
624 | "postcode",
625 | "state",
626 | ]:
627 | if tag in d.keys():
628 | self.pwned += 1
629 | self.data.append(
630 | ("LKLP_GEO", d[tag] + " (type: " + tag + ")")
631 | )
632 | for tag in [
633 | "firstname",
634 | "middlename",
635 | "lastname",
636 | "mobile",
637 | "number",
638 | "userid",
639 | ]:
640 | if tag in d.keys():
641 | self.pwned += 1
642 | self.data.append(
643 | ("LKLP_ID", d[tag] + " (type: " + tag + ")")
644 | )
645 | if self.not_exists(db):
646 | self.data.append(("LKLP_SOURCE", db))
647 |
648 | c.good_news(
649 | "Found {num} entries for {target} using LeakLookup (private)".format(
650 | num=b_counter, target=self.target
651 | )
652 | )
653 |
654 | if "false" in response["error"] and len(response["message"]) == 0:
655 | c.info_news(
656 | f"No breaches found for {self.target} using Leak-lookup (private)"
657 | )
658 | except Exception as ex:
659 | c.bad_news(
660 | f"Leak-lookup error with {self.target} (private)"
661 | )
662 | print(ex)
663 |
664 | def get_weleakinfo_priv(self, api_key, user_query):
665 | try:
666 | c.info_news("[" + self.target + "]>[weleakinfo priv]")
667 | sleep(0.4)
668 | url = "https://api.weleakinfo.com/v3/search"
669 | self.headers.update({"Authorization": "Bearer " + api_key})
670 | self.headers.update({"Content-Type": "application/x-www-form-urlencoded"})
671 |
672 | payload = {"type": user_query, "query": self.target}
673 | req = self.make_request(url, meth="POST", data=payload, timeout=30)
674 | self.headers.popitem()
675 | self.headers.popitem()
676 | response = req.json()
677 | if req.status_code == 400:
678 | c.bad_news(
679 | f"Got WLI API response code {req.status_code}: Invalid search type provided"
680 | )
681 | return
682 | elif req.status_code != 200:
683 | c.bad_news(f"Got WLI API response code {req.status_code} (private)")
684 | return
685 | if req.status_code == 200:
686 | if response["Success"] is False:
687 | c.bad_news(
688 | f"WeLeakInfo (private) error response {response['Message']}"
689 | )
690 | return
691 | c.good_news(
692 | "Found {num} entries for {target} using WeLeakInfo (private)".format(
693 | num=response["Total"], target=self.target
694 | )
695 | )
696 | self.data.append(("WLI_TOTAL", response["Total"]))
697 | if response["Total"] == 0:
698 | return
699 | for result in response["Data"]:
700 | if "Username" in result:
701 | self.data.append(("WLI_USERNAME", result["Username"]))
702 | if "Email" in result and self.not_exists(result["Email"]):
703 | self.data.append(("WLI_RELATED", result["Email"].strip()))
704 | if "Password" in result:
705 | self.data.append(("WLI_PASSWORD", result["Password"]))
706 | self.pwned += 1
707 | if "Hash" in result:
708 | self.data.append(("WLI_HASH", result["Hash"]))
709 | self.pwned += 1
710 | if "Database" in result and self.not_exists(result["Database"]):
711 | self.data.append(("WLI_SOURCE", result["Database"]))
712 | else:
713 | self.data.append(("WLI_SOURCE", "N/A"))
714 | except Exception as ex:
715 | c.bad_news(
716 | f"WeLeakInfo error with {self.target} (private)"
717 | )
718 | print(ex)
719 |
720 | def get_weleakinfo_pub(self, api_key):
721 | try:
722 | c.info_news("[" + self.target + "]>[weleakinfo public]")
723 | url = "https://api.weleakinfo.com/v3/public/email/{query}".format(
724 | query=self.target
725 | )
726 | self.headers.update({"Authorization": "Bearer " + api_key})
727 | req = self.make_request(url, timeout=30)
728 | self.headers.popitem()
729 | response = req.json()
730 | if req.status_code != 200:
731 | c.bad_news(f"Got WLI API response code {req.status_code} (public)")
732 | return
733 | else:
734 | c.good_news(
735 | "Found {num} entries for {target} using WeLeakInfo (public)".format(
736 | num=response["Total"], target=self.target
737 | )
738 | )
739 | if response["Success"] is False:
740 | c.bad_news(response["Message"])
741 | return
742 | self.data.append(("WLI_PUB_TOTAL", response["Total"]))
743 | if response["Total"] == 0:
744 | return
745 | for name, data in response["Data"].items():
746 | self.data.append(("WLI_PUB_SRC", name + " (" + str(data) + ")"))
747 | except Exception as ex:
748 | c.bad_news(
749 | f"WeLeakInfo error with {self.target} (public)"
750 | )
751 | print(ex)
752 |
753 | def get_dehashed(self, api_email, api_key, user_query):
754 | try:
755 | if user_query == "hash":
756 | user_query == "hashed_password"
757 | if user_query == "ip":
758 | user_query == "ip_address"
759 |
760 | c.info_news("[" + self.target + "]>[dehashed]")
761 | url = "https://api.dehashed.com/search?query="
762 | if user_query == "domain":
763 | search_query = "email" + ":" + '"*@' + self.target + '"'
764 | else:
765 | search_query = user_query + ":" + '"' + self.target + '"'
766 | self.headers.update({"Accept": "application/json"})
767 | req = self.make_request(
768 | url + search_query, meth="GET", timeout=60, auth=(api_email, api_key)
769 | )
770 | if req.status_code == 200:
771 | response = req.json()
772 | if response["total"] is not None:
773 | c.good_news(
774 | "Found {num} entries for {target} using Dehashed.com".format(
775 | num=str(response["total"]), target=self.target
776 | )
777 | )
778 |
779 | for result in response["entries"]:
780 | if (
781 | "username" in result
782 | and result["username"] is not None
783 | and len(result["username"].strip()) > 0
784 | ):
785 | self.data.append(("DHASHD_USERNAME", result["username"]))
786 | if (
787 | "email" in result
788 | and self.not_exists(result["email"])
789 | and result["email"] is not None
790 | and len(result["email"].strip()) > 0
791 | ):
792 | self.data.append(("DHASHD_RELATED", result["email"].strip()))
793 | if (
794 | "password" in result
795 | and result["password"] is not None
796 | and len(result["password"].strip()) > 0
797 | ):
798 | self.data.append(("DHASHD_PASSWORD", result["password"]))
799 | self.pwned += 1
800 | if (
801 | "hashed_password" in result
802 | and result["hashed_password"] is not None
803 | and len(result["hashed_password"].strip()) > 0
804 | ):
805 | self.data.append(("DHASHD_HASH", result["hashed_password"]))
806 | self.pwned += 1
807 | for tag in ["name", "vin", "address", "phone"]:
808 | if (
809 | tag in result
810 | and result[tag] is not None
811 | and len(result[tag].strip()) > 0
812 | ):
813 | self.data.append(
814 | ("DHASHD_ID", result[tag] + " (type: " + tag + ")")
815 | )
816 | self.pwned += 1
817 | # Documentation and JSON are not synced, using both source keys
818 | if "obtained_from" in result and self.not_exists(
819 | result["obtained_from"]
820 | ):
821 | self.data.append(("DHASHD_SOURCE", result["obtained_from"]))
822 | elif "database_name" in result and self.not_exists(
823 | result["database_name"]
824 | ):
825 | self.data.append(("DHASHD_SOURCE", result["database_name"]))
826 | else:
827 | self.data.append(("DHASHD_SOURCE", "N/A"))
828 | if response["balance"] is not None:
829 | self.data.append(
830 | (
831 | "DHASHD_CREDITS",
832 | str(response["balance"]) + " DEHASHED CREDITS REMAINING",
833 | )
834 | )
835 | else:
836 | c.bad_news("Dehashed error: status code " + str(req.status_code))
837 | self.headers.popitem()
838 | except Exception as ex:
839 | c.bad_news(f"Dehashed error with {self.target}")
840 | print(ex)
841 |
842 | def get_breachdirectory(self, user, passw, user_query):
843 | # Todo: implement password source search when email has answer
844 | c.info_news("[" + self.target + "]>[breachdirectory.org]")
845 | if user_query not in ["email", "username", "password", "domain"]:
846 | c.bad_news("Breachdirectory does not support this option")
847 | exit(1)
848 | mode = "pastes"
849 | url = "https://breachdirectory.org/api/index?username={user}&password={passw}&func={mode}&term={target}".format(user=user, passw=passw, mode=mode, target=self.target)
850 | try:
851 | req = self.make_request(
852 | url, timeout=60
853 | )
854 | if req.status_code == 200:
855 | response = req.json()
856 | if response["data"] is not None:
857 | for result in response["data"]:
858 | if "email" in result and "email" not in user_query:
859 | self.data.append(("BREACHDR_EMAIL", result["email"]))
860 | if "password" in result:
861 | self.data.append(("BREACHDR_PASS", result["password"]))
862 | if "hash" in result:
863 | self.data.append(("BREACHDR_HASH", result["hash"]))
864 | if "source" in result:
865 | self.data.append(("BREACHDR_SOURCE", result["source"]))
866 | self.pwned += 1
867 | else:
868 | self.data.append(("BREACHDR_SOURCE", "N/A"))
869 | # Follow up with an aggregated leak sources query
870 | url_src = "https://breachdirectory.org/api/index?username={user}&password={passw}&func={mode}&term={target}".format(user=user, passw=passw, mode="sources", target=self.target)
871 | req = self.make_request(
872 | url_src, timeout=60
873 | )
874 | if req.status_code == 200:
875 | response = req.json()
876 | if response["sources"] is not None:
877 | for result in response["sources"]:
878 | self.data.append(("BREACHDR_EXTSRC", result))
879 | ## If using the 'auto' mode instead of pastes
880 | # c.good_news(
881 | # "Found {num} entries for {target} using breachdirectory.org".format(
882 | # num=str(response["found"]), target=self.target
883 | # )
884 | # )
885 |
886 | # for result in response["result"]:
887 | # if result["has_password"] is True:
888 | # self.data.append(("BREACHDR_PASS", result["password"]))
889 | # self.data.append(("BREACHDR_MD5", result["md5"]))
890 | # if result["sources"] == "Unverified":
891 | # source = result["sources"]
892 | # elif len(result["sources"]) > 1:
893 | # source = ", ".join(result["sources"])
894 | # else:
895 | # source = result["sources"][0]
896 | # self.data.append(("BREACHDR_SOURCE", source))
897 | # self.pwned += 1
898 | except Exception as ex:
899 | c.bad_news(f"Breachdirectory error with {self.target}")
900 | print(ex)
901 |
--------------------------------------------------------------------------------
/h8mail/utils/colors.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 |
4 | class colors:
5 | """Colors class:
6 | reset all colors with colors.reset
7 | two subclasses fg for foreground and bg for background.
8 | use as colors.subclass.colorname.
9 | i.e. colors.fg.red or colors.bg.green
10 | also, the generic bold, disable, underline, reverse, strikethrough,
11 | and invisible work with the main class
12 | i.e. colors.bold
13 | """
14 |
15 | reset = "\033[0m"
16 | bold = "\033[01m"
17 | disable = "\033[02m"
18 | underline = "\033[04m"
19 | reverse = "\033[07m"
20 | strikethrough = "\033[09m"
21 | invisible = "\033[08m"
22 |
23 | class fg:
24 | black = "\033[30m"
25 | red = "\033[31m"
26 | green = "\033[32m"
27 | orange = "\033[33m"
28 | blue = "\033[34m"
29 | purple = "\033[35m"
30 | cyan = "\033[36m"
31 | lightgrey = "\033[37m"
32 | darkgrey = "\033[90m"
33 | lightred = "\033[91m"
34 | lightgreen = "\033[92m"
35 | yellow = "\033[93m"
36 | lightblue = "\033[94m"
37 | pink = "\033[95m"
38 | lightcyan = "\033[96m"
39 |
40 | class bg:
41 | black = "\033[40m"
42 | red = "\033[41m"
43 | green = "\033[42m"
44 | orange = "\033[43m"
45 | blue = "\033[44m"
46 | purple = "\033[45m"
47 | cyan = "\033[46m"
48 | lightgrey = "\033[47m"
49 |
50 | @staticmethod
51 | def good_news(news):
52 | """
53 | Print a Success
54 | """
55 | print(colors.bold + colors.fg.green + "[>] " + colors.reset + news.strip())
56 |
57 | @staticmethod
58 | def debug_news(news):
59 | """
60 | Print a Debug
61 | """
62 | print()
63 | print(colors.bold + colors.fg.lightred + "[@] " + news + colors.reset)
64 |
65 | @staticmethod
66 | def bad_news(news):
67 | """
68 | Print a Failure, error
69 | """
70 | print(colors.bold + colors.fg.red + "[!] " + colors.reset + news.strip())
71 |
72 | @staticmethod
73 | def info_news(news):
74 | """
75 | Print an information with grey text
76 | """
77 | print(
78 | colors.bold
79 | + colors.fg.lightblue
80 | + "[~] "
81 | + colors.reset
82 | + colors.fg.lightgrey
83 | + news.strip()
84 | + colors.reset
85 | )
86 |
87 | @staticmethod
88 | def question_news(news):
89 | """
90 | Print an information with yellow text
91 | """
92 | print(
93 | colors.bold
94 | + colors.fg.blue
95 | + "[?] "
96 | + colors.reset
97 | + colors.fg.yellow
98 | + news.strip()
99 | + colors.reset
100 | )
101 |
102 | @staticmethod
103 | def print_result(target, data, source):
104 | """
105 | Print Breach results
106 | """
107 | if "PASS" in source:
108 | print(
109 | "{}{}{:15}{}|{}{:>25.25}{} > {}{}{}{}".format(
110 | colors.fg.lightblue,
111 | colors.bold,
112 | source,
113 | colors.fg.lightgrey,
114 | colors.fg.pink,
115 | target,
116 | colors.fg.lightgrey,
117 | colors.bold,
118 | colors.fg.green,
119 | data,
120 | colors.reset,
121 | )
122 | )
123 | elif "LOCALSEARCH" in source:
124 | if len(data) > 140:
125 | print(
126 | "{}{}{:15}{}|{}{:>25.25}{} > {}{}{}{}".format(
127 | colors.fg.lightblue,
128 | colors.bold,
129 | source,
130 | colors.fg.lightgrey,
131 | colors.fg.pink,
132 | target,
133 | colors.fg.lightgrey,
134 | colors.bold,
135 | colors.fg.green,
136 | "[...]" + data[-135:],
137 | colors.reset,
138 | )
139 | )
140 | else:
141 | print(
142 | "{}{}{:15}{}|{}{:>25.25}{} > {}{}{}{}".format(
143 | colors.fg.lightblue,
144 | colors.bold,
145 | source,
146 | colors.fg.lightgrey,
147 | colors.fg.pink,
148 | target,
149 | colors.fg.lightgrey,
150 | colors.bold,
151 | colors.fg.green,
152 | data,
153 | colors.reset,
154 | )
155 | )
156 | # Underscore to avoid coloring like a HASH
157 | elif "_HASH" in source:
158 | print(
159 | "{}{:15}{}|{}{:>25.25}{} > {}{}{}".format(
160 | colors.fg.lightblue,
161 | source,
162 | colors.fg.lightgrey,
163 | colors.fg.pink,
164 | target,
165 | colors.fg.lightgrey,
166 | colors.fg.red,
167 | data,
168 | colors.reset,
169 | )
170 | )
171 | # Underscore to avoid coloring service with "email" in name
172 | elif "_EMAIL" in source:
173 | print(
174 | "{}{:15}{}|{}{:>25.25}{} > {}{}{}".format(
175 | colors.fg.lightblue,
176 | source,
177 | colors.fg.lightgrey,
178 | colors.fg.pink,
179 | target,
180 | colors.fg.lightgrey,
181 | colors.fg.lightgrey,
182 | data,
183 | colors.reset,
184 | )
185 | )
186 | elif "USER" in source:
187 | print(
188 | "{}{:15}{}|{}{:>25.25}{} > {}{}{}".format(
189 | colors.fg.lightblue,
190 | source,
191 | colors.fg.lightgrey,
192 | colors.fg.pink,
193 | target,
194 | colors.fg.lightgrey,
195 | colors.fg.lightcyan,
196 | data,
197 | colors.reset,
198 | )
199 | )
200 | elif "SOURCE" in source:
201 | print(
202 | "{}{:15}{}|{}{:>25.25}{} > {}{}{}\n".format(
203 | colors.fg.lightblue,
204 | source,
205 | colors.fg.lightgrey,
206 | colors.fg.pink,
207 | target,
208 | colors.fg.lightgrey,
209 | colors.reset,
210 | data,
211 | colors.reset,
212 | )
213 | )
214 | elif "IP" in source:
215 | print(
216 | "{}{:15}{}|{}{:>25.25}{} > {}{}{}".format(
217 | colors.fg.lightblue,
218 | source,
219 | colors.fg.lightgrey,
220 | colors.fg.pink,
221 | target,
222 | colors.fg.lightgrey,
223 | colors.fg.red,
224 | data,
225 | colors.reset,
226 | )
227 | )
228 | else:
229 | print(
230 | "{}{:15}{}|{}{:>25.25}{} > {}{}{}".format(
231 | colors.fg.lightblue,
232 | source,
233 | colors.fg.lightgrey,
234 | colors.fg.pink,
235 | target,
236 | colors.fg.lightgrey,
237 | colors.fg.lightgrey,
238 | data,
239 | colors.reset,
240 | )
241 | )
242 |
243 | @staticmethod
244 | def print_res_header(target):
245 | """
246 | Print Breach result header
247 | """
248 | print(colors.bold, "{:_^90}\n".format(""), colors.reset)
249 | print(
250 | colors.bold
251 | + colors.fg.green
252 | + "[>] "
253 | + colors.reset
254 | + "Showing results for "
255 | + target
256 | + colors.reset
257 | )
258 |
--------------------------------------------------------------------------------
/h8mail/utils/gen_config.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from .colors import colors as c
3 | import os
4 |
5 |
6 | def gen_config_file():
7 | with open(
8 | os.path.join(os.getcwd(), "h8mail_config.ini"), "w", newline=""
9 | ) as dest_config:
10 | config = """[h8mail]
11 | ; h8mail will automatically detect present keys & launch services accordingly
12 | ; Uncomment to activate
13 | ;hunterio =
14 | ;snusbase_token =
15 | ;;weleakinfo_priv =
16 | ;;weleakinfo_pub =
17 | ;hibp =
18 | ;leak-lookup_pub = 1bf94ff907f68d511de9a610a6ff9263
19 | ;leak-lookup_priv =
20 | ;emailrep =
21 | ;dehashed_email =
22 | ;dehashed_key =
23 | ;intelx_key =
24 | ;intelx_maxfile = 10
25 | ;breachdirectory_user =
26 | ;breachdirectory_pass =
27 | """
28 | dest_config.write(config)
29 | c.good_news(
30 | "Generated h8mail template configuration file ({})".format(
31 | os.path.join(os.getcwd(), "h8mail_config.ini")
32 | )
33 | )
34 |
--------------------------------------------------------------------------------
/h8mail/utils/helpers.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import re
5 | from .colors import colors as c
6 | import configparser
7 | import csv
8 | import os
9 | import glob
10 | from .version import __version__
11 | import requests
12 | import json
13 |
14 |
15 | def find_files(to_parse, pattern=""):
16 | """
17 | Returns list of files from t_parse filepath.
18 | Supports using globing (*) in filepaths.
19 | Can check for patterns such as 'gz'.
20 | """
21 | allfiles = []
22 | if "*" in to_parse:
23 | glob_result = glob.glob(to_parse)
24 | for g in glob_result:
25 | allfiles.append(g)
26 | c.info_news(f"Using file {g}")
27 | if os.path.isfile(to_parse):
28 | if pattern in to_parse:
29 | c.info_news(f"Using file {to_parse}")
30 | allfiles.append(to_parse)
31 | elif os.path.isdir(to_parse):
32 | for root, _, filenames in os.walk(to_parse):
33 | for filename in filenames:
34 | if pattern in filename:
35 | c.info_news("Using file {}".format(os.path.join(root, filename)))
36 | allfiles.append(os.path.join(root, filename))
37 | return allfiles
38 |
39 |
40 | def print_banner(b_type="intro"):
41 | if "intro" in b_type:
42 | banner = """
43 | ._____. ._____. ;____________;
44 | | ._. | | ._. | ; h8mail ;
45 | | !_| |_|_|_! | ;------------;
46 | !___| |_______! Heartfelt Email OSINT
47 | .___|_|_| |___. Use responsibly
48 | | ._____| |_. | ;____________________;
49 | | !_! | | !_! | ; github.com/khast3x ;
50 | !_____! !_____! ;--------------------;
51 | """
52 | # print(c.bold, c.fg.pink, banner, c.reset)
53 | banner_tab = banner.splitlines()
54 | code = 33
55 | keep = True
56 | for b in banner_tab:
57 | clr = "\u001b[38;5;" + str(code) + "m "
58 | print(c.bold + clr + b + c.reset)
59 | if keep:
60 | code += 36
61 | keep = False
62 | else:
63 | keep = True
64 | elif "warn" in b_type:
65 | print(
66 | c.fg.lightgrey,
67 | "\t\t Official h8mail posts: \n\t\t https://khast3x.club/tags/h8mail/\n\n",
68 | c.reset,
69 | )
70 | elif "version" in b_type:
71 | print(
72 | "\t",
73 | c.fg.cyan,
74 | "Version " + __version__ + ' - "ROCKSMASSON.6" ',
75 | c.reset,
76 | )
77 |
78 |
79 | def fetch_emails(target, user_args):
80 | """
81 | Returns a list of emails found in 'target'.
82 | Can be loosy to skip email pattern search.
83 | """
84 | if user_args.loose or user_args.user_query is not None:
85 | t = target.split(" ")
86 | # print(t)
87 | return t
88 | e = re.findall(r"[\w\.-]+@[\w\.-]+", target)
89 | if e:
90 | # print(", ".join(e), c.reset)
91 | return e
92 | return None
93 |
94 |
95 | def get_emails_from_file(targets_file, user_args):
96 | """
97 | For each line in file, check for emails using fetch_emails().
98 | Returns list of emails.
99 | """
100 | email_obj_list = []
101 | try:
102 | target_fd = open(targets_file).readlines()
103 | c.info_news("Parsing emails from" + targets_file)
104 | for line in target_fd:
105 | e = fetch_emails(line.strip(), user_args)
106 | if e is None:
107 | continue
108 | else:
109 | email_obj_list.extend(e)
110 | return email_obj_list
111 | except Exception as ex:
112 | c.bad_news("Problems occurred while trying to get emails from file")
113 | print(ex)
114 |
115 |
116 | def get_config_from_file(user_args):
117 | """
118 | Read config in file. If keys are passed using CLI, add them to the configparser object.
119 | Returns a configparser object already set to "DEFAULT" section.
120 | """
121 |
122 | try:
123 | config = configparser.ConfigParser()
124 | # Config file
125 | if user_args.config_file:
126 | for counter, config_file in enumerate(user_args.config_file):
127 | config_file = user_args.config_file[counter]
128 | config.read(config_file)
129 | # Use -k option
130 | if user_args.cli_apikeys:
131 | if config.has_section("h8mail") is False:
132 | config.add_section("h8mail")
133 | for counter, user_key in enumerate(user_args.cli_apikeys):
134 | user_cli_keys = user_args.cli_apikeys[counter].split(",")
135 | for user_key in user_cli_keys:
136 | if user_key and "=" in user_key:
137 | config.set(
138 | "h8mail",
139 | user_key.split("=", maxsplit=1)[0].strip(),
140 | user_key.split("=", maxsplit=1)[1].strip(),
141 | )
142 | for k in config["h8mail"]:
143 | if len((config["h8mail"][k])) != 0:
144 | c.good_news(f"Found {k} configuration key")
145 | return config["h8mail"]
146 | except Exception as ex:
147 | c.bad_news("Problems occurred while trying to get configuration file")
148 | print(ex)
149 |
150 |
151 | def save_results_csv(dest_csv, target_obj_list):
152 | """
153 | Outputs CSV from target object list.
154 | Dumps the target.data object variable into CSV file.
155 | """
156 | with open(dest_csv, "w", newline="") as csvfile:
157 | try:
158 | writer = csv.writer(csvfile)
159 |
160 | writer.writerow(["Target", "Type", "Data"])
161 | c.good_news("Writing to CSV")
162 | for t in target_obj_list:
163 | for i in range(len(t.data)):
164 | if len(t.data[i]) == 2: # Contains data header + body
165 | writer.writerow([t.target, t.data[i][0], t.data[i][1]])
166 | except Exception as ex:
167 | c.bad_news("Error writing to csv")
168 | print(ex)
169 |
170 |
171 | def check_latest_version():
172 | """
173 | Fetches local version and compares it to github api tag version
174 | """
175 | try:
176 | response = requests.request(
177 | url="https://api.github.com/repos/khast3x/h8mail/releases/latest", method="GET"
178 | )
179 | data = response.json()
180 | latest = data["tag_name"]
181 | if __version__ == data["tag_name"]:
182 | c.good_news("h8mail is up to date")
183 | else:
184 | c.bad_news(
185 | "Not running latest h8mail version. [Current: {current} | Latest: {latest}]".format(
186 | current=__version__, latest=latest
187 | )
188 | )
189 | except Exception:
190 | c.bad_news("Could not check for updates. Is Github blocking requests?")
191 | def check_scylla_online():
192 | """
193 | Checks if scylla.so is online
194 | """
195 | # Supress SSL Warning on UI
196 | # https://github.com/khast3x/h8mail/issues/64
197 | try:
198 | re = requests.head(
199 | url="https://scylla.so", verify=False, auth=requests.auth.HTTPBasicAuth("sammy", "BasicPassword!"), timeout=10,
200 | )
201 | if re.status_code == 200:
202 | c.good_news("scylla.so is up")
203 | return True
204 | else:
205 | c.info_news("scylla.so is down, skipping")
206 | return False
207 | except Exception:
208 | c.info_news("scylla.so is down, skipping")
209 |
--------------------------------------------------------------------------------
/h8mail/utils/intelx.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import requests
4 | import time
5 | import json
6 | import sys
7 | import re
8 |
9 | class intelx:
10 |
11 | # API_ROOT = 'https://public.intelx.io'
12 | API_ROOT = ''
13 | API_KEY = ''
14 | USER_AGENT = ''
15 |
16 | # If an API key isn't supplied, it will use the free API key (limited functionality)
17 | def __init__(self, key="01a61412-7629-4288-b18a-b287266f2798", ua='IX-Python/0.5'):
18 | """
19 | Initialize API by setting the API key.
20 | """
21 | if key == "01a61412-7629-4288-b18a-b287266f2798" or key == "ac572eea-3902-4e9a-972d-f5996d76174c":
22 | self.API_ROOT = "https://public.intelx.io"
23 | else:
24 | self.API_ROOT = "https://2.intelx.io"
25 |
26 | self.API_KEY = key
27 | self.USER_AGENT = ua
28 |
29 | def get_error(self, code):
30 | """
31 | Get error string by respective HTTP response code.
32 | """
33 | if code == 200:
34 | return "200 | Success"
35 | if code == 204:
36 | return "204 | No Content"
37 | if code == 400:
38 | return "400 | Bad Request"
39 | if code == 401:
40 | return "401 | Unauthorized"
41 | if code == 402:
42 | return "402 | Payment required."
43 | if code == 404:
44 | return "404 | Not Found"
45 |
46 | def cleanup_treeview(self, treeview):
47 | """
48 | Cleans up treeview output from the API.
49 | """
50 | lines = []
51 | for line in treeview.split("\r\n"):
52 | if '[intelx.io] Starting search")
11 | cap = intelx.GET_CAPABILITIES()
12 | # print(cap["buckets"])
13 | # import json
14 | # print(json.dumps(cap, indent=4))
15 | c.info_news(
16 | "["
17 | + target
18 | + "]>[intelx.io] Search credits remaining : {creds}".format(
19 | creds=cap["paths"]["/intelligent/search"]["Credit"]
20 | )
21 | )
22 | available_buckets = []
23 | desired_buckets=["leaks.public", "leaks.private", "pastes", "darknet"],
24 | for b in cap["buckets"]:
25 | if any(b in d for d in desired_buckets):
26 | available_buckets.append(b)
27 | c.info_news("[" + target + "]>[intelx.io] Available buckets for h8mail:")
28 | print(available_buckets)
29 | c.info_news("[" + target + "]>[intelx.io] Search in progress (max results : "+ str(maxfile) + ")")
30 | search = intelx.search(
31 | target,
32 | buckets=available_buckets,
33 | maxresults=maxfile,
34 | media=24,
35 | )
36 | c.good_news("[" + target + "]>[intelx.io] Search returned the following files :")
37 | for record in search["records"]:
38 | c.good_news("Name: " + record["name"])
39 | c.good_news("Bucket: " + record["bucket"])
40 | c.good_news(
41 | "Size: " + "{:,.0f}".format(record["size"] / float(1 << 20)) + " MB"
42 | )
43 | c.info_news("Storage ID: " + record["storageid"])
44 | print("----------")
45 | return search
46 |
--------------------------------------------------------------------------------
/h8mail/utils/localgzipsearch.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | from multiprocessing import Pool
4 | from .classes import local_breach_target
5 | from .colors import colors as c
6 | from .localsearch import raw_in_count, progress
7 | import gzip
8 | import os
9 | import sys
10 | import signal
11 |
12 |
13 | def progress_gzip(count):
14 | """
15 | Prints count without rewriting to stdout
16 | """
17 | sys.stdout.write("Lines checked:%i\r" % (count))
18 | sys.stdout.write("\033[K")
19 |
20 |
21 | def gzip_worker(filepath, target_list):
22 | """
23 | Searches for every email from target_list in every line of filepath.
24 | Uses python native gzip lib to decompress file line by line.
25 | Archives with multiple files are read as long single files.
26 | Attempts to decode line using cp437. If it fails, catch and use raw data.
27 | """
28 | try:
29 | found_list = []
30 | size = os.stat(filepath).st_size
31 | with gzip.open(filepath, "r") as gzipfile:
32 | c.info_news(
33 | "Worker [{PID}] is searching for targets in {filepath} ({size:,.0f} MB)".format(
34 | PID=os.getpid(), filepath=filepath, size=size / float(1 << 20)
35 | )
36 | )
37 | for cnt, line in enumerate(gzipfile):
38 | for t in target_list:
39 | if t in str(line):
40 | try:
41 | decoded = str(line, "cp437")
42 | found_list.append(
43 | local_breach_target(t, filepath, cnt, decoded)
44 | )
45 | c.good_news(
46 | f"Found occurrence [{filepath}] Line {cnt}: {decoded}"
47 | )
48 | except Exception as e:
49 | c.bad_news(
50 | f"Got a decoding error line {cnt} - file: {filepath}"
51 | )
52 | c.good_news(
53 | f"Found occurrence [{filepath}] Line {cnt}: {line}"
54 | )
55 | found_list.append(
56 | local_breach_target(t, filepath, cnt, str(line))
57 | )
58 | return found_list
59 | except Exception as e:
60 | c.bad_news("Something went wrong with gzip worker")
61 | print(e)
62 |
63 |
64 | def local_gzip_search(files_to_parse, target_list):
65 | """
66 | Receives list of all files to check for target_list.
67 | Starts a worker pool, one worker per file.
68 | Return list of local_breach_targets objects to be tranformed in target objects.
69 | """
70 | original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
71 | pool = Pool()
72 | found_list = []
73 | signal.signal(signal.SIGINT, original_sigint_handler)
74 | # Launch
75 | try:
76 | async_results = [
77 | pool.apply_async(gzip_worker, args=(f, target_list))
78 | for i, f in enumerate(files_to_parse)
79 | ]
80 | for r in async_results:
81 | if r.get() is not None:
82 | found_list.extend(r.get(60))
83 | except KeyboardInterrupt:
84 | c.bad_news("Caught KeyboardInterrupt, terminating workers")
85 | pool.terminate()
86 | else:
87 | c.info_news("Terminating worker pool")
88 | pool.close()
89 | pool.join()
90 | return found_list
91 |
92 |
93 | def local_search_single_gzip(files_to_parse, target_list):
94 | """
95 | Single process searching of every target_list emails, in every files_to_parse list.
96 | To be used when stability for big files is a priority
97 | Return list of local_breach_target objects to be tranformed in target objects
98 | """
99 | found_list = []
100 | for file_to_parse in files_to_parse:
101 | with gzip.open(file_to_parse, "r") as fp:
102 | size = os.stat(file_to_parse).st_size
103 | c.info_news(
104 | f"Searching for targets in {file_to_parse} ({size} bytes)"
105 | )
106 | for cnt, line in enumerate(fp):
107 | progress_gzip(cnt)
108 | for t in target_list:
109 | if t in str(line):
110 | try:
111 | decoded = str(line, "cp437")
112 | found_list.append(
113 | local_breach_target(t, file_to_parse, cnt, decoded)
114 | )
115 | c.good_news(
116 | f"Found occurrence [{file_to_parse}] Line {cnt}: {decoded}"
117 | )
118 | except:
119 | c.bad_news(
120 | f"Got a decoding error line {cnt} - file: {file_to_parse}"
121 | )
122 | c.good_news(
123 | f"Found occurrence [{file_to_parse}] Line {cnt}: {line}"
124 | )
125 | found_list.append(
126 | local_breach_target(t, file_to_parse, cnt, str(line))
127 | )
128 | return found_list
129 |
--------------------------------------------------------------------------------
/h8mail/utils/localsearch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import os
5 | import sys
6 | import signal
7 |
8 | from multiprocessing import Pool
9 | from itertools import takewhile, repeat
10 | from .classes import local_breach_target
11 | from .colors import colors as c
12 |
13 |
14 | def local_to_targets(targets, local_results, user_args):
15 | """
16 | Appends data from local_breach_target objects using existing list of targets.
17 | Finds corresponding email in dest object list, and adds data to the t.data object variable.
18 | Full output line is stored in t.data[1] and original found data in t.data[2]
19 |
20 | """
21 | for t in targets:
22 | for l in local_results:
23 | if l.target == t.target:
24 | t.data.append(
25 | (
26 | "LOCALSEARCH",
27 | f"[{os.path.basename(l.filepath)}] Line {l.line}: {l.content}".strip(),
28 | l.content.strip(),
29 | )
30 | )
31 | t.pwned += 1
32 | if user_args.debug:
33 | c.debug_news(f"DEBUG: Found following content matching {t.target.target}")
34 | l.target.dump()
35 | return targets
36 |
37 |
38 | def raw_in_count(filename):
39 | """
40 | StackOverflow trick to rapidly count lines in big files.
41 | Returns total line number.
42 | """
43 | c.info_news("Identifying total line number...")
44 | f = open(filename, "rb")
45 | bufgen = takewhile(lambda x: x, (f.raw.read(1024 * 1024) for _ in repeat(None)))
46 | return sum(buf.count(b"\n") for buf in bufgen)
47 |
48 |
49 | def worker(filepath, target_list):
50 | """
51 | Searches for every email from target_list in every line of filepath.
52 | Attempts to decode line using cp437. If it fails, catch and use raw data
53 | """
54 | try:
55 | with open(filepath, "rb") as fp:
56 | found_list = []
57 | size = os.stat(filepath).st_size
58 | c.info_news(
59 | "Worker [{PID}] is searching for targets in {filepath} ({size:,.0f} MB)".format(
60 | PID=os.getpid(), filepath=filepath, size=size / float(1 << 20))
61 | )
62 | for cnt, line in enumerate(fp):
63 | for t in target_list:
64 | if t in str(line, "cp437"):
65 | try:
66 | decoded = str(line, "cp437")
67 | found_list.append(
68 | local_breach_target(t, filepath, cnt, decoded)
69 | )
70 | c.good_news(
71 | f"Found occurrence [{filepath}] Line {cnt}: {decoded}"
72 | )
73 | except Exception as e:
74 | c.bad_news(
75 | f"Got a decoding error line {cnt} - file: {filepath}"
76 | )
77 | c.good_news(
78 | f"Found occurrence [{filepath}] Line {cnt}: {line}"
79 | )
80 | found_list.append(
81 | local_breach_target(t, filepath, cnt, str(line))
82 | )
83 | return found_list
84 | except Exception as e:
85 | c.bad_news("Something went wrong with worker")
86 | print(e)
87 |
88 |
89 | def local_search(files_to_parse, target_list):
90 | original_sigint_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
91 | pool = Pool()
92 | found_list = []
93 | signal.signal(signal.SIGINT, original_sigint_handler)
94 | try:
95 | async_results = [
96 | pool.apply_async(worker, args=(f, target_list))
97 | for i, f in enumerate(files_to_parse)
98 | ]
99 | for r in async_results:
100 | if r.get() is not None:
101 | found_list.extend(r.get(60))
102 | except KeyboardInterrupt:
103 | c.bad_news("Caught KeyboardInterrupt, terminating workers")
104 | pool.terminate()
105 | else:
106 | c.info_news("Terminating worker pool")
107 | pool.close()
108 | pool.join()
109 | return found_list
110 |
111 |
112 | def progress(count, total, status=""):
113 | bar_len = 60
114 | filled_len = int(round(bar_len * count / float(total)))
115 |
116 | percents = round(100.0 * count / float(total), 1)
117 | bar = "=" * filled_len + "-" * (bar_len - filled_len)
118 |
119 | sys.stdout.write("[%s] %s%s ...%s\r" % (bar, percents, "%", status))
120 | sys.stdout.write("\033[K")
121 |
122 |
123 | sys.stdout.flush()
124 |
125 |
126 | def local_search_single(files_to_parse, target_list):
127 | found_list = []
128 | for file_to_parse in files_to_parse:
129 | with open(file_to_parse, "rb") as fp:
130 | size = os.stat(file_to_parse).st_size
131 | lines_no = raw_in_count(file_to_parse)
132 | c.info_news(
133 | f"Searching for targets in {file_to_parse} ({size} bytes, {lines_no} lines)"
134 | )
135 | for cnt, line in enumerate(fp):
136 | lines_left = lines_no - cnt
137 | progress(
138 | cnt, lines_no, f"{cnt} lines checked - {lines_left} lines left"
139 | )
140 | for t in target_list:
141 | if t in str(line):
142 | try:
143 | decoded = str(line, "cp437")
144 | found_list.append(
145 | local_breach_target(t, file_to_parse, cnt, decoded)
146 | )
147 | c.good_news(
148 | f"Found occurrence [{file_to_parse}] Line {cnt}: {decoded}"
149 | )
150 | except:
151 | c.bad_news(
152 | f"Got a decoding error line {cnt} - file: {file_to_parse}"
153 | )
154 | c.good_news(
155 | f"Found occurrence [{file_to_parse}] Line {cnt}: {line}"
156 | )
157 | found_list.append(
158 | local_breach_target(t, file_to_parse, cnt, str(line))
159 | )
160 | return found_list
161 |
--------------------------------------------------------------------------------
/h8mail/utils/print_json.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from .colors import colors as c
3 | import json
4 |
5 | def generate_source_arrays(pwned_data):
6 | data_array = []
7 | no_src = 0 # To check for data with no explicit source
8 | temp_array = []
9 | for i in range(len(pwned_data)):
10 | if len(pwned_data[i]) == 2:
11 | temp_array.append(pwned_data[i][0] + ":" + str(pwned_data[i][1]))
12 | no_src += 1
13 | if "SOURCE" in pwned_data[i][0]:
14 | data_array.append(temp_array)
15 | temp_array = []
16 | no_src = 0
17 | if no_src > 0:
18 | data_array.append(temp_array)
19 | return data_array
20 |
21 |
22 |
23 | def save_results_json(dest_json, target_obj_list):
24 | data = {}
25 | data['targets'] = []
26 | for t in target_obj_list:
27 | current_target = {}
28 | current_target["target"] = t.target
29 | current_target["pwn_num"] = t.pwned
30 | current_target["data"] = generate_source_arrays(t.data)
31 | data['targets'].append(current_target)
32 |
33 | with open(dest_json, 'w') as outfile:
34 | json.dump(data, outfile)
--------------------------------------------------------------------------------
/h8mail/utils/print_results.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | from .colors import colors as c
3 | from time import sleep
4 |
5 | def print_results(results, hide=False):
6 |
7 | for t in results:
8 | print()
9 | c.print_res_header(t.target)
10 | for i in range(len(t.data)):
11 | # sleep(0.001)
12 | if len(t.data) == 1:
13 | print()
14 | c.info_news("No results founds")
15 | continue
16 | if len(t.data[i]) >= 2: # Contains header + body data
17 | if hide:
18 | if "PASS" in t.data[i][0]:
19 | c.print_result(
20 | t.target, t.data[i][1][:4] + "********", t.data[i][0]
21 | )
22 | continue
23 | if "LOCAL" in t.data[i][0]:
24 | c.print_result(
25 | t.target, t.data[i][1][:-5] + "********", t.data[i][0]
26 | )
27 | continue
28 | if "HIBP" in t.data[i][0]:
29 | c.print_result(t.target, t.data[i][1], t.data[i][0])
30 | if "HUNTER_PUB" in t.data[i][0]:
31 | c.print_result(
32 | t.target, str(t.data[i][1]) + " RELATED EMAILS", "HUNTER_PUB"
33 | )
34 | if "HUNTER_RELATED" in t.data[i][0]:
35 | c.print_result(t.target, t.data[i][1], "HUNTER_RELATED")
36 | if "EMAILREP" in t.data[i][0]:
37 | c.print_result(
38 | t.target, str(t.data[i][1]), t.data[i][0]
39 | )
40 | if "SNUS" in t.data[i][0]:
41 | c.print_result(t.target, t.data[i][1], t.data[i][0])
42 | if "LOCAL" in t.data[i][0]:
43 | c.print_result(t.target, t.data[i][1], t.data[i][0])
44 | if "BC_PASS" in t.data[i][0]:
45 | c.print_result(t.target, t.data[i][1], t.data[i][0])
46 | if "LEAKLOOKUP" in t.data[i][0]:
47 | c.print_result(t.target, t.data[i][1], t.data[i][0])
48 | if "LKLP" in t.data[i][0]:
49 | c.print_result(t.target, t.data[i][1], t.data[i][0])
50 | if "WLI" in t.data[i][0]:
51 | c.print_result(t.target, t.data[i][1], t.data[i][0])
52 | if "SCYLLA" in t.data[i][0]:
53 | c.print_result(t.target, t.data[i][1], t.data[i][0])
54 | if "DHASHD" in t.data[i][0]:
55 | c.print_result(t.target, t.data[i][1], t.data[i][0])
56 | if "INTELX" in t.data[i][0]:
57 | c.print_result(t.target, t.data[i][1], t.data[i][0])
58 | if "BREACHDR" in t.data[i][0]:
59 | c.print_result(t.target, t.data[i][1], t.data[i][0])
60 |
61 |
--------------------------------------------------------------------------------
/h8mail/utils/run.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | # Most imports are after python2/3 check further down
3 | import configparser
4 | import argparse
5 | import os
6 | import re
7 | import time
8 | import sys
9 |
10 | from .breachcompilation import breachcomp_check
11 | from .classes import target
12 | from .colors import colors as c
13 | from .helpers import (
14 | fetch_emails,
15 | find_files,
16 | get_config_from_file,
17 | get_emails_from_file,
18 | print_banner,
19 | save_results_csv,
20 | check_latest_version,
21 | check_scylla_online,
22 | )
23 | from .print_json import save_results_json
24 | from .localsearch import local_search, local_search_single, local_to_targets
25 | from .localgzipsearch import local_gzip_search, local_search_single_gzip
26 | from .summary import print_summary
27 | from .chase import chase
28 | from .print_results import print_results
29 | from .gen_config import gen_config_file
30 | from .url import target_urls
31 |
32 |
33 | def target_factory(targets, user_args):
34 | """
35 | Receives list of emails and user args. Fetchs API keys from config file using user_args path and cli keys.
36 | For each target, launch target.methods() associated to found config artifacts.
37 | Handles chase logic with counters from enumerate()
38 | """
39 | # Removing duplicates here to avoid dups from chasing
40 | targets = list(set(targets))
41 |
42 | finished = []
43 | if user_args.config_file is not None or user_args.cli_apikeys is not None:
44 | api_keys = get_config_from_file(user_args)
45 | else:
46 | api_keys = None
47 | init_targets_len = len(targets)
48 |
49 | query = "email"
50 | skip_default_queries = False
51 | if user_args.user_query is not None:
52 | query = user_args.user_query
53 | skip_default_queries = True # custom query skips default query automatically
54 |
55 | scylla_up = False
56 | if user_args.skip_defaults is False:
57 | scylla_up = check_scylla_online()
58 |
59 | for counter, t in enumerate(targets):
60 | c.info_news("Target factory started for {target}".format(target=t))
61 | if user_args.debug:
62 | current_target = target(t, debug=True)
63 | else:
64 | current_target = target(t)
65 | if not skip_default_queries:
66 | if not user_args.skip_defaults:
67 | current_target.get_hunterio_public()
68 | ## emailrep seems to insta-block h8mail user agent without a key
69 | # if api_keys is None or "emailrep" not in api_keys:
70 | # current_target.get_emailrepio()
71 | # elif (
72 | # api_keys is not None and "emailrep" in api_keys and query == "email"
73 | # ):
74 | # current_target.get_emailrepio(api_keys["emailrep"])
75 |
76 | if api_keys is not None:
77 | if (
78 | "breachdirectory_user" in api_keys
79 | and "breachdirectory_pass" in api_keys
80 | ):
81 | current_target.get_breachdirectory(
82 | api_keys["breachdirectory_user"], api_keys["breachdirectory_pass"], query
83 | )
84 | if "hibp" in api_keys and query == "email":
85 | current_target.get_hibp3(api_keys["hibp"])
86 | if "emailrep" in api_keys and query == "email":
87 | current_target.get_emailrepio(api_keys["emailrep"])
88 | if "hunterio" in api_keys and query == "email":
89 | current_target.get_hunterio_private(api_keys["hunterio"])
90 | if "intelx_key" in api_keys:
91 | current_target.get_intelx(api_keys)
92 | if "snusbase_token" in api_keys:
93 | if "snusbase_url" in api_keys:
94 | snusbase_url = api_keys["snusbase_url"]
95 | else:
96 | snusbase_url = "http://api.snusbase.com/v3/search"
97 | current_target.get_snusbase(
98 | snusbase_url, api_keys["snusbase_token"], query
99 | )
100 | if "leak-lookup_priv" in api_keys:
101 | current_target.get_leaklookup_priv(api_keys["leak-lookup_priv"], query)
102 | if "leak-lookup_pub" in api_keys and query == "email":
103 | current_target.get_leaklookup_pub(api_keys["leak-lookup_pub"])
104 | if "weleakinfo_pub" in api_keys and query == "email":
105 | current_target.get_weleakinfo_pub(api_keys["weleakinfo_pub"])
106 | if "weleakinfo_priv" in api_keys:
107 | current_target.get_weleakinfo_priv(api_keys["weleakinfo_priv"], query)
108 | if "dehashed_key" in api_keys:
109 | if "dehashed_email" in api_keys:
110 | current_target.get_dehashed(
111 | api_keys["dehashed_email"], api_keys["dehashed_key"], query
112 | )
113 | else:
114 | c.bad_news("Missing Dehashed email")
115 | if scylla_up:
116 | current_target.get_scylla(query)
117 |
118 | # Chasing
119 | if user_args.chase_limit and counter <= init_targets_len:
120 | user_args_force_email = user_args
121 | user_args_force_email.user_query = "email"
122 | user_args_force_email.chase_limit -= 1
123 | finished_chased = target_factory(
124 | chase(current_target, user_args), user_args_force_email
125 | )
126 | finished.extend((finished_chased))
127 | finished.append(current_target)
128 | return finished
129 |
130 |
131 | def h8mail(user_args):
132 | """
133 | Handles most user arg logic. Creates a list() of targets from user input.
134 | Starts the target object factory loop; starts local searches after factory if in user inputs
135 | Prints results, saves to csv or JSON if in user inputs
136 | """
137 |
138 | if user_args.user_targets and user_args.user_urls:
139 | c.bad_news("Cannot use --url with --target. Use one or the other.")
140 | exit(1)
141 |
142 | if not user_args.user_targets and not user_args.user_urls:
143 | c.bad_news("Missing Target or URL")
144 | exit(1)
145 |
146 | start_time = time.time()
147 |
148 | import warnings
149 |
150 | warnings.filterwarnings("ignore", message="Unverified HTTPS request")
151 |
152 | targets = []
153 | if user_args.user_urls:
154 | targets = target_urls(user_args)
155 | if len(targets) == 0:
156 | c.bad_news("No targets found in URLs. Quitting")
157 | exit(0)
158 |
159 | # If we found emails from URLs, `targets` array already has stuff
160 | if len(targets) != 0:
161 | if user_args.user_targets is None:
162 | user_args.user_targets = []
163 | user_args.user_targets.extend(targets)
164 |
165 | else: # Find targets in user input or file
166 | if user_args.user_targets is not None:
167 | for arg in user_args.user_targets:
168 | user_stdin_target = fetch_emails(arg, user_args)
169 | if os.path.isfile(arg):
170 | c.info_news("Reading from file " + arg)
171 | targets.extend(get_emails_from_file(arg, user_args))
172 | elif user_stdin_target:
173 | targets.extend(user_stdin_target)
174 | else:
175 | c.bad_news("No targets found in user input. Quitting")
176 | exit(0)
177 |
178 | c.info_news("Removing duplicates")
179 | targets = list(set(targets))
180 |
181 | c.good_news("Targets:")
182 | for t in targets:
183 | c.good_news(t)
184 |
185 | # Launch
186 | breached_targets = target_factory(targets, user_args)
187 |
188 | # These are not done inside the factory as the factory iterates over each target individually
189 | # The following functions perform line by line checks of all targets per line
190 |
191 | if user_args.bc_path:
192 | breached_targets = breachcomp_check(breached_targets, user_args.bc_path)
193 |
194 | local_found = None
195 | # Handle cleartext search
196 | if user_args.local_breach_src:
197 | for arg in user_args.local_breach_src:
198 | res = find_files(arg)
199 | if user_args.single_file:
200 | local_found = local_search_single(res, targets)
201 | else:
202 | local_found = local_search(res, targets)
203 | if local_found is not None:
204 | breached_targets = local_to_targets(
205 | breached_targets, local_found, user_args
206 | )
207 | # Handle gzip search
208 | if user_args.local_gzip_src:
209 | for arg in user_args.local_gzip_src:
210 | res = find_files(arg, "gz")
211 | if user_args.single_file:
212 | local_found = local_search_single_gzip(res, targets)
213 | else:
214 | local_found = local_gzip_search(res, targets)
215 | if local_found is not None:
216 | breached_targets = local_to_targets(
217 | breached_targets, local_found, user_args
218 | )
219 |
220 | print_results(breached_targets, user_args.hide)
221 |
222 | print_summary(start_time, breached_targets)
223 | if user_args.output_file:
224 | save_results_csv(user_args.output_file, breached_targets)
225 | if user_args.output_json:
226 | save_results_json(user_args.output_json, breached_targets)
227 |
228 | def parse_args(args):
229 | """
230 | Seperate functions to make it easier to run tests
231 | Pass args as an array
232 | """
233 | parser = argparse.ArgumentParser(
234 | description="Email information and password lookup tool", prog="h8mail"
235 | )
236 |
237 | parser.add_argument(
238 | "-t",
239 | "--targets",
240 | dest="user_targets",
241 | help="Either string inputs or files. Supports email pattern matching from input or file, filepath globing and multiple arguments",
242 | nargs="+",
243 | )
244 | parser.add_argument(
245 | "-u",
246 | "--url",
247 | dest="user_urls",
248 | help="Either string inputs or files. Supports URL pattern matching from input or file, filepath globing and multiple arguments. Parse URLs page for emails. Requires http:// or https:// in URL.",
249 | nargs="+",
250 | )
251 | parser.add_argument(
252 | "-q",
253 | "--custom-query",
254 | dest="user_query",
255 | help='Perform a custom query. Supports username, password, ip, hash, domain. Performs an implicit "loose" search when searching locally',
256 | )
257 | parser.add_argument(
258 | "--loose",
259 | dest="loose",
260 | help="Allow loose search by disabling email pattern recognition. Use spaces as pattern seperators",
261 | action="store_true",
262 | default=False,
263 | )
264 | parser.add_argument(
265 | "-c",
266 | "--config",
267 | dest="config_file",
268 | help="Configuration file for API keys. Accepts keys from Snusbase, WeLeakInfo, Leak-Lookup, HaveIBeenPwned, Emailrep, Dehashed and hunterio",
269 | nargs="+",
270 | )
271 | parser.add_argument(
272 | "-o", "--output", dest="output_file", help="File to write CSV output"
273 | )
274 | parser.add_argument(
275 | "-j", "--json", dest="output_json", help="File to write JSON output"
276 | )
277 | parser.add_argument(
278 | "-bc",
279 | "--breachcomp",
280 | dest="bc_path",
281 | help="Path to the breachcompilation torrent folder. Uses the query.sh script included in the torrent",
282 | )
283 | parser.add_argument(
284 | "-sk",
285 | "--skip-defaults",
286 | dest="skip_defaults",
287 | help="Skips Scylla and HunterIO check. Ideal for local scans",
288 | action="store_true",
289 | default=False,
290 | )
291 | parser.add_argument(
292 | "-k",
293 | "--apikey",
294 | dest="cli_apikeys",
295 | help='Pass config options. Supported format: "K=V,K=V"',
296 | nargs="+",
297 | )
298 | parser.add_argument(
299 | "-lb",
300 | "--local-breach",
301 | dest="local_breach_src",
302 | help="Local cleartext breaches to scan for targets. Uses multiprocesses, one separate process per file, on separate worker pool by arguments. Supports file or folder as input, and filepath globing",
303 | nargs="+",
304 | )
305 | parser.add_argument(
306 | "-gz",
307 | "--gzip",
308 | dest="local_gzip_src",
309 | help="Local tar.gz (gzip) compressed breaches to scans for targets. Uses multiprocesses, one separate process per file. Supports file or folder as input, and filepath globing. Looks for 'gz' in filename",
310 | nargs="+",
311 | )
312 | parser.add_argument(
313 | "-sf",
314 | "--single-file",
315 | dest="single_file",
316 | help="If breach contains big cleartext or tar.gz files, set this flag to view the progress bar. Disables concurrent file searching for stability",
317 | action="store_true",
318 | default=False,
319 | ),
320 | parser.add_argument(
321 | "-ch",
322 | "--chase",
323 | dest="chase_limit",
324 | help="Add related emails from hunter.io to ongoing target list. Define number of emails per target to chase. Requires hunter.io private API key if used without power-chase",
325 | type=int,
326 | nargs="?",
327 | ),
328 | parser.add_argument(
329 | "--power-chase",
330 | dest="power_chase",
331 | help="Add related emails from ALL API services to ongoing target list. Use with --chase",
332 | action="store_true",
333 | default=False,
334 | ),
335 | parser.add_argument(
336 | "--hide",
337 | dest="hide",
338 | help="Only shows the first 4 characters of found passwords to output. Ideal for demonstrations",
339 | action="store_true",
340 | default=False,
341 | ),
342 | parser.add_argument(
343 | "--debug",
344 | dest="debug",
345 | help="Print request debug information",
346 | action="store_true",
347 | default=False,
348 | ),
349 | parser.add_argument(
350 | "--gen-config",
351 | "-g",
352 | dest="gen_config",
353 | help="Generates a configuration file template in the current working directory & exits. Will overwrite existing h8mail_config.ini file",
354 | action="store_true",
355 | default=False,
356 | ),
357 |
358 | return parser.parse_args(args)
359 |
360 |
361 | def main():
362 |
363 | print_banner("warn")
364 | print_banner("version")
365 | print_banner()
366 | check_latest_version()
367 | user_args = parse_args(sys.argv[1:])
368 | if user_args.gen_config:
369 | gen_config_file()
370 | exit(0)
371 | h8mail(user_args)
372 |
--------------------------------------------------------------------------------
/h8mail/utils/summary.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import time
4 | from .colors import colors as c
5 |
6 |
7 | def print_summary(start_time, breached_targets):
8 | """
9 | Prints a padded table where each line is a target, and associated value is simplified to breached/not breached.
10 | If breached, shows len(t.data). Shown elements and total may differ as some elements of t.data are not outputted to stdout.
11 | """
12 | print("{:_^90}".format(""))
13 | print(
14 | "\n\n\n{:^32}".format(""),
15 | c.bold,
16 | c.underline,
17 | "Session Recap:",
18 | c.reset,
19 | "\n\n",
20 | )
21 | print("{:^40} | ".format("Target"), "{:^40}".format("Status"), c.reset)
22 | print("{:_^90}\n".format(""))
23 | for t in breached_targets:
24 | if t.pwned != 0:
25 | print(
26 | f"{t.target:^40} | ",
27 | c.fg.green,
28 | "{:^40}".format("Breach Found (" + str(t.pwned) + " elements)"),
29 | c.reset,
30 | )
31 | else:
32 | print(
33 | f"{t.target:^40} | ",
34 | c.fg.lightgrey,
35 | "{:^40}".format("Not Compromised"),
36 | c.reset,
37 | )
38 | print("{:_^90}\n".format(""))
39 | total_time = time.time() - start_time
40 | print("Execution time (seconds): ", c.fg.lightcyan, f"{total_time}", c.reset, "\n")
41 |
--------------------------------------------------------------------------------
/h8mail/utils/url.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import os
5 | import re
6 | import requests
7 |
8 | from .colors import colors as c
9 | from .version import __version__
10 | from .helpers import get_emails_from_file
11 | from .helpers import fetch_emails
12 |
13 | def fetch_urls(target):
14 | """
15 | Returns a list of URLs found in 'target'.
16 | """
17 | # https://www.geeksforgeeks.org/python-check-url-string/
18 | e = re.findall(
19 | "http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\), ]|(?:%[0-9a-fA-F][0-9a-fA-F]))+",
20 | target,
21 | )
22 | if e:
23 | # print(", ".join(e), c.reset)
24 | return e
25 | return None
26 |
27 |
28 | def get_urls_from_file(targets_file):
29 | """
30 | For each line in file, check for URLs using fetch_urls().
31 | Returns list of URLs.
32 | """
33 | email_obj_list = []
34 | c.info_news("Parsing urls from \t" + targets_file)
35 | try:
36 | target_fd = open(targets_file).readlines()
37 | for line in target_fd:
38 | e = fetch_urls(line)
39 | if e is None:
40 | continue
41 | else:
42 | email_obj_list.extend(e)
43 | return email_obj_list
44 | except Exception as ex:
45 | c.bad_news("Problems occurred while trying to get URLs from file")
46 | print(ex)
47 |
48 |
49 |
50 | def worker_url(url):
51 | """
52 | Fetches the URL without the h8mail UA
53 | """
54 | paramsUA = {"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36"}
55 | try:
56 | c.info_news("Worker fetching " + url)
57 | r = requests.get(url, params = paramsUA, allow_redirects=False)
58 | c.info_news("Worker done fetch url")
59 | print(f"Status code: {r.status_code}")
60 |
61 | e = re.findall(r"[\w\.-]+@[\w\.-]+", r.text)
62 | print(e)
63 | if e:
64 | print(", ".join(e), c.reset)
65 | return e
66 | return None
67 | except Exception as ex:
68 | c.bad_news("URL fetch worker error:")
69 | print(ex)
70 |
71 |
72 | def target_urls(user_args):
73 | """
74 | For each user input with --url, check if its a file.
75 | If yes open and parse each line with regexp, else parse the input with regexp directly.
76 | Parse html pages from URLs for email patterns.
77 | Returns list of email targets
78 | """
79 | try:
80 | c.info_news("Starting URL fetch")
81 | urls = []
82 | emails = []
83 | for arg in user_args.user_urls:
84 | if os.path.isfile(arg):
85 | e = get_urls_from_file(arg)
86 | else:
87 | e = fetch_urls(arg)
88 | if e is None:
89 | continue
90 | else:
91 | urls.extend(e)
92 |
93 | for url in urls:
94 | e = worker_url(url)
95 | # e = get_emails_from_file(tmpfile, user_args)
96 | if e is None:
97 | continue
98 | else:
99 | emails.extend(e)
100 |
101 | return emails
102 | except Exception as ex:
103 | c.bad_news("URL fetch error:")
104 | print(ex)
105 |
106 |
--------------------------------------------------------------------------------
/h8mail/utils/version.py:
--------------------------------------------------------------------------------
1 | __version__ = "2.5.6"
2 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """The setup script."""
5 | from setuptools import setup, find_packages, Extension
6 | from distutils.util import convert_path
7 | import os
8 |
9 | here = os.path.abspath(os.path.dirname(__file__))
10 | exec(open(os.path.join(here, "h8mail/utils/version.py")).read())
11 |
12 | with open("PyPi.rst") as readme_file:
13 | readme = readme_file.read()
14 |
15 | # with open("HISTORY.rst") as history_file:
16 | # history = history_file.read()
17 |
18 | requirements = ["requests"]
19 |
20 | setup_requirements = ["requests"]
21 |
22 | test_requirements = ["requests"]
23 |
24 | setup(
25 | author="khast3x",
26 | author_email="k@khast3x.club",
27 | classifiers=[
28 | "Development Status :: 5 - Production/Stable",
29 | "Intended Audience :: Developers",
30 | "License :: OSI Approved :: BSD License",
31 | "Natural Language :: English",
32 | "Programming Language :: Python :: 3.6",
33 | "Programming Language :: Python :: 3.7",
34 | "Environment :: Console",
35 | ],
36 | description="Email OSINT and password breach hunting. Use h8mail to find passwords through different breach and reconnaissance services, or using your local data",
37 | install_requires=requirements,
38 | license="BSD license",
39 | # long_description_content_type="text/markdown",
40 | long_description=readme + "\n\n",
41 | # long_description=readme + "\n\n" + history,
42 | include_package_data=True,
43 | keywords="h8mail",
44 | name="h8mail",
45 | packages=find_packages(),
46 | entry_points={"console_scripts": ["h8mail = h8mail.__main__:main"]},
47 | setup_requires=setup_requirements,
48 | url="https://github.com/khast3x/h8mail",
49 | version=__version__,
50 | zip_safe=False,
51 | )
52 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """Unit test package for h8mail."""
4 |
--------------------------------------------------------------------------------
/tests/test_email.txt:
--------------------------------------------------------------------------------
1 | john.smith@gmail.com
2 | test@example.com
3 | test@gmail.com
4 |
--------------------------------------------------------------------------------
/tests/test_h8mail.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | """Tests for `h8mail` package."""
5 |
6 |
7 | import unittest
8 | import sys
9 | import time
10 | import tempfile
11 | import shutil
12 | import contextlib
13 | import os
14 | import tarfile
15 | import gzip
16 | import argparse
17 | from h8mail.utils import run
18 | from h8mail.utils import classes
19 | from h8mail.utils import helpers
20 | from h8mail.utils import localsearch
21 | from h8mail.utils import localgzipsearch
22 |
23 | def print_test_banner(testname):
24 | print("========================")
25 | print("========================")
26 | print("\tTESTING: "+testname)
27 | print("========================")
28 | print("========================")
29 |
30 |
31 | def make_temp_directory():
32 | emails = """
33 | john.smith@gmail.com
34 | john.smith@gmail.com
35 | fijsdhkfnhqsdkf
36 | fdqfqsdff
37 | test@evilcorp.com
38 | notfound@email.com
39 | """
40 | creds = """
41 | john.smith@gmail.com:SecretPASS
42 | bloblfd
43 | fjsdkf,ds
44 | test@evilcorp.com:An0therSECRETpassw0rd
45 | ddqsdqs
46 | """
47 | temp_dir = tempfile.mkdtemp()
48 | try:
49 | fd_emails = open(os.path.join(temp_dir, "test-emails.txt"), "w")
50 | fd_emails.writelines(emails)
51 | fd_emails.close()
52 | fd_creds = open(os.path.join(temp_dir, "test-creds.txt"), "w")
53 | fd_creds.writelines(creds)
54 | fd_creds.close()
55 | tar = tarfile.open(os.path.join(temp_dir, "test-creds.tar.gz"), "w:gz")
56 | tar.add(os.path.join(temp_dir, "test-creds.txt"))
57 | tar.close()
58 |
59 | return temp_dir
60 | except Exception as e:
61 | print(e)
62 |
63 |
64 | class TestH8mail(unittest.TestCase):
65 | """Tests for `h8mail` package."""
66 |
67 | def setUp(self):
68 | """Generating local files"""
69 | self.temp_dir = make_temp_directory()
70 | print("Created Temp Dir: " + self.temp_dir)
71 | print(os.listdir(self.temp_dir))
72 | self.filetargets = os.path.join(self.temp_dir, "test-emails.txt")
73 | self.filetxt = os.path.join(self.temp_dir, "test-creds.txt")
74 | self.filegz = os.path.join(self.temp_dir, "test-creds.tar.gz")
75 | print("Test files generated in : " + self.temp_dir)
76 |
77 | # a = open(self.filetxt, "r")
78 | # print(a.readlines())
79 |
80 | def tearDown(self):
81 | """Cleaning temp files"""
82 | print("Removing dir + content: " + self.temp_dir)
83 | shutil.rmtree(self.temp_dir)
84 |
85 | def test_000_simple(self):
86 | """Simple test"""
87 | run.print_banner()
88 | print_test_banner("VANILLA")
89 |
90 | user_args = run.parse_args(["-t", "test@example.com"])
91 | run.h8mail(user_args)
92 |
93 | def test_002_local_files_txt_gz(self):
94 | """Local file search Test"""
95 | run.print_banner()
96 | print_test_banner("TXT LOCAL")
97 | user_args_lb = run.parse_args(["-t", self.filetargets, "-lb", self.filetxt, "-sk"])
98 | run.h8mail(user_args_lb)
99 | print_test_banner("TXT LOCAL-SINGLFILE")
100 | user_args_lb = run.parse_args(["-t", self.filetargets, "-lb", self.filetxt, "-sk", "-sf"])
101 | run.h8mail(user_args_lb)
102 |
103 | run.print_banner()
104 | print_test_banner("GZ LOCAL")
105 | user_args_gz = run.parse_args(["-t", self.filetargets, "-gz", self.filegz, "-sk"])
106 | run.h8mail(user_args_gz)
107 | print_test_banner("GZ LOCAL-SINGLEFILE")
108 | user_args_gz = run.parse_args(["-t", self.filetargets, "-gz", self.filegz, "-sk", "-sf"])
109 | run.h8mail(user_args_gz)
110 |
111 | def test_003_url(self):
112 | run.print_banner()
113 | print_test_banner("URL-RAW")
114 | user_args_lb = run.parse_args(["-u", "https://raw.githubusercontent.com/khast3x/h8mail/master/tests/test_email.txt"])
115 | run.h8mail(user_args_lb)
116 | run.print_banner()
117 | print_test_banner("URL-MESSY")
118 | user_args_lb = run.parse_args(["-u", "https://raw.githubusercontent.com/khast3x/h8mail/master/tests/test_email.txt"])
119 | run.h8mail(user_args_lb)
120 |
--------------------------------------------------------------------------------