1
0
mirror of https://github.com/checktheroads/hyperglass synced 2024-05-11 05:55:08 +00:00

improve input validation & handling

This commit is contained in:
checktheroads
2019-09-09 12:18:26 -07:00
parent 4485356447
commit 1d3f934a03
3 changed files with 186 additions and 66 deletions

View File

@@ -107,14 +107,18 @@ class Routers(BaseSettings):
"""
routers = {}
hostnames = []
vrfs = set()
for (devname, params) in input_params.items():
dev = clean_name(devname)
router_params = Router(**params)
setattr(Routers, dev, router_params)
routers.update({dev: router_params.dict()})
hostnames.append(dev)
for vrf in router_params.dict()["vrfs"]:
vrfs.add(vrf)
Routers.routers = routers
Routers.hostnames = hostnames
Routers.vrfs = list(vrfs)
return Routers()
class Config:
@@ -365,6 +369,10 @@ class Messages(BaseSettings):
"{device_name} requires IPv6 BGP lookups to be in CIDR notation."
)
invalid_input: str = "{target} is not a valid {query_type} target."
invalid_target: str = "{query_target} is invalid."
invalid_location: str = "{query_location} must be a list/array."
invalid_type: str = "{query_type} is not a supported {name}"
invalid_query_vrf: str = "{query_vrf} is not defined"
general: str = "Something went wrong."
directed_cidr: str = "{query_type} queries can not be in CIDR format."
request_timeout: str = "Request timed out."

View File

@@ -17,6 +17,8 @@ class Supported:
Netmiko. List updated 07/2019.
"""
query_parameters = ("query_location", "query_type", "query_target", "query_vrf")
query_types = ("bgp_route", "bgp_community", "bgp_aspath", "ping", "traceroute")
rest = ("frr", "bird")

View File

@@ -223,6 +223,170 @@ async def test_route(request):
return response.html(html, status=500)
async def validate_input(query_data): # noqa: C901
"""
Deletes any globally unsupported query parameters.
Performs validation functions per input type:
- query_target:
- Verifies input is not empty
- Verifies input is a string
- query_location:
- Verfies input is not empty
- Verifies input is a list
- Verifies locations in list are defined
- query_type:
- Verifies input is not empty
- Verifies input is a string
- Verifies query type is enabled and supported
- query_vrf: (if feature enabled)
- Verfies input is a list
- Verifies VRFs in list are defined
"""
# Delete any globally unsupported parameters
for (param, value) in query_data:
if param not in Supported.query_parameters:
query_data.pop(param, None)
# Unpack query data
query_location = query_data.get("query_location", [])
query_type = query_data.get("query_type", "")
query_target = query_data.get("query_target", "")
query_vrf = query_data.get("query_vrf", [])
# Verify that query_target is not empty
if not query_target:
logger.debug("No input specified")
raise InvalidUsage(
{
"message": params.messages.no_input.format(
query_type=params.branding.text.query_target
),
"alert": "warning",
"keywords": [params.branding.text.query_target],
}
)
# Verify that query_target is a string
if not isinstance(query_target, str):
logger.debug("Target is not a string")
raise InvalidUsage(
{
"message": params.messages.invalid_target.format(
query_target=query_target
),
"alert": "warning",
"keywords": [params.branding.text.query_target, query_target],
}
)
# Verify that query_location is not empty
if not query_location:
logger.debug("No selection specified")
raise InvalidUsage(
{
"message": params.messages.no_input.format(
query_type=params.branding.text.query_location
),
"alert": "warning",
"keywords": [params.branding.text.query_location],
}
)
# Verify that query_location is a list
if not isinstance(query_location, list):
logger.debug("Query Location is not a list/array")
raise InvalidUsage(
{
"message": params.messages.invalid_location.format(
query_location=params.branding.text.query_location
),
"alert": "warning",
"keywords": [params.branding.text.query_location, query_location],
}
)
# Verify that locations in query_location are actually defined
if not all(loc in query_location for loc in devices.hostnames):
raise InvalidUsage(
{
"message": params.messages.invalid_location.format(
query_location=params.branding.text.query_location
),
"alert": "warning",
"keywords": [params.branding.text.query_location, query_location],
}
)
# Verify that query_type is not empty
if not query_type:
logger.debug("No query specified")
raise InvalidUsage(
{
"message": params.messages.no_input.format(
query_type=params.branding.text.query_type
),
"alert": "warning",
"keywords": [params.branding.text.query_location],
}
)
if not isinstance(query_type, str):
logger.debug("Query Type is not a string")
raise InvalidUsage(
{
"message": params.messages.invalid_location.format(
query_location=params.branding.text.query_location
),
"alert": "warning",
"keywords": [params.branding.text.query_location, query_location],
}
)
# Verify that query_type is actually supported
query_is_supported = Supported.is_supported_query(query_type)
if not query_is_supported:
logger.debug("Query not supported")
raise InvalidUsage(
{
"message": params.messages.invalid_query_type.format(
query_type=query_type, name=params.branding.text.query_type
),
"alert": "warning",
"keywords": [params.branding.text.query_location, query_type],
}
)
elif query_is_supported:
query_is_enabled = operator.attrgetter(f"{query_type}.enable")(params.features)
if not query_is_enabled:
raise InvalidUsage(
{
"message": params.messages.invalid_query_type.format(
query_type=query_type, name=params.branding.text.query_type
),
"alert": "warning",
"keywords": [params.branding.text.query_location, query_type],
}
)
if params.features.vrf.enable:
# Verify that query_vrf is a list
if query_vrf and not isinstance(query_vrf, list):
raise InvalidUsage(
{
"message": params.messages.invalid_query_vrf.format(
query_vrf=query_vrf, name=params.branding.text.query_vrf
),
"alert": "warning",
"keywords": [params.branding.text.query_vrf, query_vrf],
}
)
# Verify that vrfs in query_vrf are defined
if query_vrf and not all(vrf in query_vrf for vrf in devices.vrfs):
raise InvalidUsage(
{
"message": params.messages.invalid_query_vrf.format(
query_vrf=query_vrf, name=params.branding.text.query_vrf
),
"alert": "warning",
"keywords": [params.branding.text.query_vrf, query_vrf],
}
)
return query_data
@app.route("/query", methods=["POST"])
@limiter.limit(
rate_limit_query,
@@ -239,67 +403,11 @@ async def hyperglass_main(request):
filtering/lookups.
"""
# Get JSON data from Ajax POST
lg_data = request.json
logger.debug(f"Unvalidated input: {lg_data}")
raw_query_data = request.json
logger.debug(f"Unvalidated input: {raw_query_data}")
query_location = lg_data.get("query_location")
query_type = lg_data.get("query_type")
query_target = lg_data.get("query_target")
query_vrf = lg_data.get("query_vrf", None)
# Return error if no target is specified
if not query_target:
logger.debug("No input specified")
raise InvalidUsage(
{
"message": params.messages.no_input.format(
query_type=params.branding.text.query_target
),
"alert": "warning",
"keywords": [params.branding.text.query_target],
}
)
# Return error if no location is selected
if query_location not in devices.hostnames:
logger.debug("No selection specified")
raise InvalidUsage(
{
"message": params.messages.no_input.format(
query_type=params.branding.text.query_location
),
"alert": "warning",
"keywords": [params.branding.text.query_location],
}
)
# Return error if no query type is selected
if not Supported.is_supported_query(query_type):
logger.debug("No query specified")
raise InvalidUsage(
{
"message": params.messages.no_input.format(
query_type=params.branding.text.query_type
),
"alert": "warning",
"keywords": [params.branding.text.query_location],
}
)
device_selector = getattr(devices, query_location)
device_vrfs = device_selector.vrfs
device_display_name = device_selector.display_name
if query_vrf and query_vrf not in device_vrfs:
logger.debug(f"VRF {query_vrf} not associated with {query_location}")
raise InvalidUsage(
{
"message": params.messages.vrf_not_associated.format(
vrf=query_vrf, device_name=device_display_name
),
"alert": "warning",
"keywords": [query_vrf, device_display_name],
}
)
# Perform basic input validation
query_data = validate_input(raw_query_data)
# Get client IP address for Prometheus logging & rate limiting
client_addr = get_remote_address(request)
@@ -307,10 +415,10 @@ async def hyperglass_main(request):
# Increment Prometheus counter
count_data.labels(
client_addr,
lg_data.get("query_type"),
lg_data.get("query_location"),
lg_data.get("query_target"),
lg_data.get("query_vrf", None),
query_data.get("query_type"),
query_data.get("query_location"),
query_data.get("query_target"),
query_data.get("query_vrf"),
).inc()
logger.debug(f"Client Address: {client_addr}")
@@ -318,7 +426,7 @@ async def hyperglass_main(request):
# Stringify the form response containing serialized JSON for the
# request, use as key for k/v cache store so each command output
# value is unique
cache_key = str(lg_data)
cache_key = str(query_data)
# Define cache entry expiry time
cache_timeout = params.features.cache.timeout
@@ -332,11 +440,13 @@ async def hyperglass_main(request):
try:
starttime = time.time()
cache_value = await Execute(lg_data).response()
cache_value = await Execute(query_data).response()
endtime = time.time()
elapsedtime = round(endtime - starttime, 4)
logger.debug(f"Query {cache_key} took {elapsedtime} seconds to run.")
except (InputInvalid, InputNotAllowed) as frontend_error:
raise InvalidUsage(frontend_error.__dict__())
except (AuthError, RestError, ScrapeError, DeviceTimeout) as backend_error: