Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
16 changes: 16 additions & 0 deletions server/secops-soar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,22 @@ $Env:SOAR_APP_KEY = "your-soar-app-key"
$Env:SOAR_INTEGRATIONS = "ServiceNow,CSV,Siemplify"
```

## Troubleshooting

If the server shuts down at startup with an error like `Failed to fetch valid
scopes from SOAR due to an SSL certificate verification error`, this is a
local CA certificate problem, not incorrect `SOAR_URL`/`SOAR_APP_KEY` values.
Install the certifi CA bundle:

- **macOS:** run `Install Certificates.command` from your Python install,
e.g. `/Applications/Python\ 3.12/Install\ Certificates.command` (match the
Python minor version you're running).
- **Linux/Windows:** point `SSL_CERT_FILE` at the certifi bundle, e.g.
`export SSL_CERT_FILE=$(python -m certifi)` (or the PowerShell equivalent
`$Env:SSL_CERT_FILE = (python -m certifi)`).

See `docs/usage_guide.md` for further details.

## Requirements

- Python 3.11+
Expand Down
17 changes: 16 additions & 1 deletion server/secops-soar/secops_soar_mcp/bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""Bindings for the SOAR client."""

import os
import ssl

import dotenv
from logger_utils import get_logger
Expand All @@ -28,9 +29,23 @@
http_client: HttpClient = None
valid_scopes = set()

_CERTIFICATE_ERROR_MESSAGE = (
"Failed to fetch valid scopes from SOAR due to an SSL certificate "
"verification error. This is usually a local CA certificate "
"configuration issue, not incorrect SOAR credentials. Install the "
"certifi CA bundle, e.g. run Python's `Install Certificates.command` "
"(macOS) or point SSL_CERT_FILE at the output of `python -m certifi`. "
"See the 'Additionally, for the secops-soar MCP server...' note in "
"README.md / docs/usage_guide.md for the exact setup steps. "
"Shutting down..."
)


async def _get_valid_scopes():
valid_scopes_list = await http_client.get(consts.Endpoints.GET_SCOPES)
try:
valid_scopes_list = await http_client.get(consts.Endpoints.GET_SCOPES)
except ssl.SSLError as e:
raise RuntimeError(_CERTIFICATE_ERROR_MESSAGE) from e
if valid_scopes_list is None:
raise RuntimeError(
"Failed to fetch valid scopes from SOAR, please make sure you have configured the right SOAR credentials. Shutting down..."
Expand Down
13 changes: 13 additions & 0 deletions server/secops-soar/secops_soar_mcp/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""HTTP client for making requests to the SecOps SOAR API."""

import json
import ssl
from typing import Any, Dict

import aiohttp
Expand Down Expand Up @@ -64,6 +65,10 @@ async def get(
return await response.json()
except aiohttp.ClientResponseError as e:
logger.debug("HTTP error occurred: %s", e)
except (ssl.SSLError, aiohttp.ClientSSLError):
# Don't mask certificate configuration problems as a plain
# "no data" result; callers need to see and report on these.
raise
except Exception as e:
logger.debug("An error occurred: %s", e)
return None
Expand Down Expand Up @@ -95,6 +100,10 @@ async def post(
return json.loads(decoded_data)
except aiohttp.ClientResponseError as e:
logger.debug("HTTP error occurred: %s", e)
except (ssl.SSLError, aiohttp.ClientSSLError):
# Don't mask certificate configuration problems as a plain
# "no data" result; callers need to see and report on these.
raise
except Exception as e:
logger.debug("An error occurred: %s", e)
return None
Expand Down Expand Up @@ -124,6 +133,10 @@ async def patch(
return await response.json()
except aiohttp.ClientResponseError as e:
logger.debug("HTTP error occurred: %s", e)
except (ssl.SSLError, aiohttp.ClientSSLError):
# Don't mask certificate configuration problems as a plain
# "no data" result; callers need to see and report on these.
raise
except Exception as e:
logger.debug("An error occurred: %s", e)
return None
Expand Down
Empty file.
23 changes: 23 additions & 0 deletions server/secops-soar/tests/unit/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright 2025 Google LLC
Comment thread
Som0111 marked this conversation as resolved.
Outdated
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import pytest_asyncio


@pytest_asyncio.fixture(loop_scope="session", autouse=True)
async def setup_bindings():
"""Overrides tests/conftest.py's fixture: these unit tests mock
bindings/http_client directly and must not require real SOAR
credentials or network access."""
yield
89 changes: 89 additions & 0 deletions server/secops-soar/tests/unit/test_certificate_error_handling.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests that certificate errors get a helpful message instead of the
generic "wrong SOAR credentials" one, and that HttpClient doesn't swallow
SSL errors as a plain None result (see issue #191)."""

import ssl
from unittest import mock

import aiohttp
import pytest

from secops_soar_mcp import bindings
from secops_soar_mcp.http_client import HttpClient


class _RaisingSession:
"""Minimal aiohttp session stand-in whose .get() raises."""

def __init__(self, exc: Exception):
self._exc = exc

def get(self, *args, **kwargs):
raise self._exc

async def close(self):
pass


@pytest.mark.asyncio
async def test_http_client_get_reraises_ssl_error():
client = HttpClient("https://example.com", "app-key")
client._session = _RaisingSession(ssl.SSLCertVerificationError("bad cert"))

with pytest.raises(ssl.SSLError):
await client.get("/some/endpoint")


@pytest.mark.asyncio
async def test_http_client_get_swallows_generic_connection_error():
client = HttpClient("https://example.com", "app-key")
client._session = _RaisingSession(aiohttp.ClientConnectionError("refused"))

assert await client.get("/some/endpoint") is None


@pytest.mark.asyncio
async def test_get_valid_scopes_reports_certificate_issue_not_credentials():
with (
mock.patch.object(
bindings,
"http_client",
new=mock.AsyncMock(get=mock.AsyncMock(side_effect=ssl.SSLCertVerificationError())),
),
pytest.raises(RuntimeError) as exc_info,
):
await bindings._get_valid_scopes()

message = str(exc_info.value)
assert "certifi" in message.lower()
assert "certificate" in message.lower()
assert "not" in message.lower() and "credentials" in message.lower()


@pytest.mark.asyncio
async def test_get_valid_scopes_still_blames_credentials_when_no_data():
with (
mock.patch.object(
bindings,
"http_client",
new=mock.AsyncMock(get=mock.AsyncMock(return_value=None)),
),
pytest.raises(RuntimeError) as exc_info,
):
await bindings._get_valid_scopes()

assert "credentials" in str(exc_info.value).lower()