Mini Shell
import asyncio
import json
import logging
import os
import time
from collections import namedtuple
from datetime import date, datetime, timedelta
from functools import wraps
from ipaddress import IPv4Network, IPv6Network, ip_network
from pathlib import Path
import jsonschema
from defence360agent.rpc_tools.validate import (
SchemaValidator as SchemaValidatorBase,
)
from defence360agent.rpc_tools.validate import validate
from im360.contracts.config import Webshield
from im360.internals.core import IPSetPort, libipset
from defence360agent.utils.validate import IP
logger = logging.getLogger(__name__)
TODAY = "today"
YESTERDAY = "yesterday"
PortProtoBase = namedtuple("PortProtoBase", ["port", "proto"])
PeriodBase = namedtuple("PeriodBase", ["since", "to"])
class PortProto(PortProtoBase):
def __new__(cls, port, proto):
if proto not in IPSetPort.PROTOS:
raise ValueError("Protocol {} is not supported".format(proto))
if not (IPSetPort.MIN_PORT < port < IPSetPort.MAX_PORT):
raise ValueError("Port {} is incorrect".format(port))
return super().__new__(cls, port, proto)
@classmethod
def fromstring(cls, pp_string):
try:
port, proto = pp_string.split(":")
port = int(port)
return cls(port, proto)
except ValueError as e:
raise ValueError(
"Incorrect port_proto ({}): {}".format(str(e), pp_string)
)
class Period(PeriodBase):
def __new__(cls, since, to):
try:
datetime.fromtimestamp(since), datetime.fromtimestamp(to)
except ValueError as e:
raise ValueError("Incorrect value for period: {}".format(str(e)))
return super().__new__(cls, since, to)
@classmethod
def fromstring(cls, period_string):
now = datetime.now()
seconds_since_midnight = (
now - now.replace(hour=0, minute=0, second=0, microsecond=0)
).total_seconds()
if period_string == TODAY:
since, to = time.time() - seconds_since_midnight, time.time()
elif period_string == YESTERDAY:
from_date = (
time.time()
- seconds_since_midnight
- timedelta(days=1).total_seconds()
)
to_date = time.time() - seconds_since_midnight
since, to = from_date, to_date
else:
period_names = "weeks", "days", "hours", "minutes", "seconds"
try:
val, sfx = int(period_string[:-1]), period_string[-1:]
except (ValueError, IndexError) as e:
raise ValueError(
"Invalid string from period: {} ({})".format(
period_string, str(e)
)
)
if not sfx.endswith(tuple(p_name[0] for p_name in period_names)):
# argparse will handle this exception
raise ValueError("Invalid suffix: {}".format(sfx))
sfx_expanded = next(xp for xp in period_names if sfx == xp[0])
real_args = {sfx_expanded: val}
since, to = (
(datetime.now() - timedelta(**real_args)).timestamp(),
time.time(),
)
return cls(since, to)
class SchemaValidator(SchemaValidatorBase):
MAX_IPSET_TIMEOUT = libipset.IPSET_TIMEOUT_MAX # ipset's maximum ttl
def _normalize_coerce_port_proto(self, value):
if isinstance(value, PortProto):
return value
elif isinstance(value, str):
return PortProto.fromstring(value)
raise ValueError("String or PortProto must be provided")
def _normalize_coerce_ip(self, value):
if isinstance(value, (IPv4Network, IPv6Network)):
return value
elif isinstance(value, str):
return ip_network(value)
def _normalize_coerce_ip_discard_host_bits(self, value):
if isinstance(value, (IPv4Network, IPv6Network)):
return value
elif isinstance(value, str):
return ip_network(value, strict=False)
def _normalize_coerce_period(self, value):
if isinstance(value, Period):
return value
elif isinstance(value, str):
return Period.fromstring(value)
raise ValueError("String or Period must be provided")
def _normalize_coerce_tolower(self, value):
# please, don't try to casefold() instead of lower()
# see https://tools.ietf.org/html/rfc4343
return value.lower()
def _validate_type_port_proto(self, value):
if isinstance(value, PortProto):
return True
return False
def _validate_type_period(self, value):
if isinstance(value, Period):
return True
return False
def _validate_type_ip(self, value):
return isinstance(value, (IPv4Network, IPv6Network))
def _validator_enforce64min_subnet_mask_for_ipv6(self, field, value):
if IP.is_valid_ipv6_network(value):
# 64 - min subnet mask for ipv6 addr
if IPv6Network(value).prefixlen > 64:
self._error(
field, "Supported only ipv6 /64 networks: {}".format(value)
)
def _validator_max_days(self, field, value):
max_days = timedelta.max.days
max_past = date.today() - date(1970, 1, 1)
if value > max_days or timedelta(days=value) > max_past:
self._error(
field,
"Number of days ({}) exceeds maximum value of {}. "
"Please, specify lesser amount of days".format(
value, max_past.days
),
)
def _validator_timestamp(self, field, value):
try:
datetime.fromtimestamp(value)
except ValueError as e:
self._error(
field, "Incorrect timestamp: {} ({})".format(value, str(e))
)
def _validator_expiration(self, field, value):
if not value:
return
expiration_time = value
now = time.time()
if expiration_time <= now:
self._error(
field,
"Expiration contains expired timestamp {}!".format(
time.strftime("%x %X %Z", time.gmtime(expiration_time))
),
)
max_expiration_time = now + self.MAX_IPSET_TIMEOUT
if expiration_time > max_expiration_time:
self._error(
field,
(
"Expiration time {} is too far into the future."
" It is more than {} seconds from now"
).format(expiration_time, self.MAX_IPSET_TIMEOUT),
)
def _validator_webshield_is_enabled(self, field, value):
if not Webshield.ENABLE:
self._error(
field,
"This command is not supported when webshield is disabled",
)
def validate_middleware(validator):
base = Path(os.path.dirname(__file__)) / "../.."
core_schemas = base / "defence360agent/simple_rpc/schema_responses/another"
imav_schemas = base / "imav/simple_rpc/schema_responses/another"
im360_schemas = base / "im360/simple_rpc/schema_responses/another"
def get_file_from_schema_responses_dirs(filename):
core_schema_path = core_schemas.with_name(filename)
if core_schema_path.exists():
return core_schema_path
imav_schema_path = imav_schemas.with_name(filename)
if imav_schema_path.exists():
return imav_schema_path
return im360_schemas.with_name(filename)
def get_response_schema(return_type):
# asserts that return_type does not contain '/'
schema_path = get_file_from_schema_responses_dirs(
return_type + ".json"
)
with schema_path.open("r") as f:
schema = json.load(f)
return schema
async def validate_response(hashable, result):
return_type = validator.schema.get(hashable).get("return_type", None)
if return_type is None:
return
schema = get_response_schema(return_type)
# validation should be performed after
# result gets such format (ui accepts it)
# in some cases result never gets such format
# like test_addmany_invalid_request
target = {"result": "success", "messages": [], "data": result}
try:
jsonschema.validate(target, schema)
except jsonschema.ValidationError as error:
logger.critical(
'Validating %r using schema %r failed with error "%s".',
target,
schema,
error,
exc_info=error,
)
def wrapped(f):
@wraps(f)
async def wrapper(request, *args, **kwargs):
hashable = tuple(request["command"])
request["params"] = validate(
validator, hashable, request["params"]
)
result = await f(request, *args, **kwargs)
# no cpu overhead during rpc request
# since validation is asynchronous
# (only next request may be delayed a little)
# run_until_complete waits until this task will be finished (why?)
# so test will be failed
asyncio.ensure_future(validate_response(hashable, result))
return result
return wrapper
return wrapped