diff --git a/docs/docs/configuration.mdx b/docs/docs/configuration.mdx
index 1bdb014..746ace0 100644
--- a/docs/docs/configuration.mdx
+++ b/docs/docs/configuration.mdx
@@ -79,14 +79,15 @@ cors_origins: [localhost:3000, 192.0.2.1]
From the top level, the following subsections may be defined and configured:
-| Section | Description | All Options |
-| :--------- | :-------------------------------------------------- | :-----------------------------------------: |
-| `cache` | Redis server & cache timeout settings. | ➡️ |
-| `docs` | API documentation settings. | ➡️ |
-| `logging` | File, syslog, and webhook settings. | ➡️ |
-| `messages` | Customize almost all user-facing UI & API messages. | ➡️ |
-| `queries` | Enable, disable, or configure query types. | ➡️ |
-| `web` | Web UI & branding settings. | ➡️ |
+| Section | Description | All Options |
+| :----------- | :-------------------------------------------------- | :------------------------------------------------: |
+| `cache` | Redis server & cache timeout settings. | ➡️ |
+| `docs` | API documentation settings. | ➡️ |
+| `logging` | File, syslog, and webhook settings. | ➡️ |
+| `messages` | Customize almost all user-facing UI & API messages. | ➡️ |
+| `queries` | Enable, disable, or configure query types. | ➡️ |
+| `structured` | Configure structured data features. | ➡️ |
+| `web` | Web UI & branding settings. | ➡️ |
## Adding Devices
diff --git a/docs/docs/structured-data.mdx b/docs/docs/structured-data.mdx
new file mode 100644
index 0000000..5d96b97
--- /dev/null
+++ b/docs/docs/structured-data.mdx
@@ -0,0 +1,59 @@
+---
+id: structured-data
+title: Structured Data
+sidebar_label: Structured Data
+description: Configure structured data parameters
+---
+
+
+
+The `structured` subsection contains multiple subsections of its own:
+
+| Section | Description | All Options |
+| :------------ | :--------------------------------- | :---------------------------------------: |
+| `communities` | Include or exclude BGP communities | ➡️ |
+| `rpki` | Configure RPKI validation | ➡️ |
+
+## `communities`
+
+| Parameter | Type | Default | Description |
+| :-------- | :----: | :------: | :------------------------------------------------------------------------------------------------------------------------------------ |
+| `mode` | String | `'deny'` | `'deny'` denies any matching patterns and permits anything else. `'permit'` only permits matching patterns and denies everything else |
+| `items` | List | `[]` | List of regular expression patterns to match |
+
+:::note Regular Expressions with YAML
+If you're using any regex patterns with special characters, you'll probably need to wrap the patterns in single quotes, to denote a raw string. For example:
+
+```yaml
+normal_string: Normal String
+raw_string: '^Raw\sString$'
+```
+
+:::
+
+## `rpki`
+
+| Parameter | Type | Default |
+| :-------- | :----: | :--------: |
+| `mode` | String | `'router'` |
+
+### `router` mode
+
+`router` mode uses the RPKI state from the perspective of the router. This means if your network is not using Origin Validation, the RPKI state will be shown as "Not Verified". Otherwise, the corresponding RPKI state will be shown.
+
+### `external` mode
+
+`external` mode takes each prefix, the last ASN in the `AS_PATH` and requests the RPKI validation state via the [Cloudflare RPKI Portal](https://rpki.cloudflare.com/).
+
+## Example
+
+```yaml title="hyperglass.yaml"
+structured:
+ communities:
+ mode: permit
+ items:
+ - '65000:\d+'
+ - '65[1-4]00:\d+'
+ rpki:
+ mode: external
+```
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 699d0f6..aac90e2 100755
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -18,6 +18,7 @@ module.exports = {
"ui",
"api",
"messages",
+ "structured-data",
],
},
{
diff --git a/hyperglass/configuration/models/params.py b/hyperglass/configuration/models/params.py
index 96687a6..caf4f7d 100644
--- a/hyperglass/configuration/models/params.py
+++ b/hyperglass/configuration/models/params.py
@@ -23,6 +23,7 @@ from hyperglass.configuration.models.cache import Cache
from hyperglass.configuration.models.logging import Logging
from hyperglass.configuration.models.queries import Queries
from hyperglass.configuration.models.messages import Messages
+from hyperglass.configuration.models.structured import Structured
class Params(HyperglassModel):
@@ -113,6 +114,7 @@ class Params(HyperglassModel):
logging: Logging = Logging()
messages: Messages = Messages()
queries: Queries = Queries()
+ structured: Structured = Structured()
web: Web = Web()
class Config:
diff --git a/hyperglass/configuration/models/structured.py b/hyperglass/configuration/models/structured.py
new file mode 100644
index 0000000..a2fa1a6
--- /dev/null
+++ b/hyperglass/configuration/models/structured.py
@@ -0,0 +1,31 @@
+"""Structured data configuration variables."""
+
+# Standard Library
+from typing import List
+
+# Third Party
+from pydantic import StrictInt, StrictStr, constr
+
+# Project
+from hyperglass.models import HyperglassModel
+
+
+class StructuredCommunities(HyperglassModel):
+ """Control structured data response for BGP communties."""
+
+ mode: constr(regex=r"(permit|deny)") = "deny"
+ items: List[StrictStr] = []
+
+
+class StructuredRpki(HyperglassModel):
+ """Control structured data response for RPKI state."""
+
+ mode: constr(regex=r"(router|external)") = "router"
+ max_age: StrictInt = 24
+
+
+class Structured(HyperglassModel):
+ """Control structured data responses."""
+
+ communities: StructuredCommunities = StructuredCommunities()
+ rpki: StructuredRpki = StructuredRpki()
diff --git a/hyperglass/external/rpki.py b/hyperglass/external/rpki.py
new file mode 100644
index 0000000..5e035b4
--- /dev/null
+++ b/hyperglass/external/rpki.py
@@ -0,0 +1,35 @@
+"""Validate RPKI state via Cloudflare GraphQL API."""
+
+from hyperglass.log import log
+from hyperglass.external._base import BaseExternal
+
+RPKI_STATE_MAP = {"Invalid": 0, "Valid": 1, "NotFound": 2, "DEFAULT": 3}
+RPKI_NAME_MAP = {v: k for k, v in RPKI_STATE_MAP.items()}
+
+
+def rpki_state(prefix, asn):
+ """Get RPKI state and map to expected integer."""
+ log.debug("Validating RPKI State for {p} via AS{a}", p=prefix, a=asn)
+
+ state = 3
+ query = 'query GetValidation {{ validation(prefix: "{prefix}", asn: {asn}) {{ state }} }}'.format( # noqa: E501
+ prefix=prefix, asn=asn
+ )
+
+ try:
+ with BaseExternal(base_url="https://rpki.cloudflare.com") as client:
+ response = client._post("/api/graphql", data={"query": query})
+ validation_state = (
+ response.get("data", {}).get("validation", {}).get("state", "DEFAULT")
+ )
+ state = RPKI_STATE_MAP[validation_state]
+ except Exception:
+ state = 3
+
+ log.debug(
+ "RPKI Validation State for {p} via AS{a} is {s}",
+ p=prefix,
+ a=asn,
+ s=RPKI_NAME_MAP[state],
+ )
+ return state