spot-sdk-python 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
spot_sdk/results.py ADDED
@@ -0,0 +1,129 @@
1
+ """Analysis result models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from enum import Enum
7
+ from typing import Any
8
+
9
+ from pydantic import BaseModel, Field, field_validator
10
+
11
+ from .analyzer import ThreatLevel
12
+
13
+
14
+ class IndicatorType(str, Enum):
15
+ """Types of phishing indicators."""
16
+
17
+ SUSPICIOUS_SENDER = "suspicious_sender"
18
+ URGENT_LANGUAGE = "urgent_language"
19
+ SUSPICIOUS_LINKS = "suspicious_links"
20
+ MALICIOUS_ATTACHMENT = "malicious_attachment"
21
+ SOCIAL_ENGINEERING = "social_engineering"
22
+ DOMAIN_SPOOFING = "domain_spoofing"
23
+ CREDENTIAL_HARVESTING = "credential_harvesting"
24
+ WIRE_TRANSFER_SCAM = "wire_transfer_scam"
25
+ CEO_FRAUD = "ceo_fraud"
26
+ INVOICE_SCAM = "invoice_scam"
27
+ IMPERSONATION = "impersonation"
28
+ SPELLING_GRAMMAR_ERRORS = "spelling_grammar_errors"
29
+ GENERIC_GREETING = "generic_greeting"
30
+ MISMATCHED_URL = "mismatched_url"
31
+ SUSPICIOUS_TIMING = "suspicious_timing"
32
+ MISP_IOC_MATCH = "misp_ioc_match"
33
+ DOMAIN_REPUTATION = "domain_reputation"
34
+ CONTEXT_MISMATCH = "context_mismatch"
35
+ SPF_FAILURE = "spf_failure"
36
+ UNUSUAL_HEADERS = "unusual_headers"
37
+ ENCODING_ANOMALY = "encoding_anomaly"
38
+ THREATENING_TONE = "threatening_tone"
39
+
40
+ def __str__(self) -> str:
41
+ return self.value
42
+
43
+
44
+ class AnalysisIndicator(BaseModel):
45
+ """Individual phishing indicator found in email."""
46
+
47
+ type: IndicatorType
48
+ confidence: float = Field(..., ge=0.0, le=1.0)
49
+ description: str
50
+ evidence: str | None = None # Text excerpt or specific evidence
51
+ severity: str = Field(..., pattern="^(undetermined|low|medium|high|critical)$")
52
+
53
+ # Optional: location in email where indicator was found
54
+ location: str | None = None # "subject", "body", "attachment", "headers"
55
+
56
+
57
+ class AnalysisMetadata(BaseModel):
58
+ """Analysis execution metadata."""
59
+
60
+ analyzer_id: str
61
+ analyzer_version: str
62
+ analysis_duration_ms: int
63
+ model_version: str | None = None
64
+ language_detected: str | None = None
65
+ processing_timestamp: datetime = Field(default_factory=datetime.utcnow)
66
+
67
+ # Resource usage (optional)
68
+ memory_usage_mb: int | None = None
69
+ cpu_usage_ms: int | None = None
70
+
71
+
72
+ class AnalysisResult(BaseModel):
73
+ """
74
+ Standardized analysis result from any analyzer.
75
+
76
+ This is the common format returned by all analyzers,
77
+ enabling consistent processing by the orchestrator.
78
+ """
79
+
80
+ # Core classification
81
+ is_phishing: bool
82
+ threat_level: ThreatLevel
83
+ confidence: float = Field(..., ge=0.0, le=1.0)
84
+
85
+ # Detailed findings
86
+ indicators: list[AnalysisIndicator] = Field(default_factory=list)
87
+ explanation: str = "" # Human-readable explanation
88
+
89
+ # Metadata
90
+ metadata: AnalysisMetadata
91
+
92
+ # Optional: raw analyzer output for debugging
93
+ raw_output: dict[str, Any] | None = None
94
+
95
+ # Additional context
96
+ recommendations: list[str] = Field(default_factory=list)
97
+ false_positive_likelihood: float | None = Field(None, ge=0.0, le=1.0)
98
+
99
+ @field_validator("threat_level")
100
+ @classmethod
101
+ def validate_threat_consistency(cls, v: str, info: Any) -> str:
102
+ """Ensure threat level is consistent with is_phishing."""
103
+ is_phishing = info.data.get("is_phishing")
104
+ if is_phishing and v == ThreatLevel.SAFE:
105
+ raise ValueError("Cannot have 'safe' threat level when is_phishing=True")
106
+ if not is_phishing and v in (ThreatLevel.HIGH, ThreatLevel.CRITICAL):
107
+ raise ValueError("High/critical threat levels require is_phishing=True")
108
+ return v
109
+
110
+ @property
111
+ def risk_score(self) -> int:
112
+ """Calculate numerical risk score (0-100)."""
113
+ base_score = int(self.confidence * 100)
114
+
115
+ # Adjust based on threat level
116
+ multipliers = {
117
+ ThreatLevel.SAFE: 0.0,
118
+ ThreatLevel.LOW: 0.3,
119
+ ThreatLevel.MEDIUM: 0.6,
120
+ ThreatLevel.HIGH: 0.8,
121
+ ThreatLevel.CRITICAL: 1.0,
122
+ }
123
+
124
+ return int(base_score * multipliers.get(self.threat_level, 0.5))
125
+
126
+ @property
127
+ def high_confidence_indicators(self) -> list[AnalysisIndicator]:
128
+ """Get indicators with confidence > 0.7."""
129
+ return [ind for ind in self.indicators if ind.confidence > 0.7]
@@ -0,0 +1,56 @@
1
+ """Analyzer settings schema endpoint registration.
2
+
3
+ Provides a helper to register a /settings/schema endpoint on any analyzer's
4
+ FastAPI app. The endpoint returns the JSON Schema of the analyzer's Pydantic
5
+ Settings model, which the platform uses to render typed configuration forms.
6
+
7
+ Usage in an analyzer's main.py:
8
+
9
+ from spot_sdk.settings_schema import register_settings_schema
10
+ from .config import Settings
11
+
12
+ app = FastAPI(...)
13
+ register_settings_schema(app, Settings)
14
+ """
15
+
16
+ from typing import Any
17
+
18
+ from fastapi import FastAPI
19
+ from pydantic_settings import BaseSettings
20
+
21
+
22
+ def register_settings_schema(app: FastAPI, settings_class: type[BaseSettings]) -> None:
23
+ """Register /settings/schema and /settings/values endpoints on a FastAPI app.
24
+
25
+ Args:
26
+ app: FastAPI application instance.
27
+ settings_class: The Pydantic BaseSettings class used by the analyzer.
28
+ """
29
+
30
+ @app.get("/settings/schema")
31
+ async def get_settings_schema() -> dict[str, Any]:
32
+ """Return JSON Schema for this analyzer's settings.
33
+
34
+ The schema includes field types, defaults, constraints (min/max),
35
+ and descriptions. The platform uses this to render typed forms
36
+ in the web dashboard.
37
+ """
38
+ return settings_class.model_json_schema()
39
+
40
+ @app.get("/settings/values")
41
+ async def get_settings_values() -> dict[str, Any]:
42
+ """Return current effective settings values.
43
+
44
+ Useful for debugging to see what values the analyzer is running with.
45
+ Sensitive fields (containing 'key', 'secret', 'password', 'token')
46
+ are redacted.
47
+ """
48
+ instance = settings_class()
49
+ values = instance.model_dump()
50
+
51
+ for key in list(values.keys()):
52
+ if any(s in key.lower() for s in ("key", "secret", "password", "token")):
53
+ if values[key]:
54
+ values[key] = "[REDACTED]"
55
+
56
+ return values
@@ -0,0 +1,83 @@
1
+ # SPOT SDK Testing Utilities
2
+
3
+ Shared test utilities for all SPOT projects (platform and analyzers).
4
+
5
+ ## Installation
6
+
7
+ The testing module is included with spot-sdk:
8
+
9
+ ```python
10
+ from spot_sdk.testing import (
11
+ create_test_email,
12
+ create_phishing_email,
13
+ create_test_analysis_result,
14
+ create_test_indicator,
15
+ )
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ### Creating Test Emails
21
+
22
+ ```python
23
+ # Create a legitimate email
24
+ email = create_test_email(
25
+ subject="Meeting Tomorrow",
26
+ sender="alice@company.com",
27
+ recipients=["team@company.com"],
28
+ body_text="Don't forget our meeting."
29
+ )
30
+
31
+ # Create a phishing email with common indicators
32
+ phishing_email = create_phishing_email()
33
+ ```
34
+
35
+ ### Creating Test Analysis Results
36
+
37
+ ```python
38
+ # Create a safe analysis result
39
+ result = create_test_analysis_result(
40
+ is_phishing=False,
41
+ confidence=0.9,
42
+ threat_level="SAFE"
43
+ )
44
+
45
+ # Create a phishing detection result
46
+ phishing_result = create_test_analysis_result(
47
+ is_phishing=True,
48
+ confidence=0.95,
49
+ threat_level="HIGH",
50
+ indicators=[
51
+ create_test_indicator(
52
+ indicator_type="URGENT_LANGUAGE",
53
+ description="Urgent language detected",
54
+ severity="high",
55
+ confidence=0.9
56
+ )
57
+ ]
58
+ )
59
+ ```
60
+
61
+ ## In pytest
62
+
63
+ Use these factories in your conftest.py:
64
+
65
+ ```python
66
+ import pytest
67
+ from spot_sdk.testing import create_test_email, create_phishing_email
68
+
69
+ @pytest.fixture
70
+ def sample_email():
71
+ return create_test_email()
72
+
73
+ @pytest.fixture
74
+ def phishing_email():
75
+ return create_phishing_email()
76
+ ```
77
+
78
+ ## Design Philosophy
79
+
80
+ - **Simple**: Plain dictionaries, not complex mocks
81
+ - **Practical**: Real test data that works with actual SDK models
82
+ - **Flexible**: Easy to customize for specific test cases
83
+ - **No magic**: No auto-mocking or complex test frameworks
@@ -0,0 +1,22 @@
1
+ """SPOT SDK Testing Utilities.
2
+
3
+ Shared test fixtures, factories, and utilities for all SPOT projects.
4
+ """
5
+
6
+ from .factories import (
7
+ create_analysis_request,
8
+ create_phishing_email,
9
+ create_test_analysis_result,
10
+ create_test_email,
11
+ create_test_indicator,
12
+ create_test_user,
13
+ )
14
+
15
+ __all__ = [
16
+ "create_test_email",
17
+ "create_phishing_email",
18
+ "create_test_analysis_result",
19
+ "create_test_indicator",
20
+ "create_analysis_request",
21
+ "create_test_user",
22
+ ]
@@ -0,0 +1,177 @@
1
+ """Test data factories for SPOT SDK.
2
+
3
+ Simple functions to create test data as plain dictionaries.
4
+ These work with spot_sdk models when validated via Pydantic.
5
+ """
6
+
7
+ from datetime import datetime, timezone
8
+ from typing import Any
9
+
10
+
11
+ def create_test_email(
12
+ subject: str = "Team Meeting Tomorrow",
13
+ sender: str = "alice@company.com",
14
+ recipients: list[str] | None = None,
15
+ body_text: str = "Hi team, don't forget about our meeting tomorrow.",
16
+ ) -> dict[str, Any]:
17
+ """Create a legitimate test email.
18
+
19
+ Args:
20
+ subject: Email subject
21
+ sender: Sender email address
22
+ recipients: List of recipient addresses
23
+ body_text: Email body text
24
+
25
+ Returns:
26
+ Email data dictionary
27
+ """
28
+ if recipients is None:
29
+ recipients = ["team@company.com"]
30
+
31
+ return {
32
+ "id": "test-email-123",
33
+ "headers": {
34
+ "message_id": "<test@test.com>",
35
+ "subject": subject,
36
+ "sender": sender,
37
+ "recipients": recipients,
38
+ "date": datetime.now(timezone.utc).isoformat(),
39
+ },
40
+ "body_text": body_text,
41
+ "body_html": None,
42
+ "attachments": [],
43
+ "received_at": datetime.now(timezone.utc).isoformat(),
44
+ }
45
+
46
+
47
+ def create_phishing_email() -> dict[str, Any]:
48
+ """Create a phishing test email with common phishing indicators.
49
+
50
+ Returns:
51
+ Email data dictionary with phishing characteristics
52
+ """
53
+ return {
54
+ "id": "phishing-email-456",
55
+ "headers": {
56
+ "message_id": "<phish@evil.com>",
57
+ "subject": "URGENT: Verify Your Account Now!",
58
+ "sender": "security@paypal-verify.com",
59
+ "recipients": ["victim@company.com"],
60
+ "date": datetime.now(timezone.utc).isoformat(),
61
+ },
62
+ "body_text": (
63
+ "Your account has been suspended due to suspicious activity. "
64
+ "Click here immediately to verify: http://bit.ly/fake-link"
65
+ ),
66
+ "body_html": None,
67
+ "attachments": [],
68
+ "received_at": datetime.now(timezone.utc).isoformat(),
69
+ }
70
+
71
+
72
+ def create_test_indicator(
73
+ indicator_type: str = "URGENT_LANGUAGE",
74
+ description: str = "Urgent language detected",
75
+ severity: str = "medium",
76
+ confidence: float = 0.8,
77
+ ) -> dict[str, Any]:
78
+ """Create a test analysis indicator.
79
+
80
+ Args:
81
+ indicator_type: Type of indicator
82
+ description: Human-readable description
83
+ severity: Severity level (low, medium, high)
84
+ confidence: Confidence score (0.0-1.0)
85
+
86
+ Returns:
87
+ Indicator data dictionary
88
+ """
89
+ return {
90
+ "type": indicator_type,
91
+ "description": description,
92
+ "severity": severity,
93
+ "confidence": confidence,
94
+ "source_analyzer": "test-analyzer",
95
+ }
96
+
97
+
98
+ def create_test_analysis_result(
99
+ is_phishing: bool = False,
100
+ confidence: float = 0.9,
101
+ threat_level: str = "SAFE",
102
+ indicators: list[dict[str, Any]] | None = None,
103
+ ) -> dict[str, Any]:
104
+ """Create a test analysis result.
105
+
106
+ Args:
107
+ is_phishing: Whether email is classified as phishing
108
+ confidence: Confidence score (0.0-1.0)
109
+ threat_level: Threat level (SAFE, LOW, MEDIUM, HIGH, CRITICAL)
110
+ indicators: List of indicators (created if None)
111
+
112
+ Returns:
113
+ Analysis result data dictionary
114
+ """
115
+ if indicators is None:
116
+ indicators = []
117
+ if is_phishing:
118
+ indicators.append(create_test_indicator())
119
+
120
+ return {
121
+ "is_phishing": is_phishing,
122
+ "confidence": confidence,
123
+ "threat_level": threat_level,
124
+ "indicators": indicators,
125
+ "metadata": {
126
+ "analyzer_id": "test-analyzer",
127
+ "analyzer_version": "1.0.0",
128
+ "analysis_timestamp": datetime.now(timezone.utc).isoformat(),
129
+ "processing_time_ms": 100,
130
+ },
131
+ "explanation": "Test analysis result",
132
+ "raw_output": {},
133
+ }
134
+
135
+
136
+ def create_analysis_request(
137
+ email: dict[str, Any] | None = None,
138
+ workflow_id: str = "default-workflow",
139
+ ) -> dict[str, Any]:
140
+ """Create an analysis request.
141
+
142
+ Args:
143
+ email: Email data dictionary (created if None)
144
+ workflow_id: Workflow ID to use
145
+
146
+ Returns:
147
+ Analysis request dictionary
148
+ """
149
+ return {
150
+ "email": email or create_test_email(),
151
+ "workflow_id": workflow_id,
152
+ }
153
+
154
+
155
+ def create_test_user(
156
+ username: str = "testuser",
157
+ email: str = "testuser@test.com",
158
+ is_active: bool = True,
159
+ is_superuser: bool = False,
160
+ ) -> dict[str, Any]:
161
+ """Create test user data.
162
+
163
+ Args:
164
+ username: Username
165
+ email: Email address
166
+ is_active: Whether user is active
167
+ is_superuser: Whether user is superuser
168
+
169
+ Returns:
170
+ User data dictionary
171
+ """
172
+ return {
173
+ "username": username,
174
+ "email": email,
175
+ "is_active": is_active,
176
+ "is_superuser": is_superuser,
177
+ }
@@ -0,0 +1,105 @@
1
+ """In-memory fake of :class:`spot_sdk.knowledge.KnowledgeClient` for tests.
2
+
3
+ No HTTP, no embedding, no vector math — just a dict keyed by ``id``
4
+ that supports the same upsert/fetch/delete methods. ``fetch()`` does
5
+ naive substring matching over content + tag-expression filtering, which
6
+ is enough for plugin unit tests to assert that an analyzer reads
7
+ documents out of the store.
8
+
9
+ Usage::
10
+
11
+ from spot_sdk.testing.fake_knowledge_client import FakeKnowledgeClient
12
+
13
+ kb = FakeKnowledgeClient()
14
+ await kb.upsert(KnowledgeDocument(id="...", content="...", tags=[...]))
15
+ docs = await kb.fetch(text="alice", tags="employee")
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import re
21
+ from typing import Any
22
+
23
+ from ..knowledge import KnowledgeDocument
24
+
25
+
26
+ class FakeKnowledgeClient:
27
+ """In-memory fake compatible with :class:`KnowledgeClient`."""
28
+
29
+ def __init__(self, retrieval_limits: dict[str, Any] | None = None) -> None:
30
+ self._docs: dict[str, KnowledgeDocument] = {}
31
+ self._limits = dict(retrieval_limits or {})
32
+
33
+ # --- write side ---------------------------------------------------- #
34
+
35
+ async def upsert(self, doc: KnowledgeDocument) -> None:
36
+ self._docs[doc.id] = doc
37
+
38
+ async def bulk_upsert(self, docs: list[KnowledgeDocument]) -> None:
39
+ for d in docs:
40
+ self._docs[d.id] = d
41
+
42
+ async def delete(self, doc_id: str) -> None:
43
+ self._docs.pop(doc_id, None)
44
+
45
+ # --- read side ----------------------------------------------------- #
46
+
47
+ async def fetch(
48
+ self,
49
+ tags: str | None = None,
50
+ text: str | None = None,
51
+ top_k: int = 5,
52
+ min_score: float = 0.0,
53
+ strategy: str = "similarity",
54
+ ) -> list[KnowledgeDocument]:
55
+ if text is None or not text.strip():
56
+ raise ValueError("fetch() requires a non-empty 'text' argument")
57
+
58
+ # Apply caps
59
+ max_top_k = self._limits.get("max_top_k")
60
+ if max_top_k is not None and top_k > max_top_k:
61
+ top_k = max_top_k
62
+ floor = self._limits.get("min_score_floor")
63
+ if floor is not None and min_score < floor:
64
+ min_score = floor
65
+
66
+ # Tag filter
67
+ candidates = list(self._docs.values())
68
+ if tags:
69
+ candidates = [d for d in candidates if _match_tag_expr(d.tags, tags)]
70
+
71
+ # Scored by naive substring overlap (good enough for tests)
72
+ terms = [t for t in re.split(r"\s+", text.lower()) if t]
73
+ scored: list[tuple[float, KnowledgeDocument]] = []
74
+ for d in candidates:
75
+ content_lower = d.content.lower()
76
+ hits = sum(1 for t in terms if t in content_lower)
77
+ score = hits / max(len(terms), 1)
78
+ if score >= min_score:
79
+ scored.append((score, d.model_copy(update={"score": score})))
80
+
81
+ scored.sort(key=lambda x: x[0], reverse=True)
82
+ return [d for _, d in scored[:top_k]]
83
+
84
+ # --- test helpers -------------------------------------------------- #
85
+
86
+ def all(self) -> list[KnowledgeDocument]:
87
+ """Return all stored documents (test helper)."""
88
+ return list(self._docs.values())
89
+
90
+ def clear(self) -> None:
91
+ """Remove all stored documents (test helper)."""
92
+ self._docs.clear()
93
+
94
+
95
+ # Same tiny tag-expression evaluator used by the real store.
96
+ # Grammar: term (op term)* where op is '+' (AND) or '|' (OR).
97
+ # AND binds tighter than OR; no parentheses.
98
+ def _match_tag_expr(doc_tags: list[str], expr: str) -> bool:
99
+ tag_set = set(doc_tags)
100
+ or_groups = expr.split("|")
101
+ for group in or_groups:
102
+ and_terms = [t.strip() for t in group.split("+") if t.strip()]
103
+ if and_terms and all(t in tag_set for t in and_terms):
104
+ return True
105
+ return False
@@ -0,0 +1,33 @@
1
+ """Standardized threat level computation for SPOT analyzers."""
2
+
3
+ from .analyzer import ThreatLevel
4
+
5
+
6
+ def confidence_to_threat_level(
7
+ confidence: float,
8
+ *,
9
+ high: float = 0.8,
10
+ medium: float = 0.5,
11
+ low: float = 0.2,
12
+ ) -> ThreatLevel:
13
+ """Map a confidence score to a ThreatLevel.
14
+
15
+ Uses consistent default thresholds across all analyzers.
16
+ Individual analyzers can override thresholds via keyword arguments.
17
+
18
+ Args:
19
+ confidence: Confidence score in [0.0, 1.0].
20
+ high: Threshold for HIGH (default 0.8).
21
+ medium: Threshold for MEDIUM (default 0.5).
22
+ low: Threshold for LOW (default 0.2).
23
+
24
+ Returns:
25
+ The corresponding ThreatLevel.
26
+ """
27
+ if confidence >= high:
28
+ return ThreatLevel.HIGH
29
+ if confidence >= medium:
30
+ return ThreatLevel.MEDIUM
31
+ if confidence >= low:
32
+ return ThreatLevel.LOW
33
+ return ThreatLevel.SAFE