Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[uss_qualifier/scenarios/netrid/misbehavior] Add checks for SP too large area search (NET0250) #873

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions monitoring/monitorlib/geo.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,15 @@ def make_latlng_rect(area) -> s2sphere.LatLngRect:
)


def rect_str(rect: s2sphere.LatLngRect) -> str:
return "({}, {})-({}, {})".format(
rect.lo().lat().degrees,
rect.lo().lng().degrees,
rect.hi().lat().degrees,
rect.hi().lng().degrees,
)


def shift_rect_lng(rect: s2sphere.LatLngRect, shift: float) -> s2sphere.LatLngRect:
"""Shift a rect's longitude by the given amount of degrees"""
return s2sphere.LatLngRect(
Expand Down
239 changes: 167 additions & 72 deletions monitoring/uss_qualifier/scenarios/astm/netrid/common/misbehavior.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
from typing import List, Set
from typing import List, Set, Callable

import s2sphere
from requests.exceptions import RequestException
from s2sphere import LatLngRect
from s2sphere import LatLngRect, LatLng

from monitoring.monitorlib import auth
from monitoring.monitorlib import auth, geo
from monitoring.monitorlib.errors import stacktrace_string
from monitoring.monitorlib.fetch import rid
from monitoring.monitorlib.infrastructure import UTMClientSession
Expand All @@ -20,6 +19,9 @@
injection,
display_data_evaluator,
)
from monitoring.uss_qualifier.scenarios.astm.netrid.display_data_evaluator import (
TelemetryMapping,
)
from monitoring.uss_qualifier.scenarios.astm.netrid.injected_flight_collection import (
InjectedFlightCollection,
)
Expand Down Expand Up @@ -72,15 +74,34 @@ def _rid_version(self) -> RIDVersion:

def run(self, context: ExecutionContext):
self.begin_test_scenario(context)
self.begin_test_case("Unauthenticated requests")
self.begin_test_case("Invalid requests")

self.begin_test_step("Injection")
self._inject_flights()
self.end_test_step()

self.begin_test_step("Invalid search area")

self._poll_during_flights(
[
self._rid_version.max_diagonal_km * 1000
- 100, # valid diagonal required for sps urls discovery
],
self._evaluate_and_test_too_large_area_requests,
)

self.end_test_step()

self.begin_test_step("Unauthenticated requests")

self._poll_unauthenticated_during_flights()
self._poll_during_flights(
[
self._rid_version.max_diagonal_km * 1000 + 500, # too large
self._rid_version.max_diagonal_km * 1000 - 100, # clustered
self._rid_version.max_details_diagonal_km * 1000 - 100, # details
],
self._evaluate_and_test_unauthenticated_requests,
)

self.end_test_step()

Expand All @@ -92,7 +113,18 @@ def _inject_flights(self):
self, self._flights_data, self._service_providers
)

def _poll_unauthenticated_during_flights(self):
def _poll_during_flights(
self,
diagonals_m: List[float],
evaluation_func: Callable[[LatLngRect], Set[str]],
):
mickmis marked this conversation as resolved.
Show resolved Hide resolved
"""
Poll until every injected flights have been observed.

:param diagonals_m: List of diagonals in meters used by the virtual observer to fetch flights.
:param evaluation_func: This method is called on each polling tick with the area to observe. It is responsible
to fetch flights and to return the list of observed injected ids.
"""
config = self._evaluation_configuration.configuration
virtual_observer = VirtualObserver(
injected_flights=InjectedFlightCollection(self._injected_flights),
Expand All @@ -106,38 +138,23 @@ def _poll_unauthenticated_during_flights(self):
inj_flight.flight.injection_id for inj_flight in self._injected_flights
)

def poll_fct(rect: LatLngRect) -> bool:
def poll_func(rect: LatLngRect) -> bool:
nonlocal remaining_injection_ids

tested_inj_ids = self._evaluate_and_test_authentication(rect)
tested_inj_ids = evaluation_func(rect)
remaining_injection_ids -= tested_inj_ids

# interrupt polling if there are no more injection IDs to cover
return len(remaining_injection_ids) == 0

virtual_observer.start_polling(
config.min_polling_interval.timedelta,
[
self._rid_version.max_diagonal_km * 1000 + 500, # too large
self._rid_version.max_diagonal_km * 1000 - 100, # clustered
self._rid_version.max_details_diagonal_km * 1000 - 100, # details
],
poll_fct,
diagonals_m,
poll_func,
)

def _evaluate_and_test_authentication(
self,
rect: s2sphere.LatLngRect,
) -> Set[str]:
"""Queries all flights in the expected way, then repeats the queries to SPs without credentials.

returns true once queries to SPS have been made without credentials. False otherwise, such as when
no flights were yet returned by the authenticated queries.

:returns: set of injection IDs that were encountered and tested
"""

# We grab all flights from the SP's (which we know how to reach by first querying the DSS).
def _fetch_flights_from_dss(self, rect: LatLngRect):
# We grab all flights from the SPs (which we know how to reach by first querying the DSS).
# This is authenticated and is expected to succeed
sp_observation = rid.all_flights(
rect,
Expand All @@ -153,74 +170,152 @@ def _evaluate_and_test_authentication(
self._injected_flights, list(sp_observation.uss_flight_queries.values())
)
)
for q in sp_observation.queries:
self.record_query(q)
self.record_queries(sp_observation.queries)

return mapping_by_injection_id

def _evaluate_and_test_too_large_area_requests(
self,
rect: LatLngRect,
) -> Set[str]:
"""Queries all flights from the DSS to discover flights urls and query them using a larger area than allowed.

:returns: set of injection IDs that were encountered and tested
"""

mapping_by_injection_id = self._fetch_flights_from_dss(rect)
for injection_id, mapping in mapping_by_injection_id.items():
participant_id = mapping.injected_flight.uss_participant_id
flights_url = mapping.observed_flight.query.flights_url
unauthenticated_session = UTMClientSession(
flights_url, auth.NoAuth(aud_override="")
)
self._evaluate_too_large_area(rect, injection_id, mapping)

return set(mapping_by_injection_id.keys())

def _evaluate_too_large_area(
self, rect: LatLngRect, injection_id: str, mapping: TelemetryMapping
):
participant_id = mapping.injected_flight.uss_participant_id
flights_url = mapping.observed_flight.query.flights_url
session = self._dss.client

scale = LatLng(0.01, 0.001)
invalid_rect = rect.expanded(scale)
diagonal_km = geo.get_latlngrect_diagonal_km(invalid_rect)
if diagonal_km > self._rid_version.max_diagonal_km:

self.record_note(
f"{participant_id}/{injection_id}/missing_credentials_queries",
f"Will attempt querying with missing credentials at flights URL {flights_url} for a flights list and {len(mapping.observed_flight.query.flights)} flight details.",
f"{participant_id}/{injection_id}/area_too_large_query",
f"Will attempt to search an area too large at {flights_url} - (diagonal: {diagonal_km} km)",
)

with self.check("Missing credentials", [participant_id]) as check:
with self.check("Area too large", [participant_id]) as check:

# check uss flights query
uss_flights_query = rid.uss_flights(
flights_url,
rect,
invalid_rect,
True,
self._rid_version,
unauthenticated_session,
session,
participant_id,
)
self.record_query(uss_flights_query.query)

if uss_flights_query.success:
if uss_flights_query.status_code not in (400, 413):
check.record_failed(
"Unauthenticated request for flights to USS was fulfilled",
severity=Severity.Medium,
details=f"Queried flights on {flights_url} for USS {participant_id} with no credentials, expected a failure but got a success reply.",
summary="Did not receive expected error code for too-large area request",
severity=Severity.High,
details=f"{participant_id} was queried for flights in {geo.rect_str(rect)} with a diagonal of {diagonal_km} which is larger than the maximum allowed diagonal of {self._rid_version.max_diagonal_km}. The expected error code is 413, but instead code {uss_flights_query.status_code} was received.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be specifying the severity through the emoji in the .md, this parameter is meant to be deprecated.

)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: expected error may also be 400

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would deserve a proper PR. Currently, 400 are silently accepted on various other cases. I will leave it as is for the moment and bring the topic to the Contributors meeting.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

elif uss_flights_query.status_code != 401:

if (
uss_flights_query.flights is not None
and len(uss_flights_query.flights) != 0
):
check.record_failed(
"Unauthenticated request for flights failed with wrong HTTP code",
severity=Severity.Medium,
details=f"Queried flights on {flights_url} for USS {participant_id} with no credentials, expected an HTTP 401 but got an HTTP {uss_flights_query.status_code}.",
summary="Received Remote ID data while an empty response was expected because the requested area was too large",
severity=Severity.High,
details=f"{participant_id} was queried for flights in {geo.rect_str(rect)} with a diagonal of {diagonal_km} which is larger than the maximum allowed diagonal of {self._rid_version.max_diagonal_km}. The Remote ID data shall be empty, instead, the following payload was received: {uss_flights_query.query.response.content}",
)

# check flight details query
for flight in mapping.observed_flight.query.flights:
uss_flight_details_query = rid.flight_details(
flights_url,
flight.id,
False,
self._rid_version,
unauthenticated_session,
participant_id,
)
self.record_query(uss_flight_details_query.query)

if uss_flight_details_query.success:
check.record_failed(
"Unauthenticated request for flight details to USS was fulfilled",
severity=Severity.Medium,
details=f"Queried flight details on {flights_url} for USS {participant_id} for flight {flight.id} with no credentials, expected a failure but got a success reply.",
)
elif uss_flight_details_query.status_code != 401:
check.record_failed(
"Unauthenticated request for flight details failed with wrong HTTP code",
severity=Severity.Medium,
details=f"Queried flight details on {flights_url} for USS {participant_id} for flight {flight.id} with no credentials, expected an HTTP 401 but got an HTTP {uss_flight_details_query.status_code}.",
)
def _evaluate_and_test_unauthenticated_requests(
self,
rect: LatLngRect,
) -> Set[str]:
"""Queries all flights in the expected way, then repeats the queries to SPs without credentials.

:returns: set of injection IDs that were encountered and tested
"""

mapping_by_injection_id = self._fetch_flights_from_dss(rect)
for injection_id, mapping in mapping_by_injection_id.items():
self._evaluate_unauthenticated(rect, injection_id, mapping)

return set(mapping_by_injection_id.keys())

def _evaluate_unauthenticated(
self, rect: LatLngRect, injection_id: str, mapping: TelemetryMapping
):
participant_id = mapping.injected_flight.uss_participant_id
flights_url = mapping.observed_flight.query.flights_url
unauthenticated_session = UTMClientSession(
flights_url, auth.NoAuth(aud_override="")
)

self.record_note(
f"{participant_id}/{injection_id}/missing_credentials_queries",
f"Will attempt querying with missing credentials at flights URL {flights_url} for a flights list and {len(mapping.observed_flight.query.flights)} flight details.",
)

with self.check("Missing credentials", [participant_id]) as check:

# check uss flights query
uss_flights_query = rid.uss_flights(
flights_url,
rect,
True,
self._rid_version,
unauthenticated_session,
participant_id,
)
self.record_query(uss_flights_query.query)

if uss_flights_query.success:
check.record_failed(
"Unauthenticated request for flights to USS was fulfilled",
severity=Severity.Medium,
details=f"Queried flights on {flights_url} for USS {participant_id} with no credentials, expected a failure but got a success reply.",
)
elif uss_flights_query.status_code != 401:
check.record_failed(
"Unauthenticated request for flights failed with wrong HTTP code",
severity=Severity.Medium,
details=f"Queried flights on {flights_url} for USS {participant_id} with no credentials, expected an HTTP 401 but got an HTTP {uss_flights_query.status_code}.",
)

# check flight details query
for flight in mapping.observed_flight.query.flights:
uss_flight_details_query = rid.flight_details(
flights_url,
flight.id,
False,
self._rid_version,
unauthenticated_session,
participant_id,
)
self.record_query(uss_flight_details_query.query)

if uss_flight_details_query.success:
check.record_failed(
"Unauthenticated request for flight details to USS was fulfilled",
severity=Severity.Medium,
details=f"Queried flight details on {flights_url} for USS {participant_id} for flight {flight.id} with no credentials, expected a failure but got a success reply.",
)
elif uss_flight_details_query.status_code != 401:
check.record_failed(
"Unauthenticated request for flight details failed with wrong HTTP code",
severity=Severity.Medium,
details=f"Queried flight details on {flights_url} for USS {participant_id} for flight {flight.id} with no credentials, expected an HTTP 401 but got an HTTP {uss_flight_details_query.status_code}.",
)

def cleanup(self):
self.begin_cleanup()
while self._injected_tests:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import math
from dataclasses import dataclass
from typing import List, Optional, Dict, Union, Set, Tuple, cast
from typing import List, Optional, Dict, Union, Set, Tuple

import arrow
import s2sphere
Expand Down Expand Up @@ -41,16 +41,6 @@
)
from monitoring.uss_qualifier.scenarios.scenario import TestScenario


def _rect_str(rect) -> str:
return "({}, {})-({}, {})".format(
rect.lo().lat().degrees,
rect.lo().lng().degrees,
rect.hi().lat().degrees,
rect.hi().lng().degrees,
)


VERTICAL_SPEED_PRECISION = 0.1
SPEED_PRECISION = 0.05
TIMESTAMP_ACCURACY_PRECISION = 0.05
Expand Down Expand Up @@ -324,7 +314,7 @@ def _evaluate_observation(
if observation is None:
check.record_failed(
summary="Observation failed",
details=f"When queried for an observation in {_rect_str(rect)}, {observer.participant_id} returned code {query.status_code}",
details=f"When queried for an observation in {geo.rect_str(rect)}, {observer.participant_id} returned code {query.status_code}",
severity=Severity.Medium,
query_timestamps=[query.request.timestamp],
)
Expand Down Expand Up @@ -598,7 +588,7 @@ def _evaluate_area_too_large_observation(
if query.status_code not in (400, 413):
check.record_failed(
summary="Did not receive expected error code for too-large area request",
details=f"{observer.participant_id} was queried for flights in {_rect_str(rect)} with a diagonal of {diagonal} which is larger than the maximum allowed diagonal of {self._rid_version.max_diagonal_km}. The expected error code is 413, but instead code {query.status_code} was received.",
details=f"{observer.participant_id} was queried for flights in {geo.rect_str(rect)} with a diagonal of {diagonal} which is larger than the maximum allowed diagonal of {self._rid_version.max_diagonal_km}. The expected error code is 413, but instead code {query.status_code} was received.",
severity=Severity.High,
query_timestamps=[query.request.timestamp],
)
Expand Down Expand Up @@ -1216,7 +1206,7 @@ def _evaluate_area_too_large_sp_observation(
) as check:
check.record_failed(
summary="Flight discovered using too-large area request",
details=f"{mapping.injected_flight.uss_participant_id} was queried for flights in {_rect_str(rect)} with a diagonal of {diagonal} km which is larger than the maximum allowed diagonal of {self._rid_version.max_diagonal_km} km. The expected error code is 413, but instead a valid response containing the expected flight was received.",
details=f"{mapping.injected_flight.uss_participant_id} was queried for flights in {geo.rect_str(rect)} with a diagonal of {diagonal} km which is larger than the maximum allowed diagonal of {self._rid_version.max_diagonal_km} km. The expected error code is 413, but instead a valid response containing the expected flight was received.",
severity=Severity.High,
query_timestamps=[
mapping.observed_flight.query.query.request.timestamp
Expand Down
Loading
Loading