mirror of
https://github.com/caronc/apprise.git
synced 2024-11-25 01:24:03 +01:00
Security CWE-312 and CWE-20 Handling (#453)
This commit is contained in:
parent
abb7547f20
commit
e2ebdbdcf8
@ -34,6 +34,7 @@ from .common import MATCH_ALL_TAG
|
|||||||
from .utils import is_exclusive_match
|
from .utils import is_exclusive_match
|
||||||
from .utils import parse_list
|
from .utils import parse_list
|
||||||
from .utils import parse_urls
|
from .utils import parse_urls
|
||||||
|
from .utils import cwe312_url
|
||||||
from .logger import logger
|
from .logger import logger
|
||||||
|
|
||||||
from .AppriseAsset import AppriseAsset
|
from .AppriseAsset import AppriseAsset
|
||||||
@ -123,9 +124,14 @@ class Apprise(object):
|
|||||||
# Initialize our result set
|
# Initialize our result set
|
||||||
results = None
|
results = None
|
||||||
|
|
||||||
|
# Prepare our Asset Object
|
||||||
|
asset = asset if isinstance(asset, AppriseAsset) else AppriseAsset()
|
||||||
|
|
||||||
if isinstance(url, six.string_types):
|
if isinstance(url, six.string_types):
|
||||||
# Acquire our url tokens
|
# Acquire our url tokens
|
||||||
results = plugins.url_to_dict(url)
|
results = plugins.url_to_dict(
|
||||||
|
url, secure_logging=asset.secure_logging)
|
||||||
|
|
||||||
if results is None:
|
if results is None:
|
||||||
# Failed to parse the server URL; detailed logging handled
|
# Failed to parse the server URL; detailed logging handled
|
||||||
# inside url_to_dict - nothing to report here.
|
# inside url_to_dict - nothing to report here.
|
||||||
@ -139,25 +145,30 @@ class Apprise(object):
|
|||||||
# schema is a mandatory dictionary item as it is the only way
|
# schema is a mandatory dictionary item as it is the only way
|
||||||
# we can index into our loaded plugins
|
# we can index into our loaded plugins
|
||||||
logger.error('Dictionary does not include a "schema" entry.')
|
logger.error('Dictionary does not include a "schema" entry.')
|
||||||
logger.trace('Invalid dictionary unpacked as:{}{}'.format(
|
logger.trace(
|
||||||
|
'Invalid dictionary unpacked as:{}{}'.format(
|
||||||
os.linesep, os.linesep.join(
|
os.linesep, os.linesep.join(
|
||||||
['{}="{}"'.format(k, v) for k, v in results.items()])))
|
['{}="{}"'.format(k, v)
|
||||||
|
for k, v in results.items()])))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logger.trace('Dictionary unpacked as:{}{}'.format(
|
logger.trace(
|
||||||
|
'Dictionary unpacked as:{}{}'.format(
|
||||||
os.linesep, os.linesep.join(
|
os.linesep, os.linesep.join(
|
||||||
['{}="{}"'.format(k, v) for k, v in results.items()])))
|
['{}="{}"'.format(k, v) for k, v in results.items()])))
|
||||||
|
|
||||||
|
# Otherwise we handle the invalid input specified
|
||||||
else:
|
else:
|
||||||
logger.error('Invalid URL specified: {}'.format(url))
|
logger.error(
|
||||||
|
'An invalid URL type (%s) was specified for instantiation',
|
||||||
|
type(url))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Build a list of tags to associate with the newly added notifications
|
# Build a list of tags to associate with the newly added notifications
|
||||||
results['tag'] = set(parse_list(tag))
|
results['tag'] = set(parse_list(tag))
|
||||||
|
|
||||||
# Prepare our Asset Object
|
# Set our Asset Object
|
||||||
results['asset'] = \
|
results['asset'] = asset
|
||||||
asset if isinstance(asset, AppriseAsset) else AppriseAsset()
|
|
||||||
|
|
||||||
if suppress_exceptions:
|
if suppress_exceptions:
|
||||||
try:
|
try:
|
||||||
@ -166,14 +177,21 @@ class Apprise(object):
|
|||||||
plugin = plugins.SCHEMA_MAP[results['schema']](**results)
|
plugin = plugins.SCHEMA_MAP[results['schema']](**results)
|
||||||
|
|
||||||
# Create log entry of loaded URL
|
# Create log entry of loaded URL
|
||||||
logger.debug('Loaded {} URL: {}'.format(
|
logger.debug(
|
||||||
|
'Loaded {} URL: {}'.format(
|
||||||
plugins.SCHEMA_MAP[results['schema']].service_name,
|
plugins.SCHEMA_MAP[results['schema']].service_name,
|
||||||
plugin.url()))
|
plugin.url(privacy=asset.secure_logging)))
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
# CWE-312 (Secure Logging) Handling
|
||||||
|
loggable_url = url if not asset.secure_logging \
|
||||||
|
else cwe312_url(url)
|
||||||
|
|
||||||
# the arguments are invalid or can not be used.
|
# the arguments are invalid or can not be used.
|
||||||
logger.error('Could not load {} URL: {}'.format(
|
logger.error(
|
||||||
plugins.SCHEMA_MAP[results['schema']].service_name, url))
|
'Could not load {} URL: {}'.format(
|
||||||
|
plugins.SCHEMA_MAP[results['schema']].service_name,
|
||||||
|
loggable_url))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -402,7 +420,7 @@ class Apprise(object):
|
|||||||
except Exception:
|
except Exception:
|
||||||
# A catch all so we don't have to abort early
|
# A catch all so we don't have to abort early
|
||||||
# just because one of our plugins has a bug in it.
|
# just because one of our plugins has a bug in it.
|
||||||
logger.exception("Notification Exception")
|
logger.exception("Unhandled Notification Exception")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -434,10 +452,10 @@ class Apprise(object):
|
|||||||
|
|
||||||
if len(self) == 0:
|
if len(self) == 0:
|
||||||
# Nothing to notify
|
# Nothing to notify
|
||||||
raise TypeError
|
raise TypeError("No service(s) to notify")
|
||||||
|
|
||||||
if not (title or body):
|
if not (title or body):
|
||||||
raise TypeError
|
raise TypeError("No message content specified to deliver")
|
||||||
|
|
||||||
if six.PY2:
|
if six.PY2:
|
||||||
# Python 2.7.x Unicode Character Handling
|
# Python 2.7.x Unicode Character Handling
|
||||||
|
@ -110,6 +110,17 @@ class AppriseAsset(object):
|
|||||||
# to a new line.
|
# to a new line.
|
||||||
interpret_escapes = False
|
interpret_escapes = False
|
||||||
|
|
||||||
|
# For more detail see CWE-312 @
|
||||||
|
# https://cwe.mitre.org/data/definitions/312.html
|
||||||
|
#
|
||||||
|
# By enabling this, the logging output has additional overhead applied to
|
||||||
|
# it preventing secure password and secret information from being
|
||||||
|
# displayed in the logging. Since there is overhead involved in performing
|
||||||
|
# this cleanup; system owners who run in a very isolated environment may
|
||||||
|
# choose to disable this for a slight performance bump. It is recommended
|
||||||
|
# that you leave this option as is otherwise.
|
||||||
|
secure_logging = True
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Asset Initialization
|
Asset Initialization
|
||||||
|
@ -39,6 +39,7 @@ from ..utils import GET_SCHEMA_RE
|
|||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
from ..utils import parse_bool
|
from ..utils import parse_bool
|
||||||
from ..utils import parse_urls
|
from ..utils import parse_urls
|
||||||
|
from ..utils import cwe312_url
|
||||||
from . import SCHEMA_MAP
|
from . import SCHEMA_MAP
|
||||||
|
|
||||||
# Test whether token is valid or not
|
# Test whether token is valid or not
|
||||||
@ -209,8 +210,8 @@ class ConfigBase(URLBase):
|
|||||||
# Configuration files were detected; recursively populate them
|
# Configuration files were detected; recursively populate them
|
||||||
# If we have been configured to do so
|
# If we have been configured to do so
|
||||||
for url in configs:
|
for url in configs:
|
||||||
if self.recursion > 0:
|
|
||||||
|
|
||||||
|
if self.recursion > 0:
|
||||||
# Attempt to acquire the schema at the very least to allow
|
# Attempt to acquire the schema at the very least to allow
|
||||||
# our configuration based urls.
|
# our configuration based urls.
|
||||||
schema = GET_SCHEMA_RE.match(url)
|
schema = GET_SCHEMA_RE.match(url)
|
||||||
@ -223,6 +224,7 @@ class ConfigBase(URLBase):
|
|||||||
url = os.path.join(self.config_path, url)
|
url = os.path.join(self.config_path, url)
|
||||||
|
|
||||||
url = '{}://{}'.format(schema, URLBase.quote(url))
|
url = '{}://{}'.format(schema, URLBase.quote(url))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Ensure our schema is always in lower case
|
# Ensure our schema is always in lower case
|
||||||
schema = schema.group('schema').lower()
|
schema = schema.group('schema').lower()
|
||||||
@ -233,13 +235,17 @@ class ConfigBase(URLBase):
|
|||||||
'Unsupported include schema {}.'.format(schema))
|
'Unsupported include schema {}.'.format(schema))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# CWE-312 (Secure Logging) Handling
|
||||||
|
loggable_url = url if not asset.secure_logging \
|
||||||
|
else cwe312_url(url)
|
||||||
|
|
||||||
# Parse our url details of the server object as dictionary
|
# Parse our url details of the server object as dictionary
|
||||||
# containing all of the information parsed from our URL
|
# containing all of the information parsed from our URL
|
||||||
results = SCHEMA_MAP[schema].parse_url(url)
|
results = SCHEMA_MAP[schema].parse_url(url)
|
||||||
if not results:
|
if not results:
|
||||||
# Failed to parse the server URL
|
# Failed to parse the server URL
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Unparseable include URL {}'.format(url))
|
'Unparseable include URL {}'.format(loggable_url))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Handle cross inclusion based on allow_cross_includes rules
|
# Handle cross inclusion based on allow_cross_includes rules
|
||||||
@ -253,7 +259,7 @@ class ConfigBase(URLBase):
|
|||||||
# Prevent the loading if insecure base protocols
|
# Prevent the loading if insecure base protocols
|
||||||
ConfigBase.logger.warning(
|
ConfigBase.logger.warning(
|
||||||
'Including {}:// based configuration is prohibited. '
|
'Including {}:// based configuration is prohibited. '
|
||||||
'Ignoring URL {}'.format(schema, url))
|
'Ignoring URL {}'.format(schema, loggable_url))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Prepare our Asset Object
|
# Prepare our Asset Object
|
||||||
@ -279,7 +285,7 @@ class ConfigBase(URLBase):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
# the arguments are invalid or can not be used.
|
# the arguments are invalid or can not be used.
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Could not load include URL: {}'.format(url))
|
'Could not load include URL: {}'.format(loggable_url))
|
||||||
self.logger.debug('Loading Exception: {}'.format(str(e)))
|
self.logger.debug('Loading Exception: {}'.format(str(e)))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -292,16 +298,23 @@ class ConfigBase(URLBase):
|
|||||||
del cfg_plugin
|
del cfg_plugin
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
# CWE-312 (Secure Logging) Handling
|
||||||
|
loggable_url = url if not asset.secure_logging \
|
||||||
|
else cwe312_url(url)
|
||||||
|
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
'Recursion limit reached; ignoring Include URL: %s' % url)
|
'Recursion limit reached; ignoring Include URL: %s',
|
||||||
|
loggable_url)
|
||||||
|
|
||||||
if self._cached_servers:
|
if self._cached_servers:
|
||||||
self.logger.info('Loaded {} entries from {}'.format(
|
self.logger.info(
|
||||||
len(self._cached_servers), self.url()))
|
'Loaded {} entries from {}'.format(
|
||||||
|
len(self._cached_servers),
|
||||||
|
self.url(privacy=asset.secure_logging)))
|
||||||
else:
|
else:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
'Failed to load Apprise configuration from {}'.format(
|
'Failed to load Apprise configuration from {}'.format(
|
||||||
self.url()))
|
self.url(privacy=asset.secure_logging)))
|
||||||
|
|
||||||
# Set the time our content was cached at
|
# Set the time our content was cached at
|
||||||
self._cached_time = time.time()
|
self._cached_time = time.time()
|
||||||
@ -531,6 +544,9 @@ class ConfigBase(URLBase):
|
|||||||
# the include keyword
|
# the include keyword
|
||||||
configs = list()
|
configs = list()
|
||||||
|
|
||||||
|
# Prepare our Asset Object
|
||||||
|
asset = asset if isinstance(asset, AppriseAsset) else AppriseAsset()
|
||||||
|
|
||||||
# Define what a valid line should look like
|
# Define what a valid line should look like
|
||||||
valid_line_re = re.compile(
|
valid_line_re = re.compile(
|
||||||
r'^\s*(?P<line>([;#]+(?P<comment>.*))|'
|
r'^\s*(?P<line>([;#]+(?P<comment>.*))|'
|
||||||
@ -567,27 +583,37 @@ class ConfigBase(URLBase):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if config:
|
if config:
|
||||||
ConfigBase.logger.debug('Include URL: {}'.format(config))
|
# CWE-312 (Secure Logging) Handling
|
||||||
|
loggable_url = config if not asset.secure_logging \
|
||||||
|
else cwe312_url(config)
|
||||||
|
|
||||||
|
ConfigBase.logger.debug(
|
||||||
|
'Include URL: {}'.format(loggable_url))
|
||||||
|
|
||||||
# Store our include line
|
# Store our include line
|
||||||
configs.append(config.strip())
|
configs.append(config.strip())
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# CWE-312 (Secure Logging) Handling
|
||||||
|
loggable_url = url if not asset.secure_logging \
|
||||||
|
else cwe312_url(url)
|
||||||
|
|
||||||
# Acquire our url tokens
|
# Acquire our url tokens
|
||||||
results = plugins.url_to_dict(url)
|
results = plugins.url_to_dict(
|
||||||
|
url, secure_logging=asset.secure_logging)
|
||||||
if results is None:
|
if results is None:
|
||||||
# Failed to parse the server URL
|
# Failed to parse the server URL
|
||||||
ConfigBase.logger.warning(
|
ConfigBase.logger.warning(
|
||||||
'Unparseable URL {} on line {}.'.format(url, line))
|
'Unparseable URL {} on line {}.'.format(
|
||||||
|
loggable_url, line))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Build a list of tags to associate with the newly added
|
# Build a list of tags to associate with the newly added
|
||||||
# notifications if any were set
|
# notifications if any were set
|
||||||
results['tag'] = set(parse_list(result.group('tags')))
|
results['tag'] = set(parse_list(result.group('tags')))
|
||||||
|
|
||||||
# Prepare our Asset Object
|
# Set our Asset Object
|
||||||
results['asset'] = \
|
results['asset'] = asset
|
||||||
asset if isinstance(asset, AppriseAsset) else AppriseAsset()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Attempt to create an instance of our plugin using the
|
# Attempt to create an instance of our plugin using the
|
||||||
@ -595,13 +621,14 @@ class ConfigBase(URLBase):
|
|||||||
plugin = plugins.SCHEMA_MAP[results['schema']](**results)
|
plugin = plugins.SCHEMA_MAP[results['schema']](**results)
|
||||||
|
|
||||||
# Create log entry of loaded URL
|
# Create log entry of loaded URL
|
||||||
ConfigBase.logger.debug('Loaded URL: {}'.format(plugin.url()))
|
ConfigBase.logger.debug(
|
||||||
|
'Loaded URL: %s', plugin.url(privacy=asset.secure_logging))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# the arguments are invalid or can not be used.
|
# the arguments are invalid or can not be used.
|
||||||
ConfigBase.logger.warning(
|
ConfigBase.logger.warning(
|
||||||
'Could not load URL {} on line {}.'.format(
|
'Could not load URL {} on line {}.'.format(
|
||||||
url, line))
|
loggable_url, line))
|
||||||
ConfigBase.logger.debug('Loading Exception: %s' % str(e))
|
ConfigBase.logger.debug('Loading Exception: %s' % str(e))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -756,6 +783,10 @@ class ConfigBase(URLBase):
|
|||||||
# we can. Reset it to None on each iteration
|
# we can. Reset it to None on each iteration
|
||||||
results = list()
|
results = list()
|
||||||
|
|
||||||
|
# CWE-312 (Secure Logging) Handling
|
||||||
|
loggable_url = url if not asset.secure_logging \
|
||||||
|
else cwe312_url(url)
|
||||||
|
|
||||||
if isinstance(url, six.string_types):
|
if isinstance(url, six.string_types):
|
||||||
# We're just a simple URL string...
|
# We're just a simple URL string...
|
||||||
schema = GET_SCHEMA_RE.match(url)
|
schema = GET_SCHEMA_RE.match(url)
|
||||||
@ -764,16 +795,18 @@ class ConfigBase(URLBase):
|
|||||||
# config file at least has something to take action
|
# config file at least has something to take action
|
||||||
# with.
|
# with.
|
||||||
ConfigBase.logger.warning(
|
ConfigBase.logger.warning(
|
||||||
'Invalid URL {}, entry #{}'.format(url, no + 1))
|
'Invalid URL {}, entry #{}'.format(
|
||||||
|
loggable_url, no + 1))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# We found a valid schema worthy of tracking; store it's
|
# We found a valid schema worthy of tracking; store it's
|
||||||
# details:
|
# details:
|
||||||
_results = plugins.url_to_dict(url)
|
_results = plugins.url_to_dict(
|
||||||
|
url, secure_logging=asset.secure_logging)
|
||||||
if _results is None:
|
if _results is None:
|
||||||
ConfigBase.logger.warning(
|
ConfigBase.logger.warning(
|
||||||
'Unparseable URL {}, entry #{}'.format(
|
'Unparseable URL {}, entry #{}'.format(
|
||||||
url, no + 1))
|
loggable_url, no + 1))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# add our results to our global set
|
# add our results to our global set
|
||||||
@ -819,7 +852,8 @@ class ConfigBase(URLBase):
|
|||||||
'Unsupported URL, entry #{}'.format(no + 1))
|
'Unsupported URL, entry #{}'.format(no + 1))
|
||||||
continue
|
continue
|
||||||
|
|
||||||
_results = plugins.url_to_dict(_url)
|
_results = plugins.url_to_dict(
|
||||||
|
_url, secure_logging=asset.secure_logging)
|
||||||
if _results is None:
|
if _results is None:
|
||||||
# Setup dictionary
|
# Setup dictionary
|
||||||
_results = {
|
_results = {
|
||||||
@ -931,7 +965,8 @@ class ConfigBase(URLBase):
|
|||||||
|
|
||||||
# Create log entry of loaded URL
|
# Create log entry of loaded URL
|
||||||
ConfigBase.logger.debug(
|
ConfigBase.logger.debug(
|
||||||
'Loaded URL: {}'.format(plugin.url()))
|
'Loaded URL: {}'.format(
|
||||||
|
plugin.url(privacy=asset.secure_logging)))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# the arguments are invalid or can not be used.
|
# the arguments are invalid or can not be used.
|
||||||
|
@ -301,7 +301,7 @@ class NotifyGoogleChat(NotifyBase):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
result = re.match(
|
result = re.match(
|
||||||
r'^https://chat.googleapis.com/v1/spaces/'
|
r'^https://chat\.googleapis\.com/v1/spaces/'
|
||||||
r'(?P<workspace>[A-Z0-9_-]+)/messages/*(?P<params>.+)$',
|
r'(?P<workspace>[A-Z0-9_-]+)/messages/*(?P<params>.+)$',
|
||||||
url, re.I)
|
url, re.I)
|
||||||
|
|
||||||
|
@ -44,6 +44,7 @@ from ..common import NOTIFY_IMAGE_SIZES
|
|||||||
from ..common import NotifyType
|
from ..common import NotifyType
|
||||||
from ..common import NOTIFY_TYPES
|
from ..common import NOTIFY_TYPES
|
||||||
from ..utils import parse_list
|
from ..utils import parse_list
|
||||||
|
from ..utils import cwe312_url
|
||||||
from ..utils import GET_SCHEMA_RE
|
from ..utils import GET_SCHEMA_RE
|
||||||
from ..logger import logger
|
from ..logger import logger
|
||||||
from ..AppriseLocale import gettext_lazy as _
|
from ..AppriseLocale import gettext_lazy as _
|
||||||
@ -442,7 +443,7 @@ def details(plugin):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def url_to_dict(url):
|
def url_to_dict(url, secure_logging=True):
|
||||||
"""
|
"""
|
||||||
Takes an apprise URL and returns the tokens associated with it
|
Takes an apprise URL and returns the tokens associated with it
|
||||||
if they can be acquired based on the plugins available.
|
if they can be acquired based on the plugins available.
|
||||||
@ -457,13 +458,16 @@ def url_to_dict(url):
|
|||||||
# swap hash (#) tag values with their html version
|
# swap hash (#) tag values with their html version
|
||||||
_url = url.replace('/#', '/%23')
|
_url = url.replace('/#', '/%23')
|
||||||
|
|
||||||
|
# CWE-312 (Secure Logging) Handling
|
||||||
|
loggable_url = url if not secure_logging else cwe312_url(url)
|
||||||
|
|
||||||
# Attempt to acquire the schema at the very least to allow our plugins to
|
# Attempt to acquire the schema at the very least to allow our plugins to
|
||||||
# determine if they can make a better interpretation of a URL geared for
|
# determine if they can make a better interpretation of a URL geared for
|
||||||
# them.
|
# them.
|
||||||
schema = GET_SCHEMA_RE.match(_url)
|
schema = GET_SCHEMA_RE.match(_url)
|
||||||
if schema is None:
|
if schema is None:
|
||||||
# Not a valid URL; take an early exit
|
# Not a valid URL; take an early exit
|
||||||
logger.error('Unsupported URL: {}'.format(url))
|
logger.error('Unsupported URL: {}'.format(loggable_url))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Ensure our schema is always in lower case
|
# Ensure our schema is always in lower case
|
||||||
@ -480,7 +484,7 @@ def url_to_dict(url):
|
|||||||
None)
|
None)
|
||||||
|
|
||||||
if not results:
|
if not results:
|
||||||
logger.error('Unparseable URL {}'.format(url))
|
logger.error('Unparseable URL {}'.format(loggable_url))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logger.trace('URL {} unpacked as:{}{}'.format(
|
logger.trace('URL {} unpacked as:{}{}'.format(
|
||||||
@ -493,7 +497,7 @@ def url_to_dict(url):
|
|||||||
results = SCHEMA_MAP[schema].parse_url(_url)
|
results = SCHEMA_MAP[schema].parse_url(_url)
|
||||||
if not results:
|
if not results:
|
||||||
logger.error('Unparseable {} URL {}'.format(
|
logger.error('Unparseable {} URL {}'.format(
|
||||||
SCHEMA_MAP[schema].service_name, url))
|
SCHEMA_MAP[schema].service_name, loggable_url))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logger.trace('{} URL {} unpacked as:{}{}'.format(
|
logger.trace('{} URL {} unpacked as:{}{}'.format(
|
||||||
|
175
apprise/utils.py
175
apprise/utils.py
@ -216,7 +216,7 @@ def is_ipaddr(addr, ipv4=True, ipv6=True):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_hostname(hostname, ipv4=True, ipv6=True):
|
def is_hostname(hostname, ipv4=True, ipv6=True, underscore=True):
|
||||||
"""
|
"""
|
||||||
Validate hostname
|
Validate hostname
|
||||||
"""
|
"""
|
||||||
@ -244,10 +244,11 @@ def is_hostname(hostname, ipv4=True, ipv6=True):
|
|||||||
# - Hostnames can not start with the hyphen (-) character.
|
# - Hostnames can not start with the hyphen (-) character.
|
||||||
# - as a workaround for https://github.com/docker/compose/issues/229 to
|
# - as a workaround for https://github.com/docker/compose/issues/229 to
|
||||||
# being able to address services in other stacks, we also allow
|
# being able to address services in other stacks, we also allow
|
||||||
# underscores in hostnames
|
# underscores in hostnames (if flag is set accordingly)
|
||||||
# - labels can not exceed 63 characters
|
# - labels can not exceed 63 characters
|
||||||
allowed = re.compile(
|
allowed = re.compile(
|
||||||
r'^[a-z0-9][a-z0-9_-]{1,62}(?<!-)$',
|
r'^[a-z0-9][a-z0-9_-]{1,62}(?<![_-])$' if underscore else
|
||||||
|
r'^[a-z0-9][a-z0-9-]{1,62}(?<!-)$',
|
||||||
re.IGNORECASE,
|
re.IGNORECASE,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1016,6 +1017,174 @@ def validate_regex(value, regex=r'[^\s]+', flags=re.I, strip=True, fmt=None):
|
|||||||
return value.strip() if strip else value
|
return value.strip() if strip else value
|
||||||
|
|
||||||
|
|
||||||
|
def cwe312_word(word, force=False, advanced=True, threshold=5):
|
||||||
|
"""
|
||||||
|
This function was written to help mask secure/private information that may
|
||||||
|
or may not be found within Apprise. The idea is to provide a presentable
|
||||||
|
word response that the user who prepared it would understand, yet not
|
||||||
|
reveal any private information for any potential intruder
|
||||||
|
|
||||||
|
For more detail see CWE-312 @
|
||||||
|
https://cwe.mitre.org/data/definitions/312.html
|
||||||
|
|
||||||
|
The `force` is an optional argument used to keep the string formatting
|
||||||
|
consistent and in one place. If set, the content passed in is presumed
|
||||||
|
to be containing secret information and will be updated accordingly.
|
||||||
|
|
||||||
|
If advanced is set to `True` then content is additionally checked for
|
||||||
|
upper/lower/ascii/numerical variances. If an obscurity threshold is
|
||||||
|
reached, then content is considered secret
|
||||||
|
"""
|
||||||
|
|
||||||
|
class Variance(object):
|
||||||
|
"""
|
||||||
|
A Simple List of Possible Character Variances
|
||||||
|
"""
|
||||||
|
# An Upper Case Character (ABCDEF... etc)
|
||||||
|
ALPHA_UPPER = '+'
|
||||||
|
# An Lower Case Character (abcdef... etc)
|
||||||
|
ALPHA_LOWER = '-'
|
||||||
|
# A Special Character ($%^;... etc)
|
||||||
|
SPECIAL = 's'
|
||||||
|
# A Numerical Character (1234... etc)
|
||||||
|
NUMERIC = 'n'
|
||||||
|
|
||||||
|
if not (isinstance(word, six.string_types) and word.strip()):
|
||||||
|
# not a password if it's not something we even support
|
||||||
|
return word
|
||||||
|
|
||||||
|
# Formatting
|
||||||
|
word = word.strip()
|
||||||
|
if force:
|
||||||
|
# We're forcing the representation to be a secret
|
||||||
|
# We do this for consistency
|
||||||
|
return '{}...{}'.format(word[0:1], word[-1:])
|
||||||
|
|
||||||
|
elif len(word) > 1 and \
|
||||||
|
not is_hostname(word, ipv4=True, ipv6=True, underscore=False):
|
||||||
|
# Verify if it is a hostname or not
|
||||||
|
return '{}...{}'.format(word[0:1], word[-1:])
|
||||||
|
|
||||||
|
elif len(word) >= 16:
|
||||||
|
# an IP will be 15 characters so we don't want to use a smaller
|
||||||
|
# value then 16 (e.g 101.102.103.104)
|
||||||
|
# we can assume very long words are passwords otherwise
|
||||||
|
return '{}...{}'.format(word[0:1], word[-1:])
|
||||||
|
|
||||||
|
if advanced:
|
||||||
|
#
|
||||||
|
# Mark word a secret based on it's obscurity
|
||||||
|
#
|
||||||
|
|
||||||
|
# Our variances will increase depending on these variables:
|
||||||
|
last_variance = None
|
||||||
|
obscurity = 0
|
||||||
|
|
||||||
|
for c in word:
|
||||||
|
# Detect our variance
|
||||||
|
if c.isdigit():
|
||||||
|
variance = Variance.NUMERIC
|
||||||
|
elif c.isalpha() and c.isupper():
|
||||||
|
variance = Variance.ALPHA_UPPER
|
||||||
|
elif c.isalpha() and c.islower():
|
||||||
|
variance = Variance.ALPHA_LOWER
|
||||||
|
else:
|
||||||
|
variance = Variance.SPECIAL
|
||||||
|
|
||||||
|
if last_variance != variance or variance == Variance.SPECIAL:
|
||||||
|
obscurity += 1
|
||||||
|
|
||||||
|
if obscurity >= threshold:
|
||||||
|
return '{}...{}'.format(word[0:1], word[-1:])
|
||||||
|
|
||||||
|
last_variance = variance
|
||||||
|
|
||||||
|
# Otherwise we're good; return our word
|
||||||
|
return word
|
||||||
|
|
||||||
|
|
||||||
|
def cwe312_url(url):
|
||||||
|
"""
|
||||||
|
This function was written to help mask secure/private information that may
|
||||||
|
or may not be found on an Apprise URL. The idea is to not disrupt the
|
||||||
|
structure of the previous URL too much, yet still protect the users
|
||||||
|
private information from being logged directly to screen.
|
||||||
|
|
||||||
|
For more detail see CWE-312 @
|
||||||
|
https://cwe.mitre.org/data/definitions/312.html
|
||||||
|
|
||||||
|
For example, consider the URL: http://user:password@localhost/
|
||||||
|
|
||||||
|
When passed into this function, the return value would be:
|
||||||
|
http://user:****@localhost/
|
||||||
|
|
||||||
|
Since apprise allows you to put private information everywhere in it's
|
||||||
|
custom URLs, it uses this function to manipulate the content before
|
||||||
|
returning to any kind of logger.
|
||||||
|
|
||||||
|
The idea is that the URL can still be interpreted by the person who
|
||||||
|
constructed them, but not to an intruder.
|
||||||
|
"""
|
||||||
|
# Parse our URL
|
||||||
|
results = parse_url(url)
|
||||||
|
if not results:
|
||||||
|
# Nothing was returned (invalid data was fed in); return our
|
||||||
|
# information as it was fed to us (without changing it)
|
||||||
|
return url
|
||||||
|
|
||||||
|
# Update our URL with values
|
||||||
|
results['password'] = cwe312_word(results['password'], force=True)
|
||||||
|
if not results['schema'].startswith('http'):
|
||||||
|
results['user'] = cwe312_word(results['user'])
|
||||||
|
results['host'] = cwe312_word(results['host'])
|
||||||
|
|
||||||
|
else:
|
||||||
|
results['host'] = cwe312_word(results['host'], advanced=False)
|
||||||
|
results['user'] = cwe312_word(results['user'], advanced=False)
|
||||||
|
|
||||||
|
# Apply our full path scan in all cases
|
||||||
|
results['fullpath'] = '/' + \
|
||||||
|
'/'.join([cwe312_word(x)
|
||||||
|
for x in re.split(
|
||||||
|
r'[\\/]+',
|
||||||
|
results['fullpath'].lstrip('/'))]) \
|
||||||
|
if results['fullpath'] else ''
|
||||||
|
|
||||||
|
#
|
||||||
|
# Now re-assemble our URL for display purposes
|
||||||
|
#
|
||||||
|
|
||||||
|
# Determine Authentication
|
||||||
|
auth = ''
|
||||||
|
if results['user'] and results['password']:
|
||||||
|
auth = '{user}:{password}@'.format(
|
||||||
|
user=results['user'],
|
||||||
|
password=results['password'],
|
||||||
|
)
|
||||||
|
elif results['user']:
|
||||||
|
auth = '{user}@'.format(
|
||||||
|
user=results['user'],
|
||||||
|
)
|
||||||
|
|
||||||
|
params = ''
|
||||||
|
if results['qsd']:
|
||||||
|
params = '?{}'.format(
|
||||||
|
"&".join(["{}={}".format(k, cwe312_word(v, force=(
|
||||||
|
k in ('password', 'secret', 'pass', 'token', 'key',
|
||||||
|
'id', 'apikey', 'to'))))
|
||||||
|
for k, v in results['qsd'].items()]))
|
||||||
|
|
||||||
|
return '{schema}://{auth}{hostname}{port}{fullpath}{params}'.format(
|
||||||
|
schema=results['schema'],
|
||||||
|
auth=auth,
|
||||||
|
# never encode hostname since we're expecting it to be a valid one
|
||||||
|
hostname=results['host'],
|
||||||
|
port='' if not results['port'] else ':{}'.format(results['port']),
|
||||||
|
fullpath=results['fullpath'] if results['fullpath'] else '',
|
||||||
|
params=params,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def environ(*remove, **update):
|
def environ(*remove, **update):
|
||||||
"""
|
"""
|
||||||
|
@ -190,7 +190,7 @@ def apprise_test(do_notify):
|
|||||||
# We fail whenever we're initialized
|
# We fail whenever we're initialized
|
||||||
raise TypeError()
|
raise TypeError()
|
||||||
|
|
||||||
def url(self):
|
def url(self, **kwargs):
|
||||||
# Support URL
|
# Support URL
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@ -204,7 +204,7 @@ def apprise_test(do_notify):
|
|||||||
super(GoodNotification, self).__init__(
|
super(GoodNotification, self).__init__(
|
||||||
notify_format=NotifyFormat.HTML, **kwargs)
|
notify_format=NotifyFormat.HTML, **kwargs)
|
||||||
|
|
||||||
def url(self):
|
def url(self, **kwargs):
|
||||||
# Support URL
|
# Support URL
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@ -292,7 +292,7 @@ def apprise_test(do_notify):
|
|||||||
# Pretend everything is okay
|
# Pretend everything is okay
|
||||||
raise TypeError()
|
raise TypeError()
|
||||||
|
|
||||||
def url(self):
|
def url(self, **kwargs):
|
||||||
# Support URL
|
# Support URL
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@ -301,7 +301,7 @@ def apprise_test(do_notify):
|
|||||||
# Pretend everything is okay
|
# Pretend everything is okay
|
||||||
raise RuntimeError()
|
raise RuntimeError()
|
||||||
|
|
||||||
def url(self):
|
def url(self, **kwargs):
|
||||||
# Support URL
|
# Support URL
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@ -311,7 +311,7 @@ def apprise_test(do_notify):
|
|||||||
# Pretend everything is okay
|
# Pretend everything is okay
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def url(self):
|
def url(self, **kwargs):
|
||||||
# Support URL
|
# Support URL
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@ -346,7 +346,7 @@ def apprise_test(do_notify):
|
|||||||
# Pretend everything is okay
|
# Pretend everything is okay
|
||||||
raise TypeError()
|
raise TypeError()
|
||||||
|
|
||||||
def url(self):
|
def url(self, **kwargs):
|
||||||
# Support URL
|
# Support URL
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@ -744,7 +744,7 @@ def test_apprise_notify_formats(tmpdir):
|
|||||||
# Pretend everything is okay
|
# Pretend everything is okay
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def url(self):
|
def url(self, **kwargs):
|
||||||
# Support URL
|
# Support URL
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@ -760,7 +760,7 @@ def test_apprise_notify_formats(tmpdir):
|
|||||||
# Pretend everything is okay
|
# Pretend everything is okay
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def url(self):
|
def url(self, **kwargs):
|
||||||
# Support URL
|
# Support URL
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@ -776,7 +776,7 @@ def test_apprise_notify_formats(tmpdir):
|
|||||||
# Pretend everything is okay
|
# Pretend everything is okay
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def url(self):
|
def url(self, **kwargs):
|
||||||
# Support URL
|
# Support URL
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@ -1066,7 +1066,7 @@ def test_apprise_details():
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
def url(self):
|
def url(self, **kwargs):
|
||||||
# Support URL
|
# Support URL
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
@ -246,7 +246,7 @@ def test_apprise_multi_config_entries(tmpdir):
|
|||||||
# Pretend everything is okay
|
# Pretend everything is okay
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def url(self):
|
def url(self, **kwargs):
|
||||||
# support url()
|
# support url()
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
@ -537,7 +537,7 @@ def test_apprise_config_with_apprise_obj(tmpdir):
|
|||||||
# Pretend everything is okay
|
# Pretend everything is okay
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def url(self):
|
def url(self, **kwargs):
|
||||||
# support url()
|
# support url()
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ def test_apprise_asyncio_runtime_error():
|
|||||||
super(GoodNotification, self).__init__(
|
super(GoodNotification, self).__init__(
|
||||||
notify_format=NotifyFormat.HTML, **kwargs)
|
notify_format=NotifyFormat.HTML, **kwargs)
|
||||||
|
|
||||||
def url(self):
|
def url(self, **kwargs):
|
||||||
# Support URL
|
# Support URL
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ def test_config_http(mock_post):
|
|||||||
# Pretend everything is okay
|
# Pretend everything is okay
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def url(self):
|
def url(self, **kwargs):
|
||||||
# Support url() function
|
# Support url() function
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
@ -28,7 +28,9 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import mock
|
import mock
|
||||||
import pytest
|
import pytest
|
||||||
|
import requests
|
||||||
from apprise import Apprise
|
from apprise import Apprise
|
||||||
|
from apprise import AppriseAsset
|
||||||
from apprise import URLBase
|
from apprise import URLBase
|
||||||
from apprise.logger import LogCapture
|
from apprise.logger import LogCapture
|
||||||
|
|
||||||
@ -620,3 +622,82 @@ def test_apprise_log_file_captures_py2(tmpdir):
|
|||||||
|
|
||||||
# Disable Logging
|
# Disable Logging
|
||||||
logging.disable(logging.CRITICAL)
|
logging.disable(logging.CRITICAL)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(sys.version_info.major <= 2, reason="Requires Python 3.x+")
|
||||||
|
@mock.patch('requests.post')
|
||||||
|
def test_apprise_secure_logging(mock_post):
|
||||||
|
"""
|
||||||
|
API: Apprise() secure logging tests
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Ensure we're not running in a disabled state
|
||||||
|
logging.disable(logging.NOTSET)
|
||||||
|
|
||||||
|
logger.setLevel(logging.CRITICAL)
|
||||||
|
|
||||||
|
# Prepare Mock
|
||||||
|
mock_post.return_value = requests.Request()
|
||||||
|
mock_post.return_value.status_code = requests.codes.ok
|
||||||
|
|
||||||
|
# Default Secure Logging is set to enabled
|
||||||
|
asset = AppriseAsset()
|
||||||
|
assert asset.secure_logging is True
|
||||||
|
|
||||||
|
# Load our asset
|
||||||
|
a = Apprise(asset=asset)
|
||||||
|
|
||||||
|
with LogCapture(level=logging.DEBUG) as stream:
|
||||||
|
# add a test server
|
||||||
|
assert a.add("json://user:pass1$-3!@localhost") is True
|
||||||
|
|
||||||
|
# Our servers should carry this flag
|
||||||
|
a[0].asset.secure_logging is True
|
||||||
|
|
||||||
|
logs = re.split(r'\r*\n', stream.getvalue().rstrip())
|
||||||
|
assert len(logs) == 1
|
||||||
|
entry = re.split(r'\s-\s', logs[0])
|
||||||
|
assert len(entry) == 3
|
||||||
|
assert entry[1] == 'DEBUG'
|
||||||
|
assert entry[2].startswith(
|
||||||
|
'Loaded JSON URL: json://user:****@localhost/')
|
||||||
|
|
||||||
|
# Send notification
|
||||||
|
assert a.notify("test") is True
|
||||||
|
|
||||||
|
# Test our call count
|
||||||
|
assert mock_post.call_count == 1
|
||||||
|
|
||||||
|
# Reset
|
||||||
|
mock_post.reset_mock()
|
||||||
|
|
||||||
|
# Now we test the reverse configuration and turn off
|
||||||
|
# secure logging.
|
||||||
|
|
||||||
|
# Default Secure Logging is set to disable
|
||||||
|
asset = AppriseAsset(secure_logging=False)
|
||||||
|
assert asset.secure_logging is False
|
||||||
|
|
||||||
|
# Load our asset
|
||||||
|
a = Apprise(asset=asset)
|
||||||
|
|
||||||
|
with LogCapture(level=logging.DEBUG) as stream:
|
||||||
|
# add a test server
|
||||||
|
assert a.add("json://user:pass1$-3!@localhost") is True
|
||||||
|
|
||||||
|
# Our servers should carry this flag
|
||||||
|
a[0].asset.secure_logging is False
|
||||||
|
|
||||||
|
logs = re.split(r'\r*\n', stream.getvalue().rstrip())
|
||||||
|
assert len(logs) == 1
|
||||||
|
entry = re.split(r'\s-\s', logs[0])
|
||||||
|
assert len(entry) == 3
|
||||||
|
assert entry[1] == 'DEBUG'
|
||||||
|
|
||||||
|
# Note that our password is no longer escaped (it is however
|
||||||
|
# url encoded)
|
||||||
|
assert entry[2].startswith(
|
||||||
|
'Loaded JSON URL: json://user:pass1%24-3%21@localhost/')
|
||||||
|
|
||||||
|
# Disable Logging
|
||||||
|
logging.disable(logging.CRITICAL)
|
||||||
|
@ -554,6 +554,13 @@ def test_is_hostname():
|
|||||||
assert utils.is_hostname('valid-underscores_in_host.ca') == \
|
assert utils.is_hostname('valid-underscores_in_host.ca') == \
|
||||||
'valid-underscores_in_host.ca'
|
'valid-underscores_in_host.ca'
|
||||||
|
|
||||||
|
# Underscores are supported by default
|
||||||
|
assert utils.is_hostname('valid_dashes_in_host.ca') == \
|
||||||
|
'valid_dashes_in_host.ca'
|
||||||
|
# However they are not if specified otherwise:
|
||||||
|
assert utils.is_hostname(
|
||||||
|
'valid_dashes_in_host.ca', underscore=False) is False
|
||||||
|
|
||||||
# Invalid Hostnames
|
# Invalid Hostnames
|
||||||
assert utils.is_hostname('-hostname.that.starts.with.a.dash') is False
|
assert utils.is_hostname('-hostname.that.starts.with.a.dash') is False
|
||||||
assert utils.is_hostname('invalid-characters_#^.ca') is False
|
assert utils.is_hostname('invalid-characters_#^.ca') is False
|
||||||
@ -1493,3 +1500,65 @@ def test_apply_templating():
|
|||||||
template, app_mode=utils.TemplateType.JSON,
|
template, app_mode=utils.TemplateType.JSON,
|
||||||
**{'value': '"quotes are escaped"'})
|
**{'value': '"quotes are escaped"'})
|
||||||
assert result == '{value: "\\"quotes are escaped\\""}'
|
assert result == '{value: "\\"quotes are escaped\\""}'
|
||||||
|
|
||||||
|
|
||||||
|
def test_cwe312_word():
|
||||||
|
"""utils: cwe312_word() testing
|
||||||
|
"""
|
||||||
|
assert utils.cwe312_word(None) is None
|
||||||
|
assert utils.cwe312_word(42) == 42
|
||||||
|
assert utils.cwe312_word('') == ''
|
||||||
|
assert utils.cwe312_word(' ') == ' '
|
||||||
|
assert utils.cwe312_word('!') == '!'
|
||||||
|
|
||||||
|
assert utils.cwe312_word('a') == 'a'
|
||||||
|
assert utils.cwe312_word('ab') == 'ab'
|
||||||
|
assert utils.cwe312_word('abc') == 'abc'
|
||||||
|
assert utils.cwe312_word('abcd') == 'abcd'
|
||||||
|
assert utils.cwe312_word('abcd', force=True) == 'a...d'
|
||||||
|
|
||||||
|
assert utils.cwe312_word('abc--d') == 'abc--d'
|
||||||
|
assert utils.cwe312_word('a-domain.ca') == 'a...a'
|
||||||
|
|
||||||
|
# Variances to still catch domain
|
||||||
|
assert utils.cwe312_word('a-domain.ca', advanced=False) == 'a-domain.ca'
|
||||||
|
assert utils.cwe312_word('a-domain.ca', threshold=6) == 'a-domain.ca'
|
||||||
|
|
||||||
|
|
||||||
|
def test_cwe312_url():
|
||||||
|
"""utils: cwe312_url() testing
|
||||||
|
"""
|
||||||
|
assert utils.cwe312_url(None) is None
|
||||||
|
assert utils.cwe312_url(42) == 42
|
||||||
|
assert utils.cwe312_url('http://') == 'http://'
|
||||||
|
assert utils.cwe312_url('discord://') == 'discord://'
|
||||||
|
assert utils.cwe312_url('path') == 'http://path'
|
||||||
|
assert utils.cwe312_url('path/') == 'http://path/'
|
||||||
|
|
||||||
|
# Now test http:// private data
|
||||||
|
assert utils.cwe312_url(
|
||||||
|
'http://user:pass123@localhost') == 'http://user:p...3@localhost'
|
||||||
|
assert utils.cwe312_url(
|
||||||
|
'http://user@localhost') == 'http://user@localhost'
|
||||||
|
assert utils.cwe312_url(
|
||||||
|
'http://user@localhost?password=abc123') == \
|
||||||
|
'http://user@localhost?password=a...3'
|
||||||
|
assert utils.cwe312_url(
|
||||||
|
'http://user@localhost?secret=secret-.12345') == \
|
||||||
|
'http://user@localhost?secret=s...5'
|
||||||
|
|
||||||
|
# Now test other:// private data
|
||||||
|
assert utils.cwe312_url(
|
||||||
|
'gitter://b5637831f563aa846bb5b2c27d8fe8f633b8f026/apprise') == \
|
||||||
|
'gitter://b...6/apprise'
|
||||||
|
assert utils.cwe312_url(
|
||||||
|
'gitter://b5637831f563aa846bb5b2c27d8fe8f633b8f026'
|
||||||
|
'/apprise/?pass=abc123') == \
|
||||||
|
'gitter://b...6/apprise?pass=a...3'
|
||||||
|
|
||||||
|
assert utils.cwe312_url(
|
||||||
|
'slack://mybot@xoxb-43598234231-3248932482278-BZK5Wj15B9mPh1RkShJoCZ44'
|
||||||
|
'/lead2gold@gmail.com') == 'slack://mybot@x...4/l...m'
|
||||||
|
assert utils.cwe312_url(
|
||||||
|
'slack://test@B4QP3WWB4/J3QWT41JM/XIl2ffpqXkzkwMXrJdevi7W3/'
|
||||||
|
'#random') == 'slack://test@B...4/J...M/X...3'
|
||||||
|
Loading…
Reference in New Issue
Block a user