Writing a connector¶
A third-party connector is a Python package that the controller loads to send the findings to another system. This page is for its developers; to install and allow a connector, see Third-party connectors.
A connector is a subclass of Connector (module security_review_mcp.connectors.api, version
API_VERSION = 2). The complete example
examples/connector
is a package to copy: it turns each finding into a risk of an Acme register.
Example¶
from security_review_mcp.connectors.api import (
Connection,
Connector,
ConnectorError,
ConnectorField,
ExportedFinding,
FindingContent,
)
class AcmeConnector(Connector):
kind = "acme" # identifier: exports, URLs, SECREVIEW_ACME_* variables
name = "Acme Risk" # display name
url_placeholder = "https://risk.acme.example/api"
fields = ( # the URL field is added by the controller
ConnectorField(
name="api_key",
label="API key",
secret=True,
required=True,
translations={"fr": {"label": "Clé d'API"}},
),
)
destination = ConnectorField(
name="destination", label="Risk register", required=True, pattern=r"[A-Z0-9_-]{1,40}"
)
def auth_headers(self, values):
return {"X-Api-Key": values["api_key"]}
def export_finding(
self, connection: Connection, destination: str, content: FindingContent
) -> ExportedFinding:
answer = connection.http.request(
"POST",
f"/registers/{destination}/risks",
json_body={"title": content.summary, "description": content.text()},
)
if not isinstance(answer.get("id"), str):
raise ConnectorError("invalid", "acme sent no risk id")
return ExportedFinding(key=answer["id"], url=f"{connection.base_url}/risks/{answer['id']}")
def test_connection(self, connection: Connection) -> None:
connection.http.request("GET", "/me")
# pyproject.toml of the package
[project.entry-points."security_review_mcp.connectors"]
acme = "acme_connector:AcmeConnector"
Elements of a connector¶
| Element | Role |
|---|---|
kind |
Lowercase letters, digits and _ (2 to 20 characters, not connectors); stored with the exports |
fields |
ConnectorField: name, label, secret, required, default, help, placeholder, pattern (full match), max_length, translations ({"fr": {"label": ..., "help": ...}}) |
destination |
What an administrator chooses per project; override check_destination to normalize it |
auth_headers(values) |
Authentication headers, added by the controller to each request |
export_finding(connection, destination, content) |
Creates the record of the finding and returns ExportedFinding(key, url) (https:// link) |
test_connection(connection) |
Read-only request that needs valid credentials |
Guarantees of the controller¶
What the controller guarantees, whatever the connector:
connection.http.request(method, path, json_body=..., query=...)is the only way out:pathis relative to the configured URL (no other host), over HTTPS, through the outgoing proxy, without following redirects, 15 second timeout, JSON answer bounded to 1 MiB; a failure raisesConnectorError(auth,refused,unreachable,invalid) without the secrets;contentis already masked:summary,facts,sections(texts written by an agent: escape them if the system renders markup),text()(plain description),fingerprint,report_url;- the claim (no duplicate record), the reuse of the record of an issue found again, the stored outcome, the audit and the retries of the automatic sending;
- any other exception of the connector becomes the
invaliderror, without its message (it could hold a value of the connection).
A connector must never open a connection by itself, log the values of its fields, nor change anything but the record it creates.