Skip to content

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

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

Merged
merged 11 commits into from
Jan 22, 2025
Merged
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
173 changes: 135 additions & 38 deletions monitoring/uss_qualifier/scenarios/astm/netrid/common/misbehavior.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import List, Set
from typing import List, Set, Callable, TypedDict, Unpack, Optional

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 +20,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,21 +75,52 @@ 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,
dict(),
)
self.end_test_step()

self.begin_test_step("Unauthenticated requests")
self._poll_unauthenticated_during_flights(
auth.NoAuth(aud_override=""), "Missing credentials", "no"
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_authentication,
{
"auth": auth.NoAuth(aud_override=""),
"check_name": "Missing credentials",
"credentials_type_description": "no",
},
)
self.end_test_step()

self.begin_test_step("Incorrectly authenticated requests")
self._poll_unauthenticated_during_flights(
auth.InvalidTokenSignatureAuth(), "Invalid credentials", "invalid"
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_authentication,
{
"auth": auth.InvalidTokenSignatureAuth(),
"check_name": "Invalid credentials",
"credentials_type_description": "invalid",
},
)
self.end_test_step()

Expand All @@ -98,9 +132,21 @@ def _inject_flights(self):
self, self._flights_data, self._service_providers
)

def _poll_unauthenticated_during_flights(
self, auth: auth.AuthAdapter, check_name: str, credentials_type_description: str
def _poll_during_flights(
self,
diagonals_m: List[float],
evaluation_func: Callable[
[LatLngRect, Unpack[dict[str, auth.AuthAdapter | str]]], Set[str]
],
evaluation_kwargs: dict[str, auth.AuthAdapter | str],
):
"""
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 @@ -114,44 +160,25 @@ def _poll_unauthenticated_during_flights(
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(
auth, check_name, credentials_type_description, rect
)
tested_inj_ids = evaluation_func(rect, **evaluation_kwargs)
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,
auth: auth.AuthAdapter,
check_name: str,
credentials_type_description: str,
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) -> dict[str, TelemetryMapping]:
# 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
# TODO: Add the following requests to the documentation. Possibly split it as a test step.
sp_observation = rid.all_flights(
rect,
include_recent_positions=True,
Expand All @@ -166,8 +193,78 @@ 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():
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)
with self.check("Area too large", [participant_id]) as check:
# check uss flights query
uss_flights_query = rid.uss_flights(
flights_url,
invalid_rect,
True,
self._rid_version,
session,
participant_id,
)
self.record_query(uss_flights_query.query)

if uss_flights_query.status_code not in (400, 413):
check.record_failed(
summary="Did not receive expected error code for too-large area request",
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 400 or 413, but instead code {uss_flights_query.status_code} was received.",
)

if (
uss_flights_query.flights is not None
and len(uss_flights_query.flights) != 0
):
check.record_failed(
summary="Received Remote ID data while an empty response was expected because the requested area was too large",
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}",
)

def _evaluate_and_test_authentication(
self,
rect: s2sphere.LatLngRect,
auth: auth.AuthAdapter,
check_name: str,
credentials_type_description: str,
) -> 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
"""

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,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,8 @@ 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],
)
return
Expand Down Expand Up @@ -505,7 +496,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 400 or 413, but instead code {query.status_code} was received.",
query_timestamps=[query.request.timestamp],
)

Expand Down Expand Up @@ -1126,7 +1117,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 400 or 413, but instead a valid response containing the expected flight was received.",
query_timestamps=[
mapping.observed_flight.query.query.request.timestamp
],
Expand Down
12 changes: 10 additions & 2 deletions monitoring/uss_qualifier/scenarios/astm/netrid/v19/misbehavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Overview

In this scenario, the service provider's endpoint are accessed directly with missing or incorrect credentials. Resources that exists as well as resources that are not expected to exist are queried.
In this scenario, the service provider's endpoints are accessed directly to test missing credentials as well as invalid requests. Resources that exists as well as resources that are not expected to exist are queried.

## Resources

Expand All @@ -22,7 +22,7 @@ This [`EvaluationConfigurationResource`](../../../../resources/netrid/evaluation

A [`DSSInstanceResource`](../../../../resources/astm/f3411/dss.py) is required for providing the qualifier with the flights URL of the service providers being tested.

## Unauthenticated requests test case
## Invalid requests test case

### Injection test step

Expand All @@ -39,6 +39,14 @@ This check will fail if the flight was not successfully injected.

This particular test requires each flight to be uniquely identifiable by its 2D telemetry position; the same (lat, lng) pair may not appear in two different telemetry points, even if the two points are in different injected flights. This should generally be achieved by injecting appropriate data.

### Invalid search area test step

This step will attempt to search for flights in a rectangular area with a diagonal greater than [NetMaxDisplayAreaDiagonal] km.

#### ⚠️ Area too large check

**[astm.f3411.v19.NET0250](../../../../requirements/astm/f3411/v19.md)** requires that a NetRID Service Provider rejects a request for a very large view area with a diagonal greater than *NetMaxDisplayAreaDiagonal*. If such a large view is requested and a 400 or 413 error code is not received or the response contains Remote ID data, then this check will fail.

### Unauthenticated requests test step

In order to properly test whether the SP handles authentication correctly, this step will first attempt to do a request with the proper credentials
Expand Down
12 changes: 10 additions & 2 deletions monitoring/uss_qualifier/scenarios/astm/netrid/v22a/misbehavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Overview

In this scenario, the service provider's endpoint are accessed directly with missing or incorrect credentials. Resources that exists as well as resources that are not expected to exist are queried.
In this scenario, the service provider's endpoints are accessed directly to test missing credentials as well as invalid requests. Resources that exists as well as resources that are not expected to exist are queried.

## Resources

Expand All @@ -22,10 +22,18 @@ This [`EvaluationConfigurationResource`](../../../../resources/netrid/evaluation

A [`DSSInstanceResource`](../../../../resources/astm/f3411/dss.py) is required for providing the qualifier with the flights URL of the service providers being tested.

## Unauthenticated requests test case
## Invalid requests test case

### [Injection test step](./fragments/flight_injection.md)

### Invalid search area test step

This step will attempt to search for flights in a rectangular area with a diagonal greater than [NetMaxDisplayAreaDiagonal] km.

#### ⚠️ Area too large check

**[astm.f3411.v22a.NET0250](../../../../requirements/astm/f3411/v22a.md)** requires that a NetRID Service Provider rejects a request for a very large view area with a diagonal greater than *NetMaxDisplayAreaDiagonal*. If such a large view is requested and a 400 or 413 error code is not received or the response contains Remote ID data, then this check will fail.

### Unauthenticated requests test step

In order to properly test whether the SP handles authentication correctly, this step will first attempt to do a request with the proper credentials
Expand Down
2 changes: 1 addition & 1 deletion monitoring/uss_qualifier/suites/astm/netrid/f3411_19.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@
<tr>
<td><a href="../../../requirements/astm/f3411/v19.md">NET0250</a></td>
<td>Implemented</td>
<td><a href="../../../scenarios/astm/netrid/v19/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
<td><a href="../../../scenarios/astm/netrid/v19/misbehavior.md">ASTM NetRID SP clients misbehavior handling</a><br><a href="../../../scenarios/astm/netrid/v19/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
</tr>
<tr>
<td><a href="../../../requirements/astm/f3411/v19.md">NET0260,NearRealTime</a></td>
Expand Down
2 changes: 1 addition & 1 deletion monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@
<tr>
<td><a href="../../../requirements/astm/f3411/v22a.md">NET0250</a></td>
<td>Implemented</td>
<td><a href="../../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
<td><a href="../../../scenarios/astm/netrid/v22a/misbehavior.md">ASTM NetRID SP clients misbehavior handling</a><br><a href="../../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
</tr>
<tr>
<td><a href="../../../requirements/astm/f3411/v22a.md">NET0260,NearRealTime</a></td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@
<tr>
<td><a href="../../requirements/astm/f3411/v22a.md">NET0250</a></td>
<td>Implemented</td>
<td><a href="../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
<td><a href="../../scenarios/astm/netrid/v22a/misbehavior.md">ASTM NetRID SP clients misbehavior handling</a><br><a href="../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
</tr>
<tr>
<td><a href="../../requirements/astm/f3411/v22a.md">NET0260,NearRealTime</a></td>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@
<tr>
<td><a href="../../requirements/astm/f3411/v22a.md">NET0250</a></td>
<td>Implemented</td>
<td><a href="../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
<td><a href="../../scenarios/astm/netrid/v22a/misbehavior.md">ASTM NetRID SP clients misbehavior handling</a><br><a href="../../scenarios/astm/netrid/v22a/nominal_behavior.md">ASTM NetRID nominal behavior</a></td>
</tr>
<tr>
<td><a href="../../requirements/astm/f3411/v22a.md">NET0260,NearRealTime</a></td>
Expand Down
Loading