summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/dev/engines/online/azure.rst8
-rw-r--r--searx/engines/azure.py190
-rw-r--r--searx/settings.yml9
3 files changed, 207 insertions, 0 deletions
diff --git a/docs/dev/engines/online/azure.rst b/docs/dev/engines/online/azure.rst
new file mode 100644
index 000000000..d0484d4c2
--- /dev/null
+++ b/docs/dev/engines/online/azure.rst
@@ -0,0 +1,8 @@
+.. _azure engine:
+
+===============
+Azure Resources
+===============
+
+.. automodule:: searx.engines.azure
+ :members:
diff --git a/searx/engines/azure.py b/searx/engines/azure.py
new file mode 100644
index 000000000..35538b561
--- /dev/null
+++ b/searx/engines/azure.py
@@ -0,0 +1,190 @@
+# SPDX-License-Identifier: AGPL-3.0-or-later
+"""Engine for Azure resources. This engine mimics the standard search bar in Azure
+Portal (for resources and resource groups).
+
+Configuration
+=============
+
+You must `register an application in Microsoft Entra ID`_ and assign it the
+'Reader' role in your subscription.
+
+To use this engine, add an entry similar to the following to your engine list in
+``settings.yml``:
+
+.. code:: yaml
+
+ - name: azure
+ engine: azure
+ ...
+ azure_tenant_id: "your_tenant_id"
+ azure_client_id: "your_client_id"
+ azure_client_secret: "your_client_secret"
+ azure_token_expiration_seconds: 5000
+
+.. _register an application in Microsoft Entra ID:
+ https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app
+
+"""
+import typing as t
+
+from searx.enginelib import EngineCache
+from searx.network import post as http_post
+from searx.result_types import EngineResults
+
+if t.TYPE_CHECKING:
+ from searx.extended_types import SXNG_Response
+ from searx.search.processors import OnlineParams
+
+engine_type = "online"
+categories = ["it", "cloud"]
+
+# Default values, should be overridden in settings.yml
+azure_tenant_id = ""
+azure_client_id = ""
+azure_client_secret = ""
+azure_token_expiration_seconds = 5000
+"""Time for which an auth token is valid (sec.)"""
+azure_batch_endpoint = "https://management.azure.com/batch?api-version=2020-06-01"
+
+about = {
+ "website": "https://www.portal.azure.com",
+ "wikidata_id": "Q725967",
+ "official_api_documentation": "https://learn.microsoft.com/en-us/\
+ rest/api/azure-resourcegraph/?view=rest-azureresourcegraph-resourcegraph-2024-04-01",
+ "use_official_api": True,
+ "require_api_key": True,
+ "results": "JSON",
+ "language": "en",
+}
+
+CACHE: EngineCache
+"""Persistent (SQLite) key/value cache that deletes its values after ``expire``
+seconds."""
+
+
+def setup(engine_settings: dict[str, t.Any]) -> bool:
+ """Initialization of the engine.
+
+ - Instantiate a cache for this engine (:py:obj:`CACHE`).
+ - Checks whether the tenant_id, client_id and client_secret are set,
+ otherwise the engine is inactive.
+
+ """
+ global CACHE # pylint: disable=global-statement
+ CACHE = EngineCache(engine_settings["name"])
+
+ missing_opts: list[str] = []
+ for opt in ("azure_tenant_id", "azure_client_id", "azure_client_secret"):
+ if not engine_settings.get(opt, ""):
+ missing_opts.append(opt)
+ if missing_opts:
+ logger.error("missing values for options: %s", ", ".join(missing_opts))
+ return False
+ return True
+
+
+def authenticate(t_id: str, c_id: str, c_secret: str) -> str:
+ """Authenticates to Azure using Oauth2 Client Credentials Flow and returns
+ an access token."""
+
+ url = f"https://login.microsoftonline.com/{t_id}/oauth2/v2.0/token"
+ body = {
+ "client_id": c_id,
+ "client_secret": c_secret,
+ "grant_type": "client_credentials",
+ "scope": "https://management.azure.com/.default",
+ }
+
+ resp: SXNG_Response = http_post(url, body)
+ if resp.status_code != 200:
+ raise RuntimeError(f"Azure authentication failed (status {resp.status_code}): {resp.text}")
+ return resp.json()["access_token"]
+
+
+def get_auth_token(t_id: str, c_id: str, c_secret: str) -> str:
+ key = f"azure_tenant_id: {t_id:}, azure_client_id: {c_id}, azure_client_secret: {c_secret}"
+ token: str | None = CACHE.get(key)
+ if token:
+ return token
+ token = authenticate(t_id, c_id, c_secret)
+ CACHE.set(key=key, value=token, expire=azure_token_expiration_seconds)
+ return token
+
+
+def request(query: str, params: "OnlineParams") -> None:
+
+ token = get_auth_token(azure_tenant_id, azure_client_id, azure_client_secret)
+
+ params["url"] = azure_batch_endpoint
+ params["method"] = "POST"
+ params["headers"]["Authorization"] = f"Bearer {token}"
+ params["headers"]["Content-Type"] = "application/json"
+ params["json"] = {
+ "requests": [
+ {
+ "url": "/providers/Microsoft.ResourceGraph/resources?api-version=2024-04-01",
+ "httpMethod": "POST",
+ "name": "resourceGroups",
+ "requestHeaderDetails": {"commandName": "Microsoft.ResourceGraph"},
+ "content": {
+ "query": (
+ f"ResourceContainers"
+ f" | where (name contains ('{query}'))"
+ f" | where (type =~ ('Microsoft.Resources/subscriptions/resourcegroups'))"
+ f" | project id,name,type,kind,subscriptionId,resourceGroup"
+ f" | extend matchscore = name startswith '{query}'"
+ f" | extend normalizedName = tolower(tostring(name))"
+ f" | sort by matchscore desc, normalizedName asc"
+ f" | take 30"
+ )
+ },
+ },
+ {
+ "url": "/providers/Microsoft.ResourceGraph/resources?api-version=2024-04-01",
+ "httpMethod": "POST",
+ "name": "resources",
+ "requestHeaderDetails": {
+ "commandName": "Microsoft.ResourceGraph",
+ },
+ "content": {
+ "query": f"Resources | where name contains '{query}' | take 30",
+ },
+ },
+ ]
+ }
+
+
+def response(resp: "SXNG_Response") -> EngineResults:
+ res = EngineResults()
+ json_data = resp.json()
+
+ for result in json_data["responses"]:
+ if result["name"] == "resourceGroups":
+ for data in result["content"]["data"]:
+ res.add(
+ res.types.MainResult(
+ url=(
+ f"https://portal.azure.com/#@/resource"
+ f"/subscriptions/{data['subscriptionId']}/resourceGroups/{data['name']}/overview"
+ ),
+ title=data["name"],
+ content=f"Resource Group in Subscription: {data['subscriptionId']}",
+ )
+ )
+ elif result["name"] == "resources":
+ for data in result["content"]["data"]:
+ res.add(
+ res.types.MainResult(
+ url=(
+ f"https://portal.azure.com/#@/resource"
+ f"/subscriptions/{data['subscriptionId']}/resourceGroups/{data['resourceGroup']}"
+ f"/providers/{data['type']}/{data['name']}/overview"
+ ),
+ title=data["name"],
+ content=(
+ f"Resource of type {data['type']} in Subscription:"
+ f" {data['subscriptionId']}, Resource Group: {data['resourceGroup']}"
+ ),
+ )
+ )
+ return res
diff --git a/searx/settings.yml b/searx/settings.yml
index 95202707f..5783e3eea 100644
--- a/searx/settings.yml
+++ b/searx/settings.yml
@@ -495,6 +495,15 @@ engines:
shortcut: ask
disabled: true
+ # - name: azure
+ # engine: azure
+ # shortcut: az
+ # categories: [it, cloud]
+ # azure_tenant_id: "your_tenant_id"
+ # azure_client_id: "your_client_id"
+ # azure_client_secret: "your_client_secret"
+ # disabled: true
+
# tmp suspended: dh key too small
# - name: base
# engine: base