mirror of
https://github.com/Grasscutters/mitmproxy.git
synced 2024-11-22 15:37:45 +00:00
added add-ons that enhance the performance of web application scanners. (#3961)
* added add-ons that enhance the performance of web application scanners. Co-authored-by: weichweich <14820950+weichweich@users.noreply.github.com>
This commit is contained in:
parent
f4aa3ee11c
commit
7fdcbb09e6
0
examples/complex/webscanner_helper/__init__.py
Normal file
0
examples/complex/webscanner_helper/__init__.py
Normal file
144
examples/complex/webscanner_helper/mapping.py
Normal file
144
examples/complex/webscanner_helper/mapping.py
Normal file
@ -0,0 +1,144 @@
|
||||
import copy
|
||||
import logging
|
||||
import typing
|
||||
from typing import Dict
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from mitmproxy.http import HTTPFlow
|
||||
from examples.complex.webscanner_helper.urldict import URLDict
|
||||
|
||||
NO_CONTENT = object()
|
||||
|
||||
|
||||
class MappingAddonConfig:
|
||||
HTML_PARSER = "html.parser"
|
||||
|
||||
|
||||
class MappingAddon:
|
||||
""" The mapping add-on can be used in combination with web application scanners to reduce their false positives.
|
||||
|
||||
Many web application scanners produce false positives caused by dynamically changing content of web applications
|
||||
such as the current time or current measurements. When testing for injection vulnerabilities, web application
|
||||
scanners are tricked into thinking they changed the content with the injected payload. In realty, the content of
|
||||
the web application changed notwithstanding the scanner's input. When the mapping add-on is used to map the content
|
||||
to a fixed value, these false positives can be avoided.
|
||||
"""
|
||||
|
||||
OPT_MAPPING_FILE = "mapping_file"
|
||||
"""File where urls and css selector to mapped content is stored.
|
||||
|
||||
Elements will be replaced with the content given in this file. If the content is none it will be set to the first
|
||||
seen value.
|
||||
|
||||
Example:
|
||||
|
||||
{
|
||||
"http://10.10.10.10": {
|
||||
"body": "My Text"
|
||||
},
|
||||
"URL": {
|
||||
"css selector": "Replace with this"
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
OPT_MAP_PERSISTENT = "map_persistent"
|
||||
"""Whether to store all new content in the configuration file."""
|
||||
|
||||
def __init__(self, filename: str, persistent: bool = False) -> None:
|
||||
""" Initializes the mapping add-on
|
||||
|
||||
Args:
|
||||
filename: str that provides the name of the file in which the urls and css selectors to mapped content is
|
||||
stored.
|
||||
persistent: bool that indicates whether to store all new content in the configuration file.
|
||||
|
||||
Example:
|
||||
The file in which the mapping config is given should be in the following format:
|
||||
{
|
||||
"http://10.10.10.10": {
|
||||
"body": "My Text"
|
||||
},
|
||||
"<URL>": {
|
||||
"<css selector>": "Replace with this"
|
||||
}
|
||||
}
|
||||
"""
|
||||
self.filename = filename
|
||||
self.persistent = persistent
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
with open(filename, "r") as f:
|
||||
self.mapping_templates = URLDict.load(f)
|
||||
|
||||
def load(self, loader):
|
||||
loader.add_option(
|
||||
self.OPT_MAPPING_FILE, str, "",
|
||||
"File where replacement configuration is stored."
|
||||
)
|
||||
loader.add_option(
|
||||
self.OPT_MAP_PERSISTENT, bool, False,
|
||||
"Whether to store all new content in the configuration file."
|
||||
)
|
||||
|
||||
def configure(self, updated):
|
||||
if self.OPT_MAPPING_FILE in updated:
|
||||
self.filename = updated[self.OPT_MAPPING_FILE]
|
||||
with open(self.filename, "r") as f:
|
||||
self.mapping_templates = URLDict.load(f)
|
||||
|
||||
if self.OPT_MAP_PERSISTENT in updated:
|
||||
self.persistent = updated[self.OPT_MAP_PERSISTENT]
|
||||
|
||||
def replace(self, soup: BeautifulSoup, css_sel: str, replace: BeautifulSoup) -> None:
|
||||
"""Replaces the content of soup that matches the css selector with the given replace content."""
|
||||
for content in soup.select(css_sel):
|
||||
self.logger.debug(f"replace \"{content}\" with \"{replace}\"")
|
||||
content.replace_with(copy.copy(replace))
|
||||
|
||||
def apply_template(self, soup: BeautifulSoup, template: Dict[str, typing.Union[BeautifulSoup]]) -> None:
|
||||
"""Applies the given mapping template to the given soup."""
|
||||
for css_sel, replace in template.items():
|
||||
mapped = soup.select(css_sel)
|
||||
if not mapped:
|
||||
self.logger.warning(f"Could not find \"{css_sel}\", can not freeze anything.")
|
||||
else:
|
||||
self.replace(soup, css_sel, BeautifulSoup(replace, features=MappingAddonConfig.HTML_PARSER))
|
||||
|
||||
def response(self, flow: HTTPFlow) -> None:
|
||||
"""If a response is received, check if we should replace some content. """
|
||||
try:
|
||||
templates = self.mapping_templates[flow]
|
||||
res = flow.response
|
||||
if res is not None:
|
||||
encoding = res.headers.get("content-encoding", "utf-8")
|
||||
content_type = res.headers.get("content-type", "text/html")
|
||||
|
||||
if "text/html" in content_type and encoding == "utf-8":
|
||||
content = BeautifulSoup(res.content, MappingAddonConfig.HTML_PARSER)
|
||||
for template in templates:
|
||||
self.apply_template(content, template)
|
||||
res.content = content.encode(encoding)
|
||||
else:
|
||||
self.logger.warning(f"Unsupported content type '{content_type}' or content encoding '{encoding}'")
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def done(self) -> None:
|
||||
"""Dumps all new content into the configuration file if self.persistent is set."""
|
||||
if self.persistent:
|
||||
|
||||
# make sure that all items are strings and not soups.
|
||||
def value_dumper(value):
|
||||
store = {}
|
||||
if value is None:
|
||||
return "None"
|
||||
try:
|
||||
for css_sel, soup in value.items():
|
||||
store[css_sel] = str(soup)
|
||||
except:
|
||||
raise RuntimeError(value)
|
||||
return store
|
||||
|
||||
with open(self.filename, "w") as f:
|
||||
self.mapping_templates.dump(f, value_dumper)
|
90
examples/complex/webscanner_helper/urldict.py
Normal file
90
examples/complex/webscanner_helper/urldict.py
Normal file
@ -0,0 +1,90 @@
|
||||
import itertools
|
||||
import json
|
||||
import typing
|
||||
from collections.abc import MutableMapping
|
||||
from typing import Any, Dict, Generator, List, TextIO, Callable
|
||||
|
||||
from mitmproxy import flowfilter
|
||||
from mitmproxy.http import HTTPFlow
|
||||
|
||||
|
||||
def f_id(x):
|
||||
return x
|
||||
|
||||
|
||||
class URLDict(MutableMapping):
|
||||
"""Data structure to store information using filters as keys."""
|
||||
def __init__(self):
|
||||
self.store: Dict[flowfilter.TFilter, Any] = {}
|
||||
|
||||
def __getitem__(self, key, *, count=0):
|
||||
if count:
|
||||
ret = itertools.islice(self.get_generator(key), 0, count)
|
||||
else:
|
||||
ret = list(self.get_generator(key))
|
||||
|
||||
if ret:
|
||||
return ret
|
||||
else:
|
||||
raise KeyError
|
||||
|
||||
def __setitem__(self, key: str, value):
|
||||
fltr = flowfilter.parse(key)
|
||||
if fltr:
|
||||
self.store.__setitem__(fltr, value)
|
||||
else:
|
||||
raise ValueError("Not a valid filter")
|
||||
|
||||
def __delitem__(self, key):
|
||||
self.store.__delitem__(key)
|
||||
|
||||
def __iter__(self):
|
||||
return self.store.__iter__()
|
||||
|
||||
def __len__(self):
|
||||
return self.store.__len__()
|
||||
|
||||
def get_generator(self, flow: HTTPFlow) -> Generator[Any, None, None]:
|
||||
|
||||
for fltr, value in self.store.items():
|
||||
if flowfilter.match(fltr, flow):
|
||||
yield value
|
||||
|
||||
def get(self, flow: HTTPFlow, default=None, *, count=0) -> List[Any]:
|
||||
try:
|
||||
return self.__getitem__(flow, count=count)
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
@classmethod
|
||||
def _load(cls, json_obj, value_loader: Callable = f_id):
|
||||
url_dict = cls()
|
||||
for fltr, value in json_obj.items():
|
||||
url_dict[fltr] = value_loader(value)
|
||||
return url_dict
|
||||
|
||||
@classmethod
|
||||
def load(cls, f: TextIO, value_loader: Callable = f_id):
|
||||
json_obj = json.load(f)
|
||||
return cls._load(json_obj, value_loader)
|
||||
|
||||
@classmethod
|
||||
def loads(cls, json_str: str, value_loader: Callable = f_id):
|
||||
json_obj = json.loads(json_str)
|
||||
return cls._load(json_obj, value_loader)
|
||||
|
||||
def _dump(self, value_dumper: Callable = f_id) -> Dict:
|
||||
dumped: Dict[typing.Union[flowfilter.TFilter, str], Any] = {}
|
||||
for fltr, value in self.store.items():
|
||||
if hasattr(fltr, 'pattern'):
|
||||
# cast necessary for mypy
|
||||
dumped[typing.cast(Any, fltr).pattern] = value_dumper(value)
|
||||
else:
|
||||
dumped[str(fltr)] = value_dumper(value)
|
||||
return dumped
|
||||
|
||||
def dump(self, f: TextIO, value_dumper: Callable = f_id):
|
||||
json.dump(self._dump(value_dumper), f)
|
||||
|
||||
def dumps(self, value_dumper: Callable = f_id):
|
||||
return json.dumps(self._dump(value_dumper))
|
168
examples/complex/webscanner_helper/urlindex.py
Normal file
168
examples/complex/webscanner_helper/urlindex.py
Normal file
@ -0,0 +1,168 @@
|
||||
import abc
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Type, Dict, Union, Optional
|
||||
|
||||
from mitmproxy import flowfilter
|
||||
from mitmproxy.http import HTTPFlow
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UrlIndexWriter(abc.ABC):
|
||||
"""Abstract Add-on to write seen URLs.
|
||||
|
||||
For example, these URLs can be injected in a web application to improve the crawling of web application scanners.
|
||||
The injection can be done using the URLInjection Add-on.
|
||||
"""
|
||||
|
||||
def __init__(self, filename: Path):
|
||||
"""Initializes the UrlIndexWriter.
|
||||
|
||||
Args:
|
||||
filename: Path to file to which the URL index will be written.
|
||||
"""
|
||||
self.filepath = filename
|
||||
|
||||
@abc.abstractmethod
|
||||
def load(self):
|
||||
"""Load existing URL index."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def add_url(self, flow: HTTPFlow):
|
||||
"""Add new URL to URL index."""
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def save(self):
|
||||
pass
|
||||
|
||||
|
||||
class SetEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, set):
|
||||
return list(obj)
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
class JSONUrlIndexWriter(UrlIndexWriter):
|
||||
"""Writes seen URLs as JSON."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.host_urls = {}
|
||||
|
||||
def load(self):
|
||||
if self.filepath.exists():
|
||||
with self.filepath.open("r") as f:
|
||||
self.host_urls = json.load(f)
|
||||
for host in self.host_urls.keys():
|
||||
for path, methods in self.host_urls[host].items():
|
||||
for method, codes in methods.items():
|
||||
self.host_urls[host][path] = {method: set(codes)}
|
||||
|
||||
def add_url(self, flow: HTTPFlow):
|
||||
req = flow.request
|
||||
res = flow.response
|
||||
|
||||
if req is not None and res is not None:
|
||||
urls = self.host_urls.setdefault(f"{req.scheme}://{req.host}:{req.port}", dict())
|
||||
methods = urls.setdefault(req.path, {})
|
||||
codes = methods.setdefault(req.method, set())
|
||||
codes.add(res.status_code)
|
||||
|
||||
def save(self):
|
||||
with self.filepath.open("w") as f:
|
||||
json.dump(self.host_urls, f, cls=SetEncoder)
|
||||
|
||||
|
||||
class TextUrlIndexWriter(UrlIndexWriter):
|
||||
"""Writes seen URLs as text."""
|
||||
|
||||
def load(self):
|
||||
pass
|
||||
|
||||
def add_url(self, flow: HTTPFlow):
|
||||
res = flow.response
|
||||
req = flow.request
|
||||
if res is not None and req is not None:
|
||||
with self.filepath.open("a+") as f:
|
||||
f.write(f"{datetime.datetime.utcnow().isoformat()} STATUS: {res.status_code} METHOD: "
|
||||
f"{req.method} URL:{req.url}\n")
|
||||
|
||||
def save(self):
|
||||
pass
|
||||
|
||||
|
||||
WRITER: Dict[str, Type[UrlIndexWriter]] = {
|
||||
"json": JSONUrlIndexWriter,
|
||||
"text": TextUrlIndexWriter,
|
||||
}
|
||||
|
||||
|
||||
def filter_404(flow) -> bool:
|
||||
"""Filters responses with status code 404."""
|
||||
return flow.response.status_code != 404
|
||||
|
||||
|
||||
class UrlIndexAddon:
|
||||
"""Add-on to write seen URLs, either as JSON or as text.
|
||||
|
||||
For example, these URLs can be injected in a web application to improve the crawling of web application scanners.
|
||||
The injection can be done using the URLInjection Add-on.
|
||||
"""
|
||||
|
||||
index_filter: Optional[Union[str, flowfilter.TFilter]]
|
||||
writer: UrlIndexWriter
|
||||
|
||||
OPT_FILEPATH = "URLINDEX_FILEPATH"
|
||||
OPT_APPEND = "URLINDEX_APPEND"
|
||||
OPT_INDEX_FILTER = "URLINDEX_FILTER"
|
||||
|
||||
def __init__(self, file_path: Union[str, Path], append: bool = True,
|
||||
index_filter: Union[str, flowfilter.TFilter] = filter_404, index_format: str = "json"):
|
||||
""" Initializes the urlindex add-on.
|
||||
|
||||
Args:
|
||||
file_path: Path to file to which the URL index will be written. Can either be given as str or Path.
|
||||
append: Bool to decide whether to append new URLs to the given file (as opposed to overwrite the contents
|
||||
of the file)
|
||||
index_filer: A mitmproxy filter with which the seen URLs will be filtered before being written. Can either
|
||||
be given as str or as flowfilter.TFilter
|
||||
index_format: The format of the URL index, can either be "json" or "text".
|
||||
"""
|
||||
|
||||
if isinstance(index_filter, str):
|
||||
self.index_filter = flowfilter.parse(index_filter)
|
||||
if self.index_filter is None:
|
||||
raise ValueError("Invalid filter expression.")
|
||||
else:
|
||||
self.index_filter = index_filter
|
||||
|
||||
file_path = Path(file_path)
|
||||
try:
|
||||
self.writer = WRITER[index_format.lower()](file_path)
|
||||
except KeyError:
|
||||
raise ValueError(f"Format '{index_format}' is not supported.")
|
||||
|
||||
if not append and file_path.exists():
|
||||
file_path.unlink()
|
||||
|
||||
self.writer.load()
|
||||
|
||||
def response(self, flow: HTTPFlow):
|
||||
"""Checks if the response should be included in the URL based on the index_filter and adds it to the URL index
|
||||
if appropriate.
|
||||
"""
|
||||
if isinstance(self.index_filter, str) or self.index_filter is None:
|
||||
raise ValueError("Invalid filter expression.")
|
||||
else:
|
||||
if self.index_filter(flow):
|
||||
self.writer.add_url(flow)
|
||||
|
||||
def done(self):
|
||||
"""Writes the URL index."""
|
||||
self.writer.save()
|
173
examples/complex/webscanner_helper/urlinjection.py
Normal file
173
examples/complex/webscanner_helper/urlinjection.py
Normal file
@ -0,0 +1,173 @@
|
||||
import abc
|
||||
import html
|
||||
import json
|
||||
import logging
|
||||
|
||||
from mitmproxy import flowfilter
|
||||
from mitmproxy.http import HTTPFlow
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class InjectionGenerator:
|
||||
"""Abstract class for an generator of the injection content in order to inject the URL index."""
|
||||
ENCODING = "UTF8"
|
||||
|
||||
@abc.abstractmethod
|
||||
def inject(self, index, flow: HTTPFlow):
|
||||
"""Injects the given URL index into the given flow."""
|
||||
pass
|
||||
|
||||
|
||||
class HTMLInjection(InjectionGenerator):
|
||||
"""Injects the URL index either by creating a new HTML page or by appending is to an existing page."""
|
||||
|
||||
def __init__(self, insert: bool = False):
|
||||
"""Initializes the HTMLInjection.
|
||||
|
||||
Args:
|
||||
insert: boolean to decide whether to insert the URL index to an existing page (True) or to create a new
|
||||
page containing the URL index.
|
||||
"""
|
||||
self.insert = insert
|
||||
|
||||
@classmethod
|
||||
def _form_html(cls, url):
|
||||
return f"<form action=\"{url}\" method=\"POST\"></form>"
|
||||
|
||||
@classmethod
|
||||
def _link_html(cls, url):
|
||||
return f"<a href=\"{url}\">link to {url}</a>"
|
||||
|
||||
@classmethod
|
||||
def index_html(cls, index):
|
||||
link_htmls = []
|
||||
for scheme_netloc, paths in index.items():
|
||||
for path, methods in paths.items():
|
||||
url = scheme_netloc + path
|
||||
if "POST" in methods:
|
||||
link_htmls.append(cls._form_html(url))
|
||||
|
||||
if "GET" in methods:
|
||||
link_htmls.append(cls._link_html(url))
|
||||
return "</ br>".join(link_htmls)
|
||||
|
||||
@classmethod
|
||||
def landing_page(cls, index):
|
||||
return (
|
||||
"<head><meta charset=\"UTF-8\"></head><body>"
|
||||
+ cls.index_html(index)
|
||||
+ "</body>"
|
||||
)
|
||||
|
||||
def inject(self, index, flow: HTTPFlow):
|
||||
if flow.response is not None:
|
||||
if flow.response.status_code != 404 and not self.insert:
|
||||
logger.warning(
|
||||
f"URL '{flow.request.url}' didn't return 404 status, "
|
||||
f"index page would overwrite valid page.")
|
||||
elif self.insert:
|
||||
content = (flow.response
|
||||
.content
|
||||
.decode(self.ENCODING, "backslashreplace"))
|
||||
if "</body>" in content:
|
||||
content = content.replace("</body>", self.index_html(index) + "</body>")
|
||||
else:
|
||||
content += self.index_html(index)
|
||||
flow.response.content = content.encode(self.ENCODING)
|
||||
else:
|
||||
flow.response.content = (self.landing_page(index)
|
||||
.encode(self.ENCODING))
|
||||
|
||||
|
||||
class RobotsInjection(InjectionGenerator):
|
||||
"""Injects the URL index by creating a new robots.txt including the URLs."""
|
||||
|
||||
def __init__(self, directive="Allow"):
|
||||
self.directive = directive
|
||||
|
||||
@classmethod
|
||||
def robots_txt(cls, index, directive="Allow"):
|
||||
lines = ["User-agent: *"]
|
||||
for scheme_netloc, paths in index.items():
|
||||
for path, methods in paths.items():
|
||||
lines.append(directive + ": " + path)
|
||||
return "\n".join(lines)
|
||||
|
||||
def inject(self, index, flow: HTTPFlow):
|
||||
if flow.response is not None:
|
||||
if flow.response.status_code != 404:
|
||||
logger.warning(
|
||||
f"URL '{flow.request.url}' didn't return 404 status, "
|
||||
f"index page would overwrite valid page.")
|
||||
else:
|
||||
flow.response.content = self.robots_txt(index,
|
||||
self.directive).encode(
|
||||
self.ENCODING)
|
||||
|
||||
|
||||
class SitemapInjection(InjectionGenerator):
|
||||
"""Injects the URL index by creating a new sitemap including the URLs."""
|
||||
|
||||
@classmethod
|
||||
def sitemap(cls, index):
|
||||
lines = [
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">"]
|
||||
for scheme_netloc, paths in index.items():
|
||||
for path, methods in paths.items():
|
||||
url = scheme_netloc + path
|
||||
lines.append(f"<url><loc>{html.escape(url)}</loc></url>")
|
||||
lines.append("</urlset>")
|
||||
return "\n".join(lines)
|
||||
|
||||
def inject(self, index, flow: HTTPFlow):
|
||||
if flow.response is not None:
|
||||
if flow.response.status_code != 404:
|
||||
logger.warning(
|
||||
f"URL '{flow.request.url}' didn't return 404 status, "
|
||||
f"index page would overwrite valid page.")
|
||||
else:
|
||||
flow.response.content = self.sitemap(index).encode(self.ENCODING)
|
||||
|
||||
|
||||
class UrlInjectionAddon:
|
||||
""" The UrlInjection add-on can be used in combination with web application scanners to improve their crawling
|
||||
performance.
|
||||
|
||||
The given URls will be injected into the web application. With this, web application scanners can find pages to
|
||||
crawl much easier. Depending on the Injection generator, the URLs will be injected at different places of the
|
||||
web application. It is possible to create a landing page which includes the URL (HTMLInjection()), to inject the
|
||||
URLs to an existing page (HTMLInjection(insert=True)), to create a robots.txt containing the URLs
|
||||
(RobotsInjection()) or to create a sitemap.xml which includes the URLS (SitemapInjection()).
|
||||
It is necessary that the web application scanner can find the newly created page containing the URL index. For
|
||||
example, the newly created page can be set as starting point for the web application scanner.
|
||||
The URL index needed for the injection can be generated by the UrlIndex Add-on.
|
||||
"""
|
||||
|
||||
def __init__(self, flt: str, url_index_file: str,
|
||||
injection_gen: InjectionGenerator):
|
||||
"""Initializes the UrlIndex add-on.
|
||||
|
||||
Args:
|
||||
flt: mitmproxy filter to decide on which pages the URLs will be injected (str).
|
||||
url_index_file: Path to the file which includes the URL index in JSON format (e.g. generated by the UrlIndexAddon), given
|
||||
as str.
|
||||
injection_gen: InjectionGenerator that should be used to inject the URLs into the web application.
|
||||
"""
|
||||
self.name = f"{self.__class__.__name__}-{injection_gen.__class__.__name__}-{self.__hash__()}"
|
||||
self.flt = flowfilter.parse(flt)
|
||||
self.injection_gen = injection_gen
|
||||
with open(url_index_file, "r") as f:
|
||||
self.url_store = json.load(f)
|
||||
|
||||
def response(self, flow: HTTPFlow):
|
||||
"""Checks if the response matches the filter and such should be injected.
|
||||
Injects the URL index if appropriate.
|
||||
"""
|
||||
if flow.response is not None:
|
||||
if self.flt is not None and self.flt(flow):
|
||||
self.injection_gen.inject(self.url_store, flow)
|
||||
flow.response.status_code = 200
|
||||
flow.response.headers["content-type"] = "text/html"
|
||||
logger.debug(f"Set status code to 200 and set content to logged "
|
||||
f"urls. Method: {self.injection_gen}")
|
71
examples/complex/webscanner_helper/watchdog.py
Normal file
71
examples/complex/webscanner_helper/watchdog.py
Normal file
@ -0,0 +1,71 @@
|
||||
import pathlib
|
||||
import time
|
||||
import typing
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
import mitmproxy.connections
|
||||
import mitmproxy.http
|
||||
from mitmproxy.addons.export import curl_command, raw
|
||||
from mitmproxy.exceptions import HttpSyntaxException
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WatchdogAddon():
|
||||
""" The Watchdog Add-on can be used in combination with web application scanners in oder to check if the device
|
||||
under test responds correctls to the scanner's responses.
|
||||
|
||||
The Watchdog Add-on checks if the device under test responds correctly to the scanner's responses.
|
||||
If the Watchdog sees that the DUT is no longer responding correctly, an multiprocessing event is set.
|
||||
This information can be used to restart the device under test if necessary.
|
||||
"""
|
||||
|
||||
def __init__(self, event, outdir: pathlib.Path, timeout=None):
|
||||
"""Initializes the Watchdog.
|
||||
|
||||
Args:
|
||||
event: multiprocessing.Event that will be set if the watchdog is triggered.
|
||||
outdir: path to a directory in which the triggering requests will be saved (curl and raw).
|
||||
timeout_conn: float that specifies the timeout for the server connection
|
||||
"""
|
||||
self.error_event = event
|
||||
self.flow_dir = outdir
|
||||
if self.flow_dir.exists() and not self.flow_dir.is_dir():
|
||||
raise RuntimeError("Watchtdog output path must be a directory.")
|
||||
elif not self.flow_dir.exists():
|
||||
self.flow_dir.mkdir(parents=True)
|
||||
self.last_trigger: typing.Union[None, float] = None
|
||||
self.timeout: typing.Union[None, float] = timeout
|
||||
|
||||
def serverconnect(self, conn: mitmproxy.connections.ServerConnection):
|
||||
if self.timeout is not None:
|
||||
conn.settimeout(self.timeout)
|
||||
|
||||
@classmethod
|
||||
def not_in_timeout(cls, last_triggered, timeout):
|
||||
"""Checks if current error lies not in timeout after last trigger (potential reset of connection)."""
|
||||
return last_triggered is None or timeout is None or (time.time() - last_triggered > timeout)
|
||||
|
||||
def error(self, flow):
|
||||
""" Checks if the watchdog will be triggered.
|
||||
|
||||
Only triggers watchdog for timeouts after last reset and if flow.error is set (shows that error is a server
|
||||
error). Ignores HttpSyntaxException Errors since this can be triggered on purpose by web application scanner.
|
||||
|
||||
Args:
|
||||
flow: mitmproxy.http.flow
|
||||
"""
|
||||
if (self.not_in_timeout(self.last_trigger, self.timeout)
|
||||
and flow.error is not None and not isinstance(flow.error, HttpSyntaxException)):
|
||||
|
||||
self.last_trigger = time.time()
|
||||
logger.error(f"Watchdog triggered! Cause: {flow}")
|
||||
self.error_event.set()
|
||||
|
||||
# save the request which might have caused the problem
|
||||
if flow.request:
|
||||
with (self.flow_dir / f"{datetime.utcnow().isoformat()}.curl").open("w") as f:
|
||||
f.write(curl_command(flow))
|
||||
with (self.flow_dir / f"{datetime.utcnow().isoformat()}.raw").open("wb") as f:
|
||||
f.write(raw(flow))
|
0
test/examples/webscanner_helper/__init__.py
Normal file
0
test/examples/webscanner_helper/__init__.py
Normal file
165
test/examples/webscanner_helper/test_mapping.py
Normal file
165
test/examples/webscanner_helper/test_mapping.py
Normal file
@ -0,0 +1,165 @@
|
||||
from typing import TextIO, Callable
|
||||
from unittest import mock
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from mitmproxy.test import tflow
|
||||
from mitmproxy.test import tutils
|
||||
|
||||
from examples.complex.webscanner_helper.mapping import MappingAddon, MappingAddonConfig
|
||||
|
||||
|
||||
class TestConfig:
|
||||
|
||||
def test_config(self):
|
||||
assert MappingAddonConfig.HTML_PARSER == "html.parser"
|
||||
|
||||
|
||||
url = "http://10.10.10.10"
|
||||
new_content = "My Text"
|
||||
mapping_content = f'{{"{url}": {{"body": "{new_content}"}}}}'
|
||||
|
||||
|
||||
class TestMappingAddon:
|
||||
|
||||
def test_init(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write(mapping_content)
|
||||
mapping = MappingAddon(tmpfile)
|
||||
assert "My Text" in str(mapping.mapping_templates._dump())
|
||||
|
||||
def test_load(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write(mapping_content)
|
||||
mapping = MappingAddon(tmpfile)
|
||||
loader = MagicMock()
|
||||
|
||||
mapping.load(loader)
|
||||
assert 'mapping_file' in str(loader.add_option.call_args_list)
|
||||
assert 'map_persistent' in str(loader.add_option.call_args_list)
|
||||
|
||||
def test_configure(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write(mapping_content)
|
||||
mapping = MappingAddon(tmpfile)
|
||||
new_filename = "My new filename"
|
||||
updated = {str(mapping.OPT_MAPPING_FILE): new_filename, str(mapping.OPT_MAP_PERSISTENT): True}
|
||||
|
||||
open_mock = mock.mock_open(read_data="{}")
|
||||
with mock.patch("builtins.open", open_mock):
|
||||
mapping.configure(updated)
|
||||
assert new_filename in str(open_mock.mock_calls)
|
||||
assert mapping.filename == new_filename
|
||||
assert mapping.persistent
|
||||
|
||||
def test_response_filtered(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write(mapping_content)
|
||||
mapping = MappingAddon(tmpfile)
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
test_content = b"Test"
|
||||
f.response.content = test_content
|
||||
|
||||
mapping.response(f)
|
||||
assert f.response.content == test_content
|
||||
|
||||
def test_response(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write(mapping_content)
|
||||
mapping = MappingAddon(tmpfile)
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
test_content = b"<body> Test </body>"
|
||||
f.response.content = test_content
|
||||
f.request.url = url
|
||||
|
||||
mapping.response(f)
|
||||
assert f.response.content.decode("utf-8") == new_content
|
||||
|
||||
def test_response_content_type(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write(mapping_content)
|
||||
mapping = MappingAddon(tmpfile)
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
test_content = b"<body> Test </body>"
|
||||
f.response.content = test_content
|
||||
f.request.url = url
|
||||
f.response.headers.add("content-type", "content-type")
|
||||
|
||||
mapping.response(f)
|
||||
assert f.response.content == test_content
|
||||
|
||||
def test_response_not_existing(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write(mapping_content)
|
||||
mapping = MappingAddon(tmpfile)
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
test_content = b"<title> Test </title>"
|
||||
f.response.content = test_content
|
||||
f.request.url = url
|
||||
mapping.response(f)
|
||||
assert f.response.content == test_content
|
||||
|
||||
def test_persistance_false(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write(mapping_content)
|
||||
mapping = MappingAddon(tmpfile)
|
||||
|
||||
open_mock = mock.mock_open(read_data="{}")
|
||||
with mock.patch("builtins.open", open_mock):
|
||||
mapping.done()
|
||||
assert len(open_mock.mock_calls) == 0
|
||||
|
||||
def test_persistance_true(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write(mapping_content)
|
||||
mapping = MappingAddon(tmpfile, persistent=True)
|
||||
|
||||
open_mock = mock.mock_open(read_data="{}")
|
||||
with mock.patch("builtins.open", open_mock):
|
||||
mapping.done()
|
||||
with open(tmpfile, "r") as tfile:
|
||||
results = tfile.read()
|
||||
assert len(open_mock.mock_calls) != 0
|
||||
assert results == mapping_content
|
||||
|
||||
def test_persistance_true_add_content(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write(mapping_content)
|
||||
mapping = MappingAddon(tmpfile, persistent=True)
|
||||
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
test_content = b"<title> Test </title>"
|
||||
f.response.content = test_content
|
||||
f.request.url = url
|
||||
|
||||
mapping.response(f)
|
||||
mapping.done()
|
||||
with open(tmpfile, "r") as tfile:
|
||||
results = tfile.read()
|
||||
assert mapping_content in results
|
||||
|
||||
def mock_dump(self, f: TextIO, value_dumper: Callable):
|
||||
assert value_dumper(None) == "None"
|
||||
try:
|
||||
value_dumper("Test")
|
||||
except RuntimeError:
|
||||
assert True
|
||||
else:
|
||||
assert False
|
||||
|
||||
def test_dump(selfself, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write("{}")
|
||||
mapping = MappingAddon(tmpfile, persistent=True)
|
||||
with mock.patch('examples.complex.webscanner_helper.urldict.URLDict.dump', selfself.mock_dump):
|
||||
mapping.done()
|
89
test/examples/webscanner_helper/test_urldict.py
Normal file
89
test/examples/webscanner_helper/test_urldict.py
Normal file
@ -0,0 +1,89 @@
|
||||
from mitmproxy.test import tflow, tutils
|
||||
from examples.complex.webscanner_helper.urldict import URLDict
|
||||
|
||||
url = "http://10.10.10.10"
|
||||
new_content_body = "New Body"
|
||||
new_content_title = "New Title"
|
||||
content = f'{{"body": "{new_content_body}", "title": "{new_content_title}"}}'
|
||||
url_error = "i~nvalid"
|
||||
input_file_content = f'{{"{url}": {content}}}'
|
||||
input_file_content_error = f'{{"{url_error}": {content}}}'
|
||||
|
||||
|
||||
class TestUrlDict:
|
||||
|
||||
def test_urldict_empty(self):
|
||||
urldict = URLDict()
|
||||
dump = urldict.dumps()
|
||||
assert dump == '{}'
|
||||
|
||||
def test_urldict_loads(self):
|
||||
urldict = URLDict.loads(input_file_content)
|
||||
dump = urldict.dumps()
|
||||
assert dump == input_file_content
|
||||
|
||||
def test_urldict_set_error(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write(input_file_content_error)
|
||||
with open(tmpfile, "r") as tfile:
|
||||
try:
|
||||
URLDict.load(tfile)
|
||||
except ValueError:
|
||||
assert True
|
||||
else:
|
||||
assert False
|
||||
|
||||
def test_urldict_get(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write(input_file_content)
|
||||
with open(tmpfile, "r") as tfile:
|
||||
urldict = URLDict.load(tfile)
|
||||
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
f.request.url = url
|
||||
selection = urldict[f]
|
||||
assert "body" in selection[0]
|
||||
assert new_content_body in selection[0]["body"]
|
||||
assert "title" in selection[0]
|
||||
assert new_content_title in selection[0]["title"]
|
||||
|
||||
selection_get = urldict.get(f)
|
||||
assert "body" in selection_get[0]
|
||||
assert new_content_body in selection_get[0]["body"]
|
||||
assert "title" in selection_get[0]
|
||||
assert new_content_title in selection_get[0]["title"]
|
||||
|
||||
try:
|
||||
urldict["body"]
|
||||
except KeyError:
|
||||
assert True
|
||||
else:
|
||||
assert False
|
||||
|
||||
assert urldict.get("body", default="default") == "default"
|
||||
|
||||
def test_urldict_dumps(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write(input_file_content)
|
||||
with open(tmpfile, "r") as tfile:
|
||||
urldict = URLDict.load(tfile)
|
||||
|
||||
dump = urldict.dumps()
|
||||
assert dump == input_file_content
|
||||
|
||||
def test_urldict_dump(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
outfile = tmpdir.join("outfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write(input_file_content)
|
||||
with open(tmpfile, "r") as tfile:
|
||||
urldict = URLDict.load(tfile)
|
||||
with open(outfile, "w") as ofile:
|
||||
urldict.dump(ofile)
|
||||
|
||||
with open(outfile, "r") as ofile:
|
||||
output = ofile.read()
|
||||
assert output == input_file_content
|
234
test/examples/webscanner_helper/test_urlindex.py
Normal file
234
test/examples/webscanner_helper/test_urlindex.py
Normal file
@ -0,0 +1,234 @@
|
||||
import json
|
||||
from json import JSONDecodeError
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
from typing import List
|
||||
from unittest.mock import patch
|
||||
|
||||
from mitmproxy.test import tflow
|
||||
from mitmproxy.test import tutils
|
||||
|
||||
from examples.complex.webscanner_helper.urlindex import UrlIndexWriter, SetEncoder, JSONUrlIndexWriter, TextUrlIndexWriter, WRITER, \
|
||||
filter_404, \
|
||||
UrlIndexAddon
|
||||
|
||||
|
||||
class TestBaseClass:
|
||||
|
||||
@patch.multiple(UrlIndexWriter, __abstractmethods__=set())
|
||||
def test_base_class(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
index_writer = UrlIndexWriter(tmpfile)
|
||||
index_writer.load()
|
||||
index_writer.add_url(tflow.tflow())
|
||||
index_writer.save()
|
||||
|
||||
|
||||
class TestSetEncoder:
|
||||
|
||||
def test_set_encoder_set(self):
|
||||
test_set = {"foo", "bar", "42"}
|
||||
result = SetEncoder.default(SetEncoder(), test_set)
|
||||
assert isinstance(result, List)
|
||||
assert 'foo' in result
|
||||
assert 'bar' in result
|
||||
assert '42' in result
|
||||
|
||||
def test_set_encoder_str(self):
|
||||
test_str = "test"
|
||||
try:
|
||||
SetEncoder.default(SetEncoder(), test_str)
|
||||
except TypeError:
|
||||
assert True
|
||||
else:
|
||||
assert False
|
||||
|
||||
|
||||
class TestJSONUrlIndexWriter:
|
||||
|
||||
def test_load(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write(
|
||||
"{\"http://example.com:80\": {\"/\": {\"GET\": [301]}}, \"http://www.example.com:80\": {\"/\": {\"GET\": [302]}}}")
|
||||
writer = JSONUrlIndexWriter(filename=tmpfile)
|
||||
writer.load()
|
||||
assert 'http://example.com:80' in writer.host_urls
|
||||
assert '/' in writer.host_urls['http://example.com:80']
|
||||
assert 'GET' in writer.host_urls['http://example.com:80']['/']
|
||||
assert 301 in writer.host_urls['http://example.com:80']['/']['GET']
|
||||
|
||||
def test_load_empty(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write("{}")
|
||||
writer = JSONUrlIndexWriter(filename=tmpfile)
|
||||
writer.load()
|
||||
assert len(writer.host_urls) == 0
|
||||
|
||||
def test_load_nonexisting(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
writer = JSONUrlIndexWriter(filename=tmpfile)
|
||||
writer.load()
|
||||
assert len(writer.host_urls) == 0
|
||||
|
||||
def test_add(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
writer = JSONUrlIndexWriter(filename=tmpfile)
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
url = f"{f.request.scheme}://{f.request.host}:{f.request.port}"
|
||||
writer.add_url(f)
|
||||
assert url in writer.host_urls
|
||||
assert f.request.path in writer.host_urls[url]
|
||||
|
||||
def test_save(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
writer = JSONUrlIndexWriter(filename=tmpfile)
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
url = f"{f.request.scheme}://{f.request.host}:{f.request.port}"
|
||||
writer.add_url(f)
|
||||
writer.save()
|
||||
|
||||
with open(tmpfile, "r") as results:
|
||||
try:
|
||||
content = json.load(results)
|
||||
except JSONDecodeError:
|
||||
assert False
|
||||
assert url in content
|
||||
|
||||
|
||||
class TestTestUrlIndexWriter:
|
||||
def test_load(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write(
|
||||
"2020-04-22T05:41:08.679231 STATUS: 200 METHOD: GET URL:http://example.com")
|
||||
writer = TextUrlIndexWriter(filename=tmpfile)
|
||||
writer.load()
|
||||
assert True
|
||||
|
||||
def test_load_empty(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write("{}")
|
||||
writer = TextUrlIndexWriter(filename=tmpfile)
|
||||
writer.load()
|
||||
assert True
|
||||
|
||||
def test_load_nonexisting(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
writer = TextUrlIndexWriter(filename=tmpfile)
|
||||
writer.load()
|
||||
assert True
|
||||
|
||||
def test_add(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
writer = TextUrlIndexWriter(filename=tmpfile)
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
url = f"{f.request.scheme}://{f.request.host}:{f.request.port}"
|
||||
method = f.request.method
|
||||
code = f.response.status_code
|
||||
writer.add_url(f)
|
||||
|
||||
with open(tmpfile, "r") as results:
|
||||
content = results.read()
|
||||
assert url in content
|
||||
assert method in content
|
||||
assert str(code) in content
|
||||
|
||||
def test_save(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
writer = TextUrlIndexWriter(filename=tmpfile)
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
url = f"{f.request.scheme}://{f.request.host}:{f.request.port}"
|
||||
method = f.request.method
|
||||
code = f.response.status_code
|
||||
writer.add_url(f)
|
||||
writer.save()
|
||||
|
||||
with open(tmpfile, "r") as results:
|
||||
content = results.read()
|
||||
assert url in content
|
||||
assert method in content
|
||||
assert str(code) in content
|
||||
|
||||
|
||||
class TestWriter:
|
||||
def test_writer_dict(self):
|
||||
assert "json" in WRITER
|
||||
assert isinstance(WRITER["json"], JSONUrlIndexWriter.__class__)
|
||||
assert "text" in WRITER
|
||||
assert isinstance(WRITER["text"], TextUrlIndexWriter.__class__)
|
||||
|
||||
|
||||
class TestFilter:
|
||||
def test_filer_true(self):
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
assert filter_404(f)
|
||||
|
||||
def test_filter_false(self):
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
f.response.status_code = 404
|
||||
assert not filter_404(f)
|
||||
|
||||
|
||||
class TestUrlIndexAddon:
|
||||
|
||||
def test_init(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
UrlIndexAddon(tmpfile)
|
||||
|
||||
def test_init_format(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
try:
|
||||
UrlIndexAddon(tmpfile, index_format="test")
|
||||
except ValueError:
|
||||
assert True
|
||||
else:
|
||||
assert False
|
||||
|
||||
def test_init_filter(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
try:
|
||||
UrlIndexAddon(tmpfile, index_filter="i~nvalid")
|
||||
except ValueError:
|
||||
assert True
|
||||
else:
|
||||
assert False
|
||||
|
||||
def test_init_append(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write("")
|
||||
url_index = UrlIndexAddon(tmpfile, append=False)
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
with mock.patch('examples.complex.webscanner_helper.urlindex.JSONUrlIndexWriter.add_url'):
|
||||
url_index.response(f)
|
||||
assert not Path(tmpfile).exists()
|
||||
|
||||
def test_response(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
url_index = UrlIndexAddon(tmpfile)
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
with mock.patch('examples.complex.webscanner_helper.urlindex.JSONUrlIndexWriter.add_url') as mock_add_url:
|
||||
url_index.response(f)
|
||||
mock_add_url.assert_called()
|
||||
|
||||
def test_response_None(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
url_index = UrlIndexAddon(tmpfile)
|
||||
url_index.index_filter = None
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
try:
|
||||
url_index.response(f)
|
||||
except ValueError:
|
||||
assert True
|
||||
else:
|
||||
assert False
|
||||
|
||||
def test_done(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
url_index = UrlIndexAddon(tmpfile)
|
||||
with mock.patch('examples.complex.webscanner_helper.urlindex.JSONUrlIndexWriter.save') as mock_save:
|
||||
url_index.done()
|
||||
mock_save.assert_called()
|
111
test/examples/webscanner_helper/test_urlinjection.py
Normal file
111
test/examples/webscanner_helper/test_urlinjection.py
Normal file
@ -0,0 +1,111 @@
|
||||
import json
|
||||
from unittest import mock
|
||||
|
||||
from mitmproxy import flowfilter
|
||||
from mitmproxy.test import tflow
|
||||
from mitmproxy.test import tutils
|
||||
|
||||
from examples.complex.webscanner_helper.urlinjection import InjectionGenerator, HTMLInjection, RobotsInjection, SitemapInjection, \
|
||||
UrlInjectionAddon, logger
|
||||
|
||||
index = json.loads(
|
||||
"{\"http://example.com:80\": {\"/\": {\"GET\": [301]}}, \"http://www.example.com:80\": {\"/test\": {\"POST\": [302]}}}")
|
||||
|
||||
|
||||
class TestInjectionGenerator:
|
||||
|
||||
def test_inject(self):
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
injection_generator = InjectionGenerator()
|
||||
injection_generator.inject(index=index, flow=f)
|
||||
assert True
|
||||
|
||||
|
||||
class TestHTMLInjection:
|
||||
|
||||
def test_inject_not404(self):
|
||||
html_injection = HTMLInjection()
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
|
||||
with mock.patch.object(logger, 'warning') as mock_warning:
|
||||
html_injection.inject(index, f)
|
||||
assert mock_warning.called
|
||||
|
||||
def test_inject_insert(self):
|
||||
html_injection = HTMLInjection(insert=True)
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
assert "example.com" not in str(f.response.content)
|
||||
html_injection.inject(index, f)
|
||||
assert "example.com" in str(f.response.content)
|
||||
|
||||
def test_inject_insert_body(self):
|
||||
html_injection = HTMLInjection(insert=True)
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
f.response.text = "<body></body>"
|
||||
assert "example.com" not in str(f.response.content)
|
||||
html_injection.inject(index, f)
|
||||
assert "example.com" in str(f.response.content)
|
||||
|
||||
def test_inject_404(self):
|
||||
html_injection = HTMLInjection()
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
f.response.status_code = 404
|
||||
assert "example.com" not in str(f.response.content)
|
||||
html_injection.inject(index, f)
|
||||
assert "example.com" in str(f.response.content)
|
||||
|
||||
|
||||
class TestRobotsInjection:
|
||||
|
||||
def test_inject_not404(self):
|
||||
robots_injection = RobotsInjection()
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
|
||||
with mock.patch.object(logger, 'warning') as mock_warning:
|
||||
robots_injection.inject(index, f)
|
||||
assert mock_warning.called
|
||||
|
||||
def test_inject_404(self):
|
||||
robots_injection = RobotsInjection()
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
f.response.status_code = 404
|
||||
assert "Allow: /test" not in str(f.response.content)
|
||||
robots_injection.inject(index, f)
|
||||
assert "Allow: /test" in str(f.response.content)
|
||||
|
||||
|
||||
class TestSitemapInjection:
|
||||
|
||||
def test_inject_not404(self):
|
||||
sitemap_injection = SitemapInjection()
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
|
||||
with mock.patch.object(logger, 'warning') as mock_warning:
|
||||
sitemap_injection.inject(index, f)
|
||||
assert mock_warning.called
|
||||
|
||||
def test_inject_404(self):
|
||||
sitemap_injection = SitemapInjection()
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
f.response.status_code = 404
|
||||
assert "<url><loc>http://example.com:80/</loc></url>" not in str(f.response.content)
|
||||
sitemap_injection.inject(index, f)
|
||||
assert "<url><loc>http://example.com:80/</loc></url>" in str(f.response.content)
|
||||
|
||||
|
||||
class TestUrlInjectionAddon:
|
||||
|
||||
def test_init(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
json.dump(index, tfile)
|
||||
flt = f"~u .*/site.html$"
|
||||
url_injection = UrlInjectionAddon(f"~u .*/site.html$", tmpfile, HTMLInjection(insert=True))
|
||||
assert "http://example.com:80" in url_injection.url_store
|
||||
fltr = flowfilter.parse(flt)
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
f.request.url = "http://example.com/site.html"
|
||||
assert fltr(f)
|
||||
assert "http://example.com:80" not in str(f.response.content)
|
||||
url_injection.response(f)
|
||||
assert "http://example.com:80" in str(f.response.content)
|
84
test/examples/webscanner_helper/test_watchdog.py
Normal file
84
test/examples/webscanner_helper/test_watchdog.py
Normal file
@ -0,0 +1,84 @@
|
||||
import time
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from mitmproxy.connections import ServerConnection
|
||||
from mitmproxy.exceptions import HttpSyntaxException
|
||||
from mitmproxy.test import tflow
|
||||
from mitmproxy.test import tutils
|
||||
import multiprocessing
|
||||
|
||||
from examples.complex.webscanner_helper.watchdog import WatchdogAddon, logger
|
||||
|
||||
|
||||
class TestWatchdog:
|
||||
|
||||
def test_init_file(self, tmpdir):
|
||||
tmpfile = tmpdir.join("tmpfile")
|
||||
with open(tmpfile, "w") as tfile:
|
||||
tfile.write("")
|
||||
event = multiprocessing.Event()
|
||||
try:
|
||||
WatchdogAddon(event, Path(tmpfile))
|
||||
except RuntimeError:
|
||||
assert True
|
||||
else:
|
||||
assert False
|
||||
|
||||
def test_init_dir(self, tmpdir):
|
||||
event = multiprocessing.Event()
|
||||
mydir = tmpdir.join("mydir")
|
||||
assert not Path(mydir).exists()
|
||||
WatchdogAddon(event, Path(mydir))
|
||||
assert Path(mydir).exists()
|
||||
|
||||
def test_serverconnect(self, tmpdir):
|
||||
event = multiprocessing.Event()
|
||||
w = WatchdogAddon(event, Path(tmpdir), timeout=10)
|
||||
with mock.patch('mitmproxy.connections.ServerConnection.settimeout') as mock_set_timeout:
|
||||
w.serverconnect(ServerConnection("127.0.0.1"))
|
||||
mock_set_timeout.assert_called()
|
||||
|
||||
def test_serverconnect_None(self, tmpdir):
|
||||
event = multiprocessing.Event()
|
||||
w = WatchdogAddon(event, Path(tmpdir))
|
||||
with mock.patch('mitmproxy.connections.ServerConnection.settimeout') as mock_set_timeout:
|
||||
w.serverconnect(ServerConnection("127.0.0.1"))
|
||||
assert not mock_set_timeout.called
|
||||
|
||||
def test_trigger(self, tmpdir):
|
||||
event = multiprocessing.Event()
|
||||
w = WatchdogAddon(event, Path(tmpdir))
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
f.error = "Test Error"
|
||||
|
||||
with mock.patch.object(logger, 'error') as mock_error:
|
||||
open_mock = mock.mock_open()
|
||||
with mock.patch("pathlib.Path.open", open_mock, create=True):
|
||||
w.error(f)
|
||||
mock_error.assert_called()
|
||||
open_mock.assert_called()
|
||||
|
||||
def test_trigger_http_synatx(self, tmpdir):
|
||||
event = multiprocessing.Event()
|
||||
w = WatchdogAddon(event, Path(tmpdir))
|
||||
f = tflow.tflow(resp=tutils.tresp())
|
||||
f.error = HttpSyntaxException()
|
||||
assert isinstance(f.error, HttpSyntaxException)
|
||||
|
||||
with mock.patch.object(logger, 'error') as mock_error:
|
||||
open_mock = mock.mock_open()
|
||||
with mock.patch("pathlib.Path.open", open_mock, create=True):
|
||||
w.error(f)
|
||||
assert not mock_error.called
|
||||
assert not open_mock.called
|
||||
|
||||
def test_timeout(self, tmpdir):
|
||||
event = multiprocessing.Event()
|
||||
w = WatchdogAddon(event, Path(tmpdir))
|
||||
|
||||
assert w.not_in_timeout(None, None)
|
||||
assert w.not_in_timeout(time.time, None)
|
||||
with mock.patch('time.time', return_value=5):
|
||||
assert not w.not_in_timeout(3, 20)
|
||||
assert w.not_in_timeout(3, 1)
|
Loading…
Reference in New Issue
Block a user