Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
* @michaelmoore-s1
* @samuelmatos-s1
* @tjkuson-s1
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Libraries implement **standalone, reusable business logic**:

```python
# ✅ CORRECT: Library with explicit configuration
import uuid
from purple_mcp.libs.purple_ai import (
PurpleAIConfig,
PurpleAIUserDetails,
Expand All @@ -52,6 +53,7 @@ from purple_mcp.libs.purple_ai import (
user_details = PurpleAIUserDetails(
account_id="account-123",
team_token="team-token",
session_id=uuid.uuid4().hex,
email_address="[email protected]",
user_agent="purple-mcp/1.0",
build_date="2024-01-01",
Expand Down Expand Up @@ -104,6 +106,7 @@ async def purple_ai(query: str) -> str:
user_details = PurpleAIUserDetails(
account_id=settings.purple_ai_account_id,
team_token=settings.purple_ai_team_token,
session_id=settings.purple_ai_session_id,
email_address=settings.purple_ai_email_address,
user_agent=settings.purple_ai_user_agent,
build_date=settings.purple_ai_build_date,
Expand Down
5 changes: 3 additions & 2 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# To see the full list of contributors, see the revision history.

Michael Moore
Tom Kuson
Timothy Ng
Samuel Matos
Simin Chen
Timothy Ng
Tom Kuson
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased] - YYYY-MM-DD

## Changed

- Updated default values for client details to be more accurate

## [0.5.1] - 2025-11-08

### Added
Expand Down
29 changes: 19 additions & 10 deletions src/purple_mcp/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
"""

import logging
import uuid
from functools import lru_cache
from typing import ClassVar, Final
from urllib.parse import urlparse

from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

from purple_mcp import __version__

logger = logging.getLogger(__name__)

# Environment variable prefix constants
Expand All @@ -34,6 +37,7 @@
INVENTORY_RESTAPI_ENDPOINT_ENV: Final[str] = f"{ENV_PREFIX}INVENTORY_RESTAPI_ENDPOINT"
PURPLE_AI_ACCOUNT_ID_ENV: Final[str] = f"{ENV_PREFIX}PURPLE_AI_ACCOUNT_ID"
PURPLE_AI_TEAM_TOKEN_ENV: Final[str] = f"{ENV_PREFIX}PURPLE_AI_TEAM_TOKEN"
PURPLE_AI_SESSION_ID_ENV: Final[str] = f"{ENV_PREFIX}PURPLE_AI_SESSION_ID"
PURPLE_AI_EMAIL_ADDRESS_ENV: Final[str] = f"{ENV_PREFIX}PURPLE_AI_EMAIL_ADDRESS"
PURPLE_AI_USER_AGENT_ENV: Final[str] = f"{ENV_PREFIX}PURPLE_AI_USER_AGENT"
PURPLE_AI_BUILD_DATE_ENV: Final[str] = f"{ENV_PREFIX}PURPLE_AI_BUILD_DATE"
Expand Down Expand Up @@ -110,39 +114,44 @@ class Settings(BaseSettings):

# Purple AI User Details
purple_ai_account_id: str = Field(
default="AIMONITORING",
default="0",
description="Account ID for Purple AI user details",
validation_alias=PURPLE_AI_ACCOUNT_ID_ENV,
)
purple_ai_team_token: str = Field(
default="AIMONITORING",
default="0",
description="Team token for Purple AI user details",
validation_alias=PURPLE_AI_TEAM_TOKEN_ENV,
)
purple_ai_email_address: str = Field(
default="[email protected]",
purple_ai_session_id: str | None = Field(
default_factory=lambda: uuid.uuid4().hex,
description="Session ID for Purple AI user details",
validation_alias=PURPLE_AI_SESSION_ID_ENV,
)
purple_ai_email_address: str | None = Field(
default=None,
description="Email address for Purple AI user details",
validation_alias=PURPLE_AI_EMAIL_ADDRESS_ENV,
)
purple_ai_user_agent: str = Field(
default="IsaacAsimovMonitoringInc",
default=f"sentinelone/purple-mcp (version {__version__})",
description="User agent for Purple AI user details",
validation_alias=PURPLE_AI_USER_AGENT_ENV,
)
purple_ai_build_date: str = Field(
default="02/28/2025, 00:00:00 AM",
purple_ai_build_date: str | None = Field(
default=None,
description="Build date for Purple AI user details",
validation_alias=PURPLE_AI_BUILD_DATE_ENV,
)
purple_ai_build_hash: str = Field(
default="N/A",
purple_ai_build_hash: str | None = Field(
default=None,
description="Build hash for Purple AI user details",
validation_alias=PURPLE_AI_BUILD_HASH_ENV,
)

# Purple AI Console Details
purple_ai_console_version: str = Field(
default="S-25.1.1#30",
default="S",
description="Version for Purple AI console details",
validation_alias=PURPLE_AI_CONSOLE_VERSION_ENV,
)
Expand Down
3 changes: 2 additions & 1 deletion src/purple_mcp/libs/purple_ai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ async def main():
user_details=PurpleAIUserDetails(
account_id="your-account-id",
team_token="your-team-token",
session_id="your-session-id",
email_address="[email protected]",
user_agent="PurpleAI-Client/1.0",
build_date="2024-01-01",
Expand Down Expand Up @@ -79,4 +80,4 @@ The library requires a SentinelOne console service token with appropriate permis

## Contributing

This library follows the purple-mcp project's contribution guidelines. See the main project's CONTRIBUTING.md for details.
This library follows the purple-mcp project's contribution guidelines. See the main project's CONTRIBUTING.md for details.
45 changes: 26 additions & 19 deletions src/purple_mcp/libs/purple_ai/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import secrets
import string
import time
import uuid
from datetime import timedelta
from enum import Enum
from http import HTTPStatus
Expand All @@ -26,6 +27,7 @@
wait_exponential,
)

from purple_mcp import __version__
from purple_mcp.libs.purple_ai.config import (
PurpleAIConfig,
PurpleAIConsoleDetails,
Expand Down Expand Up @@ -67,12 +69,13 @@ def _build_graphql_request(
end_time: int,
base_url: str,
version: str,
account_id: str,
team_token: str,
email_address: str,
user_agent: str,
build_date: str,
build_hash: str,
scalyr_account_id: str,
scalyr_team_token: str,
session_id: str | None,
email_address: str | None,
user_agent: str | None,
build_date: str | None,
build_hash: str | None,
conversation_id: str,
) -> str:
"""Construct a GraphQL request with properly escaped string values.
Expand All @@ -87,8 +90,9 @@ def _build_graphql_request(
end_time: End time in milliseconds since epoch
base_url: Console base URL
version: Console version
account_id: User account ID
team_token: User team token
scalyr_account_id: Scalyr User account ID
scalyr_team_token: Scalyr User team token
session_id: User session ID
email_address: User email address
user_agent: User agent string
build_date: Build date string
Expand Down Expand Up @@ -121,8 +125,9 @@ def _build_graphql_request(
viewSelector: EDR
contentType: NATURAL_LANGUAGE
userDetails: {{
accountId: {json.dumps(account_id)}
teamToken: {json.dumps(team_token)}
accountId: {json.dumps(scalyr_account_id)}
teamToken: {json.dumps(scalyr_team_token)}
sessionId: {json.dumps(session_id)}
emailAddress: {json.dumps(email_address)}
userAgent: {json.dumps(user_agent)}
buildDate: {json.dumps(build_date)}
Expand Down Expand Up @@ -213,8 +218,9 @@ def _generate_query(self, query: str, conversation_id_for_tests: str | None = No
end_time=current_time_millis,
base_url=self.config.console_details.base_url,
version=self.config.console_details.version,
account_id=self.config.user_details.account_id,
team_token=self.config.user_details.team_token,
scalyr_account_id=self.config.user_details.account_id,
scalyr_team_token=self.config.user_details.team_token,
session_id=self.config.user_details.session_id,
email_address=self.config.user_details.email_address,
user_agent=self.config.user_details.user_agent,
build_date=self.config.user_details.build_date,
Expand Down Expand Up @@ -519,16 +525,17 @@ async def main() -> None:
"""Run a test query against Purple AI."""
config = PurpleAIConfig(
user_details=PurpleAIUserDetails(
account_id="AIMONITORING",
team_token="AIMONITORING",
email_address="[email protected]",
user_agent="IsaacAsimovMonitoringInc",
build_date="02/28/2025, 00:00:00 AM",
build_hash="N/A",
account_id="0",
team_token="0",
session_id=uuid.uuid4().hex,
email_address=None,
user_agent=f"sentinelone/purple-mcp (version {__version__})",
build_date=None,
build_hash=None,
),
console_details=PurpleAIConsoleDetails(
base_url="https://console.example.com",
version="S-25.1.1#30",
version="S",
),
)
result = await ask_purple(config, "What's the weather in Tokyo?")
Expand Down
9 changes: 5 additions & 4 deletions src/purple_mcp/libs/purple_ai/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ class PurpleAIUserDetails(_ProgrammaticSettings):

account_id: str = Field(..., description="Account ID for the user.")
team_token: str = Field(..., description="Team token for the user.")
email_address: str = Field(..., description="Email address of the user.")
user_agent: str = Field(..., description="User agent for the request.")
build_date: str = Field(..., description="Build date of the client.")
build_hash: str = Field(..., description="Build hash of the client.")
session_id: str | None = Field(..., description="Session ID for the user.")
email_address: str | None = Field(..., description="Email address of the user.")
user_agent: str | None = Field(..., description="User agent for the request.")
build_date: str | None = Field(..., description="Build date of the client.")
build_hash: str | None = Field(..., description="Build hash of the client.")


class PurpleAIConsoleDetails(_ProgrammaticSettings):
Expand Down
22 changes: 17 additions & 5 deletions src/purple_mcp/libs/purple_ai/docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Ask Purple AI a question asynchronously.

**Example:**
```python
import uuid
import asyncio
from purple_mcp.libs.purple_ai import (
ask_purple,
Expand All @@ -34,14 +35,15 @@ async def main():
user_details=PurpleAIUserDetails(
account_id="123456789",
team_token="team-token",
session_id=uuid.uuid4().hex,
email_address="[email protected]",
user_agent="MyApp/1.0",
build_date="2025-01-01",
build_hash="abc123",
),
console_details=PurpleAIConsoleDetails(
base_url="https://console.example.com",
version="S-25.1.1#30",
version="S",
),
)

Expand Down Expand Up @@ -72,6 +74,7 @@ Ask Purple AI a question synchronously.

**Example:**
```python
import uuid
from purple_mcp.libs.purple_ai import sync_ask_purple, PurpleAIConfig, PurpleAIUserDetails, PurpleAIConsoleDetails

config = PurpleAIConfig(
Expand All @@ -80,14 +83,15 @@ config = PurpleAIConfig(
user_details=PurpleAIUserDetails(
account_id="123456789",
team_token="team-token",
session_id=uuid.uuid4().hex,
email_address="[email protected]",
user_agent="MyApp/1.0",
build_date="2025-01-01",
build_hash="abc123"
),
console_details=PurpleAIConsoleDetails(
base_url="https://console.example.com",
version="S-25.1.1#30"
version="S"
)
)

Expand Down Expand Up @@ -120,6 +124,7 @@ PurpleAIConfig(

#### Example
```python
import uuid
from purple_mcp.libs.purple_ai import (
PurpleAIConfig,
PurpleAIConsoleDetails,
Expand All @@ -131,11 +136,12 @@ config = PurpleAIConfig(
auth_token="your-service-token",
console_details=PurpleAIConsoleDetails(
base_url="https://console.example.com",
version="S-25.1.1#30"
version="S"
),
user_details=PurpleAIUserDetails(
account_id="123456789",
team_token="team-token-abc",
session_id=uuid.uuid4().hex,
email_address="[email protected]",
user_agent="MyApp/1.0",
build_date="2025-01-15",
Expand Down Expand Up @@ -164,7 +170,7 @@ PurpleAIConsoleDetails(
```python
console_details = PurpleAIConsoleDetails(
base_url="https://console.example.com",
version="S-25.1.1#30"
version="S"
)
```

Expand All @@ -177,6 +183,7 @@ User-specific configuration details.
PurpleAIUserDetails(
account_id: str,
team_token: str,
session_id: str,
email_address: str,
user_agent: str,
build_date: str,
Expand All @@ -197,6 +204,7 @@ PurpleAIUserDetails(
user_details = PurpleAIUserDetails(
account_id="123456789",
team_token="team-token-abc",
session_id=uuid.uuid4().hex,
email_address="[email protected]",
user_agent="SecurityTools/2.1 (Python/3.11)",
build_date="2025-01-15",
Expand Down Expand Up @@ -411,6 +419,7 @@ The library can use environment variables for configuration:

```python
import os
import uuid
from purple_mcp.libs.purple_ai import PurpleAIConfig

def create_config_from_env():
Expand All @@ -422,6 +431,7 @@ def create_config_from_env():
user_details=PurpleAIUserDetails(
account_id=os.getenv("PURPLEMCP_PURPLE_AI_ACCOUNT_ID", ""),
team_token=os.getenv("PURPLEMCP_PURPLE_AI_TEAM_TOKEN", ""),
session_id=os.getenv("PURPLEMCP_PURPLE_AI_SESSION_ID", uuid.uuid4().hex),
email_address=os.getenv("PURPLEMCP_PURPLE_AI_EMAIL_ADDRESS", ""),
user_agent=os.getenv("PURPLEMCP_PURPLE_AI_USER_AGENT", "PurpleAI/1.0"),
build_date=os.getenv("PURPLEMCP_PURPLE_AI_BUILD_DATE", ""),
Expand Down Expand Up @@ -468,6 +478,7 @@ test_config = PurpleAIConfig(
user_details=PurpleAIUserDetails(
account_id="test-account",
team_token="test-team-token",
session_id="test-session-id",
email_address="[email protected]",
user_agent="TestClient/1.0",
build_date="2025-01-01",
Expand Down Expand Up @@ -503,6 +514,7 @@ async def test_purple_ai_integration():
user_details=PurpleAIUserDetails(
account_id=os.getenv("PURPLEMCP_PURPLE_AI_ACCOUNT_ID"),
team_token=os.getenv("PURPLEMCP_PURPLE_AI_TEAM_TOKEN"),
session_id=os.getenv("PURPLEMCP_PURPLE_AI_SESSION_ID"),
email_address=os.getenv("PURPLEMCP_PURPLE_AI_EMAIL_ADDRESS"),
user_agent=os.getenv("PURPLEMCP_PURPLE_AI_USER_AGENT", "IntegrationTest/1.0"),
build_date=os.getenv("PURPLEMCP_PURPLE_AI_BUILD_DATE"),
Expand All @@ -519,4 +531,4 @@ _result_type, response = await ask_purple("What is SentinelOne?", config)
assert response is not None
assert len(response) > 0
assert isinstance(response, str)
```
```
Loading