diff --git a/gsap_broker/connectors/bascule.py b/gsap_broker/connectors/bascule.py new file mode 100644 index 0000000..4ca91ed --- /dev/null +++ b/gsap_broker/connectors/bascule.py @@ -0,0 +1,87 @@ +# Copyright 2026 Guildhouse Dev +# SPDX-License-Identifier: Apache-2.0 + +"""Bascule connector — governed shell sessions via Shellstream. + +The Bascule connector uses the AC as the credential. Bascule (the +governed-shell runtime) validates ACs natively — no separate secrets +backend is needed. The transport establishes a Shellstream session +to the target endpoint via a Bascule proxy. + +Real integration: + The ``BasculeTransport`` will use the Shellstream protocol library + (``substrate/shellstream/``) to establish a 3-way attested handshake + with the target. The SAT (Shell Attestation Token) embedded in the + Shellstream frame header is derived from the AC. See + ``SS-SPEC-0001 — Shellstream Protocol.md`` for the handshake flow. + + Library: ``shellstream-py`` (future) or gRPC to Bascule proxy + Protocol: Shellstream over TCP / QUIC + Auth: AC passthrough — the AC IS the credential + +Stubbed in this sprint — transport returns placeholder results. +""" + +from __future__ import annotations + +from typing import Any, Optional + +from gsap_broker.connectors.session import SessionConnector, SessionTransport +from gsap_broker.credentials.resolver import Credential, CredentialResolver + + +class BasculeTransport(SessionTransport): + """Shellstream transport to a Bascule-governed endpoint. + + Stubbed — actual Shellstream integration in a future sprint. + """ + + transport_id = "bascule" + + def __init__(self) -> None: + self._target = "" + self._connected = False + + async def connect(self, target: str, credential: Credential) -> None: + # TODO: establish Shellstream connection to target via Bascule + # proxy. The credential IS the AC (BasculeCredential). + # Handshake: ATTEST-INIT → ATTEST-VERIFY → ATTEST-CONFIRM. + self._target = target + self._connected = True + + async def execute(self, command: str, params: Optional[dict[str, Any]] = None) -> dict[str, Any]: + # TODO: send command via Shellstream, capture output. + # Shellstream frames carry the SAT in the header for + # per-frame attestation. + if not self._connected: + raise RuntimeError("Not connected") + return { + "stub": True, + "transport": self.transport_id, + "target": self._target, + "command": command, + "params": params or {}, + } + + async def disconnect(self) -> None: + self._connected = False + + async def is_alive(self) -> bool: + return self._connected + + +class BasculeConnector(SessionConnector): + """Governed shell connector using Bascule/Shellstream.""" + + connector_id = "bascule" + corpus_entry_cid = "sha256:bascule-connector-v1" + credential_type = "bascule_ac" + transport_class = BasculeTransport + capability_mask = 0x7 # READ | PROPOSE | MUTATE + declared_endpoints = ["shellstream://*"] + accord_template = "governed-shell" + gsap_required = True + chronicle_enabled = True + + def __init__(self, credential_resolver: CredentialResolver): + super().__init__(credential_resolver) diff --git a/gsap_broker/connectors/powershell.py b/gsap_broker/connectors/powershell.py new file mode 100644 index 0000000..f73e16e --- /dev/null +++ b/gsap_broker/connectors/powershell.py @@ -0,0 +1,88 @@ +# Copyright 2026 Guildhouse Dev +# SPDX-License-Identifier: Apache-2.0 + +"""PowerShell connector — WinRM/PSRP sessions to Windows endpoints. + +Uses Kerberos credentials from the CredentialResolver (acquired via +Entra cloud trust or on-prem KDC) to establish a PSRP (PowerShell +Remoting Protocol) session over WinRM/HTTPS. + +Real integration: + Library: ``pypsrp`` (well-maintained PSRP client) + Protocol: PSRP over WinRM (HTTPS port 5986) + Auth: Kerberos (from ``KerberosCredential``) or CredSSP + Output: Structured PSObject results deserialized to dicts + + The transport will: + 1. Create a ``pypsrp.wsman.WSMan`` connection with the Kerberos + ticket from the credential. + 2. Open a ``pypsrp.powershell.PowerShell`` runspace. + 3. Execute commands via ``ps.add_script(command).invoke()``. + 4. Return deserialized PSObject results as Python dicts. + 5. Close the runspace and WSMan connection on disconnect. + +Stubbed in this sprint — transport returns placeholder results. +""" + +from __future__ import annotations + +from typing import Any, Optional + +from gsap_broker.connectors.session import SessionConnector, SessionTransport +from gsap_broker.credentials.resolver import Credential, CredentialResolver + + +class PowerShellTransport(SessionTransport): + """PSRP transport over WinRM/HTTPS. + + Stubbed — actual pypsrp integration in a future sprint. + """ + + transport_id = "psrp" + + def __init__(self) -> None: + self._target = "" + self._connected = False + + async def connect(self, target: str, credential: Credential) -> None: + # TODO: create pypsrp.wsman.WSMan with Kerberos auth from + # credential.ticket. Target format: hostname:5986. + self._target = target + self._connected = True + + async def execute(self, command: str, params: Optional[dict[str, Any]] = None) -> dict[str, Any]: + # TODO: execute PowerShell command via PSRP. + # ps = PowerShell(wsman); ps.add_script(command).invoke() + # Return structured PSObject results as dicts. + if not self._connected: + raise RuntimeError("Not connected") + return { + "stub": True, + "transport": self.transport_id, + "target": self._target, + "command": command, + "params": params or {}, + } + + async def disconnect(self) -> None: + self._connected = False + + async def is_alive(self) -> bool: + return self._connected + + +class PowerShellConnector(SessionConnector): + """Windows management via PowerShell Remoting.""" + + connector_id = "powershell" + corpus_entry_cid = "sha256:powershell-connector-v1" + credential_type = "kerberos" + transport_class = PowerShellTransport + capability_mask = 0x7 # READ | PROPOSE | MUTATE + declared_endpoints = ["wsman://*:5986"] + accord_template = "windows-management" + gsap_required = True + chronicle_enabled = True + + def __init__(self, credential_resolver: CredentialResolver): + super().__init__(credential_resolver)