fastapi-gsap/gsap_broker/connectors/powershell.py
Tyler J King eee8740ce8 feat(connectors): add stubbed Bascule and PowerShell connectors
Bascule: session-based connector using AC as credential.
Transport stubbed — Shellstream integration in future sprint.

PowerShell: session-based connector using Kerberos credentials
from CredentialResolver. PSRP transport stubbed — pypsrp
integration in future sprint.

Signed-off-by: Tyler King <tking@guildhouse.dev>
2026-04-14 05:59:56 -04:00

88 lines
3 KiB
Python

# 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)