mirror of
https://github.com/aquasecurity/kube-hunter.git
synced 2026-02-14 09:59:55 +00:00
Packaging Kube-Hunter for PyPi (#272)
* Inital Commit Signed-off-by: Vipul Gupta (@vipulgupta2048) <vipulgupta2048@gmail.com> * Suggestions implemented as suggested Signed-off-by: Vipul Gupta (@vipulgupta2048) <vipulgupta2048@gmail.com> * Package with setuptools Use setuptools to package kube-hunter as redistributable file. Once packages, it can be pushed to PyPi. The package version is taken from git tags (using setuptools_scm). Closes #185 * Ignore __main__.py script in code coverage The entrypoint script should not be tested but rather be calling to tested modules. Ideally, __main__ should only make a call to single function from another tested module. * Update requirements files Use install_requires from setup.cfg file as single source of truth for dependencies. Install regular dependencies when installing dev dependencies. * Symlink kube-hunter.py to entry point Support the old way to run kube-hunter via the main script by making a link to the new kube_hunter/__main__.py script. Co-authored-by: Yehuda Chikvashvili <yehudaac1@gmail.com>
This commit is contained in:
17
.gitignore
vendored
17
.gitignore
vendored
@@ -7,6 +7,23 @@ venv/
|
||||
.coverage
|
||||
.idea
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Directory Cache Files
|
||||
.DS_Store
|
||||
thumbs.db
|
||||
|
||||
41
README.md
41
README.md
@@ -55,17 +55,17 @@ By default, kube-hunter will open an interactive session, in which you will be a
|
||||
1. **Remote scanning**
|
||||
|
||||
To specify remote machines for hunting, select option 1 or use the `--remote` option. Example:
|
||||
`./kube-hunter.py --remote some.node.com`
|
||||
`kube-hunter --remote some.node.com`
|
||||
|
||||
2. **Interface scanning**
|
||||
|
||||
To specify interface scanning, you can use the `--interface` option (this will scan all of the machine's network interfaces). Example:
|
||||
`./kube-hunter.py --interface`
|
||||
`kube-hunter --interface`
|
||||
|
||||
3. **Network scanning**
|
||||
|
||||
To specify a specific CIDR to scan, use the `--cidr` option. Example:
|
||||
`./kube-hunter.py --cidr 192.168.0.0/24`
|
||||
`kube-hunter --cidr 192.168.0.0/24`
|
||||
|
||||
### Active Hunting
|
||||
|
||||
@@ -73,23 +73,23 @@ Active hunting is an option in which kube-hunter will exploit vulnerabilities it
|
||||
The main difference between normal and active hunting is that a normal hunt will never change the state of the cluster, while active hunting can potentially do state-changing operations on the cluster, **which could be harmful**.
|
||||
|
||||
By default, kube-hunter does not do active hunting. To active hunt a cluster, use the `--active` flag. Example:
|
||||
`./kube-hunter.py --remote some.domain.com --active`
|
||||
`kube-hunter --remote some.domain.com --active`
|
||||
|
||||
### List of tests
|
||||
You can see the list of tests with the `--list` option: Example:
|
||||
`./kube-hunter.py --list`
|
||||
`kube-hunter --list`
|
||||
|
||||
To see active hunting tests as well as passive:
|
||||
`./kube-hunter.py --list --active`
|
||||
`kube-hunter --list --active`
|
||||
|
||||
### Nodes Mapping
|
||||
To see only a mapping of your nodes network, run with `--mapping` option. Example:
|
||||
`./kube-hunter.py --cidr 192.168.0.0/24 --mapping`
|
||||
`kube-hunter --cidr 192.168.0.0/24 --mapping`
|
||||
This will output all the Kubernetes nodes kube-hunter has found.
|
||||
|
||||
### Output
|
||||
To control logging, you can specify a log level, using the `--log` option. Example:
|
||||
`./kube-hunter.py --active --log WARNING`
|
||||
`kube-hunter --active --log WARNING`
|
||||
Available log levels are:
|
||||
|
||||
* DEBUG
|
||||
@@ -98,7 +98,7 @@ Available log levels are:
|
||||
|
||||
### Dispatching
|
||||
By default, the report will be dispatched to `stdout`, but you can specify different methods by using the `--dispatch` option. Example:
|
||||
`./kube-hunter.py --report json --dispatch http`
|
||||
`kube-hunter --report json --dispatch http`
|
||||
Available dispatch methods are:
|
||||
|
||||
* stdout (default)
|
||||
@@ -111,13 +111,27 @@ There are three methods for deploying kube-hunter:
|
||||
|
||||
### On Machine
|
||||
|
||||
You can run the kube-hunter python code directly on your machine.
|
||||
You can run kube-hunter directly on your machine.
|
||||
|
||||
#### Prerequisites
|
||||
|
||||
You will need the following installed:
|
||||
* python 3.x
|
||||
* pip
|
||||
|
||||
##### Install with pip
|
||||
|
||||
Install:
|
||||
~~~
|
||||
pip install kube-hunter
|
||||
~~~
|
||||
|
||||
Run:
|
||||
~~~
|
||||
kube-hunter
|
||||
~~~
|
||||
|
||||
##### Run from source
|
||||
Clone the repository:
|
||||
~~~
|
||||
git clone https://github.com/aquasecurity/kube-hunter.git
|
||||
@@ -130,15 +144,18 @@ pip install -r requirements.txt
|
||||
~~~
|
||||
|
||||
Run:
|
||||
`./kube-hunter.py`
|
||||
~~~
|
||||
python3 kube_hunter
|
||||
~~~
|
||||
|
||||
_If you want to use pyinstaller/py2exe you need to first run the install_imports.py script._
|
||||
|
||||
### Container
|
||||
Aqua Security maintains a containerized version of kube-hunter at `aquasec/kube-hunter`. This container includes this source code, plus an additional (closed source) reporting plugin for uploading results into a report that can be viewed at [kube-hunter.aquasec.com](https://kube-hunter.aquasec.com). Please note, that running the `aquasec/kube-hunter` container and uploading reports data are subject to additional [terms and conditions](https://kube-hunter.aquasec.com/eula.html).
|
||||
|
||||
The Dockerfile in this repository allows you to build a containerized version without the reporting plugin.
|
||||
|
||||
If you run the kube-hunter container with the host network, it will be able to probe all the interfaces on the host:
|
||||
If you run kube-hunter container with the host network, it will be able to probe all the interfaces on the host:
|
||||
|
||||
`docker run -it --rm --network host aquasec/kube-hunter`
|
||||
|
||||
|
||||
130
kube-hunter.py
130
kube-hunter.py
@@ -1,130 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from kube_hunter.conf import config
|
||||
from kube_hunter.modules.report.plain import PlainReporter
|
||||
from kube_hunter.modules.report.yaml import YAMLReporter
|
||||
from kube_hunter.modules.report.json import JSONReporter
|
||||
from kube_hunter.modules.report.dispatchers import STDOUTDispatcher, HTTPDispatcher
|
||||
from kube_hunter.core.events import handler
|
||||
from kube_hunter.core.events.types import HuntFinished, HuntStarted
|
||||
from kube_hunter.modules.discovery.hosts import RunningAsPodEvent, HostScanEvent
|
||||
|
||||
|
||||
loglevel = getattr(logging, config.log.upper(), logging.INFO)
|
||||
|
||||
if config.log.lower() != "none":
|
||||
logging.basicConfig(level=loglevel, format='%(message)s', datefmt='%H:%M:%S')
|
||||
|
||||
reporters = {
|
||||
'yaml': YAMLReporter,
|
||||
'json': JSONReporter,
|
||||
'plain': PlainReporter
|
||||
}
|
||||
|
||||
if config.report.lower() in reporters.keys():
|
||||
config.reporter = reporters[config.report.lower()]()
|
||||
else:
|
||||
logging.warning('Unknown reporter selected, using plain')
|
||||
config.reporter = reporters['plain']()
|
||||
|
||||
dispatchers = {
|
||||
'stdout': STDOUTDispatcher,
|
||||
'http': HTTPDispatcher
|
||||
}
|
||||
|
||||
if config.dispatch.lower() in dispatchers.keys():
|
||||
config.dispatcher = dispatchers[config.dispatch.lower()]()
|
||||
else:
|
||||
logging.warning('Unknown dispatcher selected, using stdout')
|
||||
config.dispatcher = dispatchers['stdout']()
|
||||
|
||||
import kube_hunter
|
||||
|
||||
|
||||
def interactive_set_config():
|
||||
"""Sets config manually, returns True for success"""
|
||||
options = [("Remote scanning", "scans one or more specific IPs or DNS names"),
|
||||
("Interface scanning","scans subnets on all local network interfaces"),
|
||||
("IP range scanning","scans a given IP range")]
|
||||
|
||||
print("Choose one of the options below:")
|
||||
for i, (option, explanation) in enumerate(options):
|
||||
print("{}. {} ({})".format(i+1, option.ljust(20), explanation))
|
||||
choice = input("Your choice: ")
|
||||
if choice == '1':
|
||||
config.remote = input("Remotes (separated by a ','): ").replace(' ', '').split(',')
|
||||
elif choice == '2':
|
||||
config.interface = True
|
||||
elif choice == '3':
|
||||
config.cidr = input("CIDR (example - 192.168.1.0/24): ").replace(' ', '')
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def list_hunters():
|
||||
print("\nPassive Hunters:\n----------------")
|
||||
for hunter, docs in handler.passive_hunters.items():
|
||||
name, doc = hunter.parse_docs(docs)
|
||||
print("* {}\n {}\n".format(name, doc))
|
||||
|
||||
if config.active:
|
||||
print("\n\nActive Hunters:\n---------------")
|
||||
for hunter, docs in handler.active_hunters.items():
|
||||
name, doc = hunter.parse_docs(docs)
|
||||
print("* {}\n {}\n".format( name, doc))
|
||||
|
||||
|
||||
global hunt_started_lock
|
||||
hunt_started_lock = threading.Lock()
|
||||
hunt_started = False
|
||||
|
||||
def main():
|
||||
global hunt_started
|
||||
scan_options = [
|
||||
config.pod,
|
||||
config.cidr,
|
||||
config.remote,
|
||||
config.interface
|
||||
]
|
||||
try:
|
||||
if config.list:
|
||||
list_hunters()
|
||||
return
|
||||
|
||||
if not any(scan_options):
|
||||
if not interactive_set_config(): return
|
||||
|
||||
with hunt_started_lock:
|
||||
hunt_started = True
|
||||
handler.publish_event(HuntStarted())
|
||||
if config.pod:
|
||||
handler.publish_event(RunningAsPodEvent())
|
||||
else:
|
||||
handler.publish_event(HostScanEvent())
|
||||
|
||||
# Blocking to see discovery output
|
||||
handler.join()
|
||||
except KeyboardInterrupt:
|
||||
logging.debug("Kube-Hunter stopped by user")
|
||||
# happens when running a container without interactive option
|
||||
except EOFError:
|
||||
logging.error("\033[0;31mPlease run again with -it\033[0m")
|
||||
finally:
|
||||
hunt_started_lock.acquire()
|
||||
if hunt_started:
|
||||
hunt_started_lock.release()
|
||||
handler.publish_event(HuntFinished())
|
||||
handler.join()
|
||||
handler.free()
|
||||
logging.debug("Cleaned Queue")
|
||||
else:
|
||||
hunt_started_lock.release()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
1
kube-hunter.py
Symbolic link
1
kube-hunter.py
Symbolic link
@@ -0,0 +1 @@
|
||||
kube_hunter/__main__.py
|
||||
@@ -16,7 +16,7 @@ kube-hunter/
|
||||
# your module
|
||||
report/
|
||||
# your module
|
||||
kube-hunter.py
|
||||
__main__.py
|
||||
~~~
|
||||
### Design Pattern
|
||||
Kube-hunter is built with the [Observer Pattern](https://en.wikipedia.org/wiki/Observer_pattern).
|
||||
@@ -247,5 +247,5 @@ __Note: In filters, you should not change attributes in the event.previous. This
|
||||
Although we haven't been rigorous about this in the past, please add tests to support your code changes. Tests are executed like this:
|
||||
|
||||
```bash
|
||||
python runtest.py
|
||||
pytest
|
||||
```
|
||||
|
||||
130
kube_hunter/__main__.py
Executable file
130
kube_hunter/__main__.py
Executable file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import threading
|
||||
|
||||
from kube_hunter.conf import config
|
||||
from kube_hunter.modules.report.plain import PlainReporter
|
||||
from kube_hunter.modules.report.yaml import YAMLReporter
|
||||
from kube_hunter.modules.report.json import JSONReporter
|
||||
from kube_hunter.modules.report.dispatchers import STDOUTDispatcher, HTTPDispatcher
|
||||
from kube_hunter.core.events import handler
|
||||
from kube_hunter.core.events.types import HuntFinished, HuntStarted
|
||||
from kube_hunter.modules.discovery.hosts import RunningAsPodEvent, HostScanEvent
|
||||
|
||||
|
||||
loglevel = getattr(logging, config.log.upper(), logging.INFO)
|
||||
|
||||
if config.log.lower() != "none":
|
||||
logging.basicConfig(level=loglevel, format='%(message)s', datefmt='%H:%M:%S')
|
||||
|
||||
reporters = {
|
||||
'yaml': YAMLReporter,
|
||||
'json': JSONReporter,
|
||||
'plain': PlainReporter
|
||||
}
|
||||
|
||||
if config.report.lower() in reporters.keys():
|
||||
config.reporter = reporters[config.report.lower()]()
|
||||
else:
|
||||
logging.warning('Unknown reporter selected, using plain')
|
||||
config.reporter = reporters['plain']()
|
||||
|
||||
dispatchers = {
|
||||
'stdout': STDOUTDispatcher,
|
||||
'http': HTTPDispatcher
|
||||
}
|
||||
|
||||
if config.dispatch.lower() in dispatchers.keys():
|
||||
config.dispatcher = dispatchers[config.dispatch.lower()]()
|
||||
else:
|
||||
logging.warning('Unknown dispatcher selected, using stdout')
|
||||
config.dispatcher = dispatchers['stdout']()
|
||||
|
||||
import kube_hunter
|
||||
|
||||
|
||||
def interactive_set_config():
|
||||
"""Sets config manually, returns True for success"""
|
||||
options = [("Remote scanning", "scans one or more specific IPs or DNS names"),
|
||||
("Interface scanning","scans subnets on all local network interfaces"),
|
||||
("IP range scanning","scans a given IP range")]
|
||||
|
||||
print("Choose one of the options below:")
|
||||
for i, (option, explanation) in enumerate(options):
|
||||
print("{}. {} ({})".format(i+1, option.ljust(20), explanation))
|
||||
choice = input("Your choice: ")
|
||||
if choice == '1':
|
||||
config.remote = input("Remotes (separated by a ','): ").replace(' ', '').split(',')
|
||||
elif choice == '2':
|
||||
config.interface = True
|
||||
elif choice == '3':
|
||||
config.cidr = input("CIDR (example - 192.168.1.0/24): ").replace(' ', '')
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def list_hunters():
|
||||
print("\nPassive Hunters:\n----------------")
|
||||
for hunter, docs in handler.passive_hunters.items():
|
||||
name, doc = hunter.parse_docs(docs)
|
||||
print("* {}\n {}\n".format(name, doc))
|
||||
|
||||
if config.active:
|
||||
print("\n\nActive Hunters:\n---------------")
|
||||
for hunter, docs in handler.active_hunters.items():
|
||||
name, doc = hunter.parse_docs(docs)
|
||||
print("* {}\n {}\n".format( name, doc))
|
||||
|
||||
|
||||
global hunt_started_lock
|
||||
hunt_started_lock = threading.Lock()
|
||||
hunt_started = False
|
||||
|
||||
def main():
|
||||
global hunt_started
|
||||
scan_options = [
|
||||
config.pod,
|
||||
config.cidr,
|
||||
config.remote,
|
||||
config.interface
|
||||
]
|
||||
try:
|
||||
if config.list:
|
||||
list_hunters()
|
||||
return
|
||||
|
||||
if not any(scan_options):
|
||||
if not interactive_set_config(): return
|
||||
|
||||
with hunt_started_lock:
|
||||
hunt_started = True
|
||||
handler.publish_event(HuntStarted())
|
||||
if config.pod:
|
||||
handler.publish_event(RunningAsPodEvent())
|
||||
else:
|
||||
handler.publish_event(HostScanEvent())
|
||||
|
||||
# Blocking to see discovery output
|
||||
handler.join()
|
||||
except KeyboardInterrupt:
|
||||
logging.debug("Kube-Hunter stopped by user")
|
||||
# happens when running a container without interactive option
|
||||
except EOFError:
|
||||
logging.error("\033[0;31mPlease run again with -it\033[0m")
|
||||
finally:
|
||||
hunt_started_lock.acquire()
|
||||
if hunt_started:
|
||||
hunt_started_lock.release()
|
||||
handler.publish_event(HuntFinished())
|
||||
handler.join()
|
||||
handler.free()
|
||||
logging.debug("Cleaned Queue")
|
||||
else:
|
||||
hunt_started_lock.release()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,4 +1,9 @@
|
||||
-r requirements.txt
|
||||
|
||||
flake8
|
||||
pytest
|
||||
coverage
|
||||
pytest >= 2.9.1
|
||||
coverage < 5.0
|
||||
pytest-cov
|
||||
setuptools >= 30.3.0
|
||||
setuptools_scm
|
||||
twine
|
||||
|
||||
@@ -1,11 +1 @@
|
||||
enum34 ; python_version<='3.4'
|
||||
netaddr
|
||||
netifaces
|
||||
scapy>=2.4.3
|
||||
requests
|
||||
PrettyTable
|
||||
urllib3>=1.24.2,<1.25
|
||||
ruamel.yaml
|
||||
requests_mock
|
||||
future
|
||||
packaging
|
||||
-e .
|
||||
|
||||
57
setup.cfg
57
setup.cfg
@@ -1,3 +1,56 @@
|
||||
[metadata]
|
||||
name = kube-hunter
|
||||
description = Kubernetes security weaknesses hunter for humans
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
author = Aqua Security
|
||||
author_email = support@aquasec.com
|
||||
url = https://github.com/aquasecurity/kube-hunter
|
||||
keywords =
|
||||
aquasec
|
||||
hunter
|
||||
kubernetes
|
||||
k8s
|
||||
security
|
||||
license_file = LICENSE
|
||||
classifiers =
|
||||
Development Status :: 4 - Beta
|
||||
Environment :: Console
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Natural Language :: English
|
||||
Operating System :: OS Independent
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
Topic :: Security
|
||||
|
||||
[options]
|
||||
zip_safe = False
|
||||
packages = find:
|
||||
install_requires =
|
||||
netaddr
|
||||
netifaces
|
||||
scapy>=2.4.3
|
||||
requests
|
||||
PrettyTable
|
||||
urllib3>=1.24.2,<1.25
|
||||
ruamel.yaml
|
||||
requests_mock
|
||||
future
|
||||
packaging
|
||||
setup_requires =
|
||||
setuptools>=30.3.0
|
||||
setuptools_scm
|
||||
test_requires =
|
||||
pytest>=2.9.1
|
||||
coverage<5.0
|
||||
pytest-cov
|
||||
python_requires = >=3.6
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
kube-hunter = kube_hunter.__main__:main
|
||||
|
||||
[aliases]
|
||||
test=pytest
|
||||
|
||||
@@ -33,3 +86,7 @@ exclude_lines =
|
||||
if __name__ == .__main__.:
|
||||
# Don't complain about log messages not being tested
|
||||
logging\.
|
||||
|
||||
# Files to exclude from consideration
|
||||
omit =
|
||||
kube_hunter/__main__.py
|
||||
|
||||
Reference in New Issue
Block a user