mirror of
https://github.com/aquasecurity/kube-hunter.git
synced 2026-08-23 06:06:18 +00:00
* Make config initialized explicitly * Add mypy linting * Make tests run individually Resolve #341
127 lines
4.8 KiB
Python
127 lines
4.8 KiB
Python
import logging
|
|
import requests
|
|
|
|
from kube_hunter.core.types import Discovery
|
|
from kube_hunter.core.events import handler
|
|
from kube_hunter.core.events.types import OpenPortEvent, Service, Event, EventFilterBase
|
|
|
|
from kube_hunter.conf import get_config
|
|
|
|
KNOWN_API_PORTS = [443, 6443, 8080]
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class K8sApiService(Service, Event):
|
|
"""A Kubernetes API service"""
|
|
|
|
def __init__(self, protocol="https"):
|
|
Service.__init__(self, name="Unrecognized K8s API")
|
|
self.protocol = protocol
|
|
|
|
|
|
class ApiServer(Service, Event):
|
|
"""The API server is in charge of all operations on the cluster."""
|
|
|
|
def __init__(self):
|
|
Service.__init__(self, name="API Server")
|
|
self.protocol = "https"
|
|
|
|
|
|
class MetricsServer(Service, Event):
|
|
"""The Metrics server is in charge of providing resource usage metrics for pods and nodes to the API server"""
|
|
|
|
def __init__(self):
|
|
Service.__init__(self, name="Metrics Server")
|
|
self.protocol = "https"
|
|
|
|
|
|
# Other devices could have this port open, but we can check to see if it looks like a Kubernetes api
|
|
# A Kubernetes API service will respond with a JSON message that includes a "code" field for the HTTP status code
|
|
@handler.subscribe(OpenPortEvent, predicate=lambda x: x.port in KNOWN_API_PORTS)
|
|
class ApiServiceDiscovery(Discovery):
|
|
"""API Service Discovery
|
|
Checks for the existence of K8s API Services
|
|
"""
|
|
|
|
def __init__(self, event):
|
|
self.event = event
|
|
self.session = requests.Session()
|
|
self.session.verify = False
|
|
|
|
def execute(self):
|
|
logger.debug(f"Attempting to discover an API service on {self.event.host}:{self.event.port}")
|
|
protocols = ["http", "https"]
|
|
for protocol in protocols:
|
|
if self.has_api_behaviour(protocol):
|
|
self.publish_event(K8sApiService(protocol))
|
|
|
|
def has_api_behaviour(self, protocol):
|
|
config = get_config()
|
|
try:
|
|
r = self.session.get(f"{protocol}://{self.event.host}:{self.event.port}", timeout=config.network_timeout)
|
|
if ("k8s" in r.text) or ('"code"' in r.text and r.status_code != 200):
|
|
return True
|
|
except requests.exceptions.SSLError:
|
|
logger.debug(f"{[protocol]} protocol not accepted on {self.event.host}:{self.event.port}")
|
|
except Exception:
|
|
logger.debug(f"Failed probing {self.event.host}:{self.event.port}", exc_info=True)
|
|
|
|
|
|
# Acts as a Filter for services, In the case that we can classify the API,
|
|
# We swap the filtered event with a new corresponding Service to next be published
|
|
# The classification can be regarding the context of the execution,
|
|
# Currently we classify: Metrics Server and Api Server
|
|
# If running as a pod:
|
|
# We know the Api server IP, so we can classify easily
|
|
# If not:
|
|
# We determine by accessing the /version on the service.
|
|
# Api Server will contain a major version field, while the Metrics will not
|
|
@handler.subscribe(K8sApiService)
|
|
class ApiServiceClassify(EventFilterBase):
|
|
"""API Service Classifier
|
|
Classifies an API service
|
|
"""
|
|
|
|
def __init__(self, event):
|
|
self.event = event
|
|
self.classified = False
|
|
self.session = requests.Session()
|
|
self.session.verify = False
|
|
# Using the auth token if we can, for the case that authentication is needed for our checks
|
|
if self.event.auth_token:
|
|
self.session.headers.update({"Authorization": f"Bearer {self.event.auth_token}"})
|
|
|
|
def classify_using_version_endpoint(self):
|
|
"""Tries to classify by accessing /version. if could not access succeded, returns"""
|
|
config = get_config()
|
|
try:
|
|
endpoint = f"{self.event.protocol}://{self.event.host}:{self.event.port}/version"
|
|
versions = self.session.get(endpoint, timeout=config.network_timeout).json()
|
|
if "major" in versions:
|
|
if versions.get("major") == "":
|
|
self.event = MetricsServer()
|
|
else:
|
|
self.event = ApiServer()
|
|
except Exception:
|
|
logging.warning("Could not access /version on API service", exc_info=True)
|
|
|
|
def execute(self):
|
|
discovered_protocol = self.event.protocol
|
|
# if running as pod
|
|
if self.event.kubeservicehost:
|
|
# if the host is the api server's IP, we know it's the Api Server
|
|
if self.event.kubeservicehost == str(self.event.host):
|
|
self.event = ApiServer()
|
|
else:
|
|
self.event = MetricsServer()
|
|
# if not running as pod.
|
|
else:
|
|
self.classify_using_version_endpoint()
|
|
|
|
# in any case, making sure to link previously discovered protocol
|
|
self.event.protocol = discovered_protocol
|
|
# If some check classified the Service,
|
|
# the event will have been replaced.
|
|
return self.event
|