iflow-mcp-m507_ai-soc-agent 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.
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/METADATA +410 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/RECORD +85 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/WHEEL +5 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_m507_ai_soc_agent-1.0.0.dist-info/top_level.txt +1 -0
- src/__init__.py +8 -0
- src/ai_controller/README.md +139 -0
- src/ai_controller/__init__.py +12 -0
- src/ai_controller/agent_executor.py +596 -0
- src/ai_controller/cli/__init__.py +2 -0
- src/ai_controller/cli/main.py +243 -0
- src/ai_controller/session_manager.py +409 -0
- src/ai_controller/web/__init__.py +2 -0
- src/ai_controller/web/server.py +1181 -0
- src/ai_controller/web/static/css/README.md +102 -0
- src/api/__init__.py +13 -0
- src/api/case_management.py +271 -0
- src/api/edr.py +187 -0
- src/api/kb.py +136 -0
- src/api/siem.py +308 -0
- src/core/__init__.py +10 -0
- src/core/config.py +242 -0
- src/core/config_storage.py +684 -0
- src/core/dto.py +50 -0
- src/core/errors.py +36 -0
- src/core/logging.py +128 -0
- src/integrations/__init__.py +8 -0
- src/integrations/case_management/__init__.py +5 -0
- src/integrations/case_management/iris/__init__.py +11 -0
- src/integrations/case_management/iris/iris_client.py +885 -0
- src/integrations/case_management/iris/iris_http.py +274 -0
- src/integrations/case_management/iris/iris_mapper.py +263 -0
- src/integrations/case_management/iris/iris_models.py +128 -0
- src/integrations/case_management/thehive/__init__.py +8 -0
- src/integrations/case_management/thehive/thehive_client.py +193 -0
- src/integrations/case_management/thehive/thehive_http.py +147 -0
- src/integrations/case_management/thehive/thehive_mapper.py +190 -0
- src/integrations/case_management/thehive/thehive_models.py +125 -0
- src/integrations/cti/__init__.py +6 -0
- src/integrations/cti/local_tip/__init__.py +10 -0
- src/integrations/cti/local_tip/local_tip_client.py +90 -0
- src/integrations/cti/local_tip/local_tip_http.py +110 -0
- src/integrations/cti/opencti/__init__.py +10 -0
- src/integrations/cti/opencti/opencti_client.py +101 -0
- src/integrations/cti/opencti/opencti_http.py +418 -0
- src/integrations/edr/__init__.py +6 -0
- src/integrations/edr/elastic_defend/__init__.py +6 -0
- src/integrations/edr/elastic_defend/elastic_defend_client.py +351 -0
- src/integrations/edr/elastic_defend/elastic_defend_http.py +162 -0
- src/integrations/eng/__init__.py +10 -0
- src/integrations/eng/clickup/__init__.py +8 -0
- src/integrations/eng/clickup/clickup_client.py +513 -0
- src/integrations/eng/clickup/clickup_http.py +156 -0
- src/integrations/eng/github/__init__.py +8 -0
- src/integrations/eng/github/github_client.py +169 -0
- src/integrations/eng/github/github_http.py +158 -0
- src/integrations/eng/trello/__init__.py +8 -0
- src/integrations/eng/trello/trello_client.py +207 -0
- src/integrations/eng/trello/trello_http.py +162 -0
- src/integrations/kb/__init__.py +12 -0
- src/integrations/kb/fs_kb_client.py +313 -0
- src/integrations/siem/__init__.py +6 -0
- src/integrations/siem/elastic/__init__.py +6 -0
- src/integrations/siem/elastic/elastic_client.py +3319 -0
- src/integrations/siem/elastic/elastic_http.py +165 -0
- src/mcp/README.md +183 -0
- src/mcp/TOOLS.md +2827 -0
- src/mcp/__init__.py +13 -0
- src/mcp/__main__.py +18 -0
- src/mcp/agent_profiles.py +408 -0
- src/mcp/flow_agent_profiles.py +424 -0
- src/mcp/mcp_server.py +4086 -0
- src/mcp/rules_engine.py +487 -0
- src/mcp/runbook_manager.py +264 -0
- src/orchestrator/__init__.py +11 -0
- src/orchestrator/incident_workflow.py +244 -0
- src/orchestrator/tools_case.py +1085 -0
- src/orchestrator/tools_cti.py +359 -0
- src/orchestrator/tools_edr.py +315 -0
- src/orchestrator/tools_eng.py +378 -0
- src/orchestrator/tools_kb.py +156 -0
- src/orchestrator/tools_siem.py +1709 -0
- src/web/__init__.py +8 -0
- src/web/config_server.py +511 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Low-level HTTP client for Trello API.
|
|
3
|
+
|
|
4
|
+
This module is responsible for:
|
|
5
|
+
- authentication (API key and token)
|
|
6
|
+
- building URLs
|
|
7
|
+
- making HTTP requests
|
|
8
|
+
- basic error handling
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Any, Dict, Optional
|
|
16
|
+
|
|
17
|
+
import requests
|
|
18
|
+
|
|
19
|
+
from ....core.errors import IntegrationError
|
|
20
|
+
from ....core.logging import get_logger
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
logger = get_logger("sami.integrations.trello.http")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class TrelloHttpClient:
|
|
28
|
+
"""
|
|
29
|
+
Simple HTTP client for Trello's REST API.
|
|
30
|
+
|
|
31
|
+
Trello API documentation: https://developer.atlassian.com/cloud/trello/guides/rest-api/api-introduction/
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
api_key: str
|
|
35
|
+
api_token: str
|
|
36
|
+
timeout_seconds: int = 30
|
|
37
|
+
verify_ssl: bool = True
|
|
38
|
+
|
|
39
|
+
def _auth_params(self) -> Dict[str, str]:
|
|
40
|
+
"""Get authentication query parameters."""
|
|
41
|
+
return {
|
|
42
|
+
"key": self.api_key,
|
|
43
|
+
"token": self.api_token,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
def _build_url(self, endpoint: str) -> str:
|
|
47
|
+
"""
|
|
48
|
+
Build a full URL from an endpoint.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
endpoint: API endpoint path (e.g., "/1/boards/{boardId}/cards")
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Full URL string
|
|
55
|
+
"""
|
|
56
|
+
base_url = "https://api.trello.com"
|
|
57
|
+
endpoint = endpoint.lstrip("/")
|
|
58
|
+
return f"{base_url}/{endpoint}"
|
|
59
|
+
|
|
60
|
+
def _handle_trello_error(self, response: requests.Response) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Raise IntegrationError if the response indicates an error.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
response: HTTP response object
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
IntegrationError: If the response indicates an error
|
|
69
|
+
"""
|
|
70
|
+
if response.status_code < 400:
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
error_data = response.json()
|
|
75
|
+
message = error_data.get("message", f"HTTP {response.status_code}")
|
|
76
|
+
except Exception:
|
|
77
|
+
message = f"HTTP {response.status_code}: {response.text[:200]}"
|
|
78
|
+
|
|
79
|
+
raise IntegrationError(f"Trello API error: {message}")
|
|
80
|
+
|
|
81
|
+
def request(
|
|
82
|
+
self,
|
|
83
|
+
method: str,
|
|
84
|
+
endpoint: str,
|
|
85
|
+
json_data: Optional[Dict[str, Any]] = None,
|
|
86
|
+
params: Optional[Dict[str, Any]] = None,
|
|
87
|
+
) -> Dict[str, Any]:
|
|
88
|
+
"""
|
|
89
|
+
Make an HTTP request to Trello API.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
method: HTTP method (GET, POST, PUT, PATCH, DELETE)
|
|
93
|
+
endpoint: API endpoint path
|
|
94
|
+
json_data: JSON payload (for POST, PUT, PATCH)
|
|
95
|
+
params: Query parameters (merged with auth params)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Response JSON as dictionary
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
IntegrationError: If the request fails
|
|
102
|
+
"""
|
|
103
|
+
url = self._build_url(endpoint)
|
|
104
|
+
headers = {
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
"Accept": "application/json",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# Merge auth params with provided params
|
|
110
|
+
all_params = self._auth_params().copy()
|
|
111
|
+
if params:
|
|
112
|
+
all_params.update(params)
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
logger.debug(f"Trello {method} {url}")
|
|
116
|
+
if all_params:
|
|
117
|
+
logger.debug(f" Query params: {all_params}")
|
|
118
|
+
if json_data:
|
|
119
|
+
logger.debug(f" JSON payload: {json.dumps(json_data)[:200]}...")
|
|
120
|
+
|
|
121
|
+
response = requests.request(
|
|
122
|
+
method=method,
|
|
123
|
+
url=url,
|
|
124
|
+
headers=headers,
|
|
125
|
+
json=json_data,
|
|
126
|
+
params=all_params,
|
|
127
|
+
timeout=self.timeout_seconds,
|
|
128
|
+
verify=self.verify_ssl,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
logger.debug(f"Trello response status: {response.status_code}")
|
|
132
|
+
if response.status_code >= 400:
|
|
133
|
+
logger.error(f"Trello API error - Status: {response.status_code}, URL: {url}, Response: {response.text[:500]}")
|
|
134
|
+
|
|
135
|
+
self._handle_trello_error(response)
|
|
136
|
+
|
|
137
|
+
if response.status_code == 204: # No Content
|
|
138
|
+
return {}
|
|
139
|
+
|
|
140
|
+
return response.json()
|
|
141
|
+
|
|
142
|
+
except requests.exceptions.Timeout as e:
|
|
143
|
+
raise IntegrationError(f"Trello API request timeout: {e}") from e
|
|
144
|
+
except requests.exceptions.RequestException as e:
|
|
145
|
+
raise IntegrationError(f"Trello API request failed: {e}") from e
|
|
146
|
+
|
|
147
|
+
def get(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
148
|
+
"""GET request."""
|
|
149
|
+
return self.request("GET", endpoint, params=params)
|
|
150
|
+
|
|
151
|
+
def post(self, endpoint: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
152
|
+
"""POST request."""
|
|
153
|
+
return self.request("POST", endpoint, json_data=json_data, params=params)
|
|
154
|
+
|
|
155
|
+
def put(self, endpoint: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
156
|
+
"""PUT request."""
|
|
157
|
+
return self.request("PUT", endpoint, json_data=json_data, params=params)
|
|
158
|
+
|
|
159
|
+
def delete(self, endpoint: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
160
|
+
"""DELETE request."""
|
|
161
|
+
return self.request("DELETE", endpoint, params=params)
|
|
162
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Knowledge base integrations for SamiGPT.
|
|
3
|
+
|
|
4
|
+
Currently this provides a filesystem-backed KB client that reads client
|
|
5
|
+
infrastructure descriptions from ``client_env/*`` in the project root.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .fs_kb_client import FileSystemKBClient
|
|
9
|
+
|
|
10
|
+
__all__ = ["FileSystemKBClient"]
|
|
11
|
+
|
|
12
|
+
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Filesystem-backed KB client.
|
|
3
|
+
|
|
4
|
+
This client reads client infrastructure knowledge from JSON files under
|
|
5
|
+
``client_env/*`` in the project root and exposes it via the KBClient API.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from typing import List, Optional
|
|
13
|
+
|
|
14
|
+
from ...core.errors import IntegrationError
|
|
15
|
+
from ...api.kb import (
|
|
16
|
+
KBClient,
|
|
17
|
+
KBClientInfra,
|
|
18
|
+
KBSubnet,
|
|
19
|
+
KBServer,
|
|
20
|
+
KBUser,
|
|
21
|
+
KBDeviceSchema,
|
|
22
|
+
KBUserSchema,
|
|
23
|
+
KBEnvRules,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FileSystemKBClient(KBClient):
|
|
28
|
+
"""
|
|
29
|
+
Knowledge base client that reads from local JSON files.
|
|
30
|
+
|
|
31
|
+
Folder layout (relative to project root):
|
|
32
|
+
- client_env/
|
|
33
|
+
- all_clients/ # generic templates (not treated as a client)
|
|
34
|
+
- env_rules.json # shared rules across all clients
|
|
35
|
+
- <client_name>_client/
|
|
36
|
+
- internal_subnets.json
|
|
37
|
+
- internal_servers.json
|
|
38
|
+
- internal_users.json
|
|
39
|
+
- naming_schemas.json
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, client_env_dir: Optional[str] = None) -> None:
|
|
43
|
+
# Infer project root from this file path
|
|
44
|
+
if client_env_dir is None:
|
|
45
|
+
project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
|
46
|
+
client_env_dir = os.path.join(project_root, "client_env")
|
|
47
|
+
|
|
48
|
+
self.client_env_dir = client_env_dir
|
|
49
|
+
|
|
50
|
+
# ------------------------------------------------------------------
|
|
51
|
+
# KBClient interface
|
|
52
|
+
# ------------------------------------------------------------------
|
|
53
|
+
def list_clients(self) -> List[str]:
|
|
54
|
+
"""List available client environment folders."""
|
|
55
|
+
if not os.path.isdir(self.client_env_dir):
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
clients: List[str] = []
|
|
59
|
+
for entry in os.listdir(self.client_env_dir):
|
|
60
|
+
full_path = os.path.join(self.client_env_dir, entry)
|
|
61
|
+
if not os.path.isdir(full_path):
|
|
62
|
+
continue
|
|
63
|
+
if entry == "all_clients":
|
|
64
|
+
continue
|
|
65
|
+
# Conventionally treat folders ending with _client as full environments
|
|
66
|
+
if entry.endswith("_client"):
|
|
67
|
+
clients.append(entry)
|
|
68
|
+
clients.sort()
|
|
69
|
+
return clients
|
|
70
|
+
|
|
71
|
+
def get_client_infra(self, client_name: str) -> KBClientInfra:
|
|
72
|
+
"""Load and aggregate infrastructure knowledge for a specific client."""
|
|
73
|
+
# Allow passing bare client name without _client suffix
|
|
74
|
+
if not client_name.endswith("_client"):
|
|
75
|
+
candidate = f"{client_name}_client"
|
|
76
|
+
else:
|
|
77
|
+
candidate = client_name
|
|
78
|
+
|
|
79
|
+
client_dir = os.path.join(self.client_env_dir, candidate)
|
|
80
|
+
if not os.path.isdir(client_dir):
|
|
81
|
+
raise IntegrationError(
|
|
82
|
+
f"Client environment not found: {client_name} "
|
|
83
|
+
f"(looked in {client_dir})"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
subnets = self._load_subnets(client_dir)
|
|
87
|
+
servers = self._load_servers(client_dir)
|
|
88
|
+
users = self._load_users(client_dir)
|
|
89
|
+
device_schemas, user_schemas = self._load_naming_schemas(client_dir)
|
|
90
|
+
env_rules = self._load_env_rules()
|
|
91
|
+
|
|
92
|
+
summary = self._build_summary(
|
|
93
|
+
client_name=candidate,
|
|
94
|
+
subnets=subnets,
|
|
95
|
+
servers=servers,
|
|
96
|
+
users=users,
|
|
97
|
+
device_schemas=device_schemas,
|
|
98
|
+
user_schemas=user_schemas,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
return KBClientInfra(
|
|
102
|
+
client_name=candidate,
|
|
103
|
+
subnets=subnets,
|
|
104
|
+
servers=servers,
|
|
105
|
+
users=users,
|
|
106
|
+
device_schemas=device_schemas,
|
|
107
|
+
user_schemas=user_schemas,
|
|
108
|
+
env_rules=env_rules,
|
|
109
|
+
summary=summary,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
# Helpers for loading JSON and building DTOs
|
|
114
|
+
# ------------------------------------------------------------------
|
|
115
|
+
def _load_json(self, path: str) -> Optional[dict]:
|
|
116
|
+
if not os.path.exists(path):
|
|
117
|
+
return None
|
|
118
|
+
try:
|
|
119
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
120
|
+
return json.load(f)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
raise IntegrationError(f"Failed to load JSON from {path}: {e}") from e
|
|
123
|
+
|
|
124
|
+
def _load_subnets(self, client_dir: str) -> List[KBSubnet]:
|
|
125
|
+
data = self._load_json(os.path.join(client_dir, "internal_subnets.json")) or {}
|
|
126
|
+
raw_subnets = data.get("subnets", [])
|
|
127
|
+
subnets: List[KBSubnet] = []
|
|
128
|
+
for s in raw_subnets:
|
|
129
|
+
subnets.append(
|
|
130
|
+
KBSubnet(
|
|
131
|
+
name=s.get("name", ""),
|
|
132
|
+
cidr=s.get("cidr", ""),
|
|
133
|
+
network_type=s.get("network_type"),
|
|
134
|
+
access_method=s.get("access_method"),
|
|
135
|
+
description=s.get("description"),
|
|
136
|
+
tags=s.get("tags"),
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
return subnets
|
|
140
|
+
|
|
141
|
+
def _load_servers(self, client_dir: str) -> List[KBServer]:
|
|
142
|
+
data = self._load_json(os.path.join(client_dir, "internal_servers.json")) or {}
|
|
143
|
+
raw_servers = data.get("servers", [])
|
|
144
|
+
servers: List[KBServer] = []
|
|
145
|
+
for s in raw_servers:
|
|
146
|
+
servers.append(
|
|
147
|
+
KBServer(
|
|
148
|
+
hostname=s.get("hostname", ""),
|
|
149
|
+
ip_address=s.get("ip_address"),
|
|
150
|
+
role=s.get("role"),
|
|
151
|
+
environment=s.get("environment"),
|
|
152
|
+
os=s.get("os"),
|
|
153
|
+
description=s.get("description"),
|
|
154
|
+
criticality=s.get("criticality"),
|
|
155
|
+
tags=s.get("tags"),
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
return servers
|
|
159
|
+
|
|
160
|
+
def _load_users(self, client_dir: str) -> List[KBUser]:
|
|
161
|
+
data = self._load_json(os.path.join(client_dir, "internal_users.json")) or {}
|
|
162
|
+
raw_users = data.get("users", [])
|
|
163
|
+
users: List[KBUser] = []
|
|
164
|
+
for u in raw_users:
|
|
165
|
+
users.append(
|
|
166
|
+
KBUser(
|
|
167
|
+
username=u.get("username", ""),
|
|
168
|
+
display_name=u.get("display_name"),
|
|
169
|
+
account_type=u.get("account_type"),
|
|
170
|
+
department=u.get("department"),
|
|
171
|
+
privilege_level=u.get("privilege_level"),
|
|
172
|
+
description=u.get("description"),
|
|
173
|
+
tags=u.get("tags"),
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
return users
|
|
177
|
+
|
|
178
|
+
def _load_naming_schemas(
|
|
179
|
+
self, client_dir: str
|
|
180
|
+
) -> tuple[List[KBDeviceSchema], List[KBUserSchema]]:
|
|
181
|
+
data = self._load_json(os.path.join(client_dir, "naming_schemas.json")) or {}
|
|
182
|
+
raw_device_schemas = data.get("device_schemas", [])
|
|
183
|
+
raw_user_schemas = data.get("user_schemas", [])
|
|
184
|
+
|
|
185
|
+
device_schemas: List[KBDeviceSchema] = []
|
|
186
|
+
for d in raw_device_schemas:
|
|
187
|
+
# Skip entries missing required fields
|
|
188
|
+
if not d.get("pattern") or not d.get("device_type"):
|
|
189
|
+
continue
|
|
190
|
+
device_schemas.append(
|
|
191
|
+
KBDeviceSchema(
|
|
192
|
+
pattern=d.get("pattern", ""),
|
|
193
|
+
pattern_style=d.get("pattern_style", "regex"),
|
|
194
|
+
device_type=d.get("device_type", ""),
|
|
195
|
+
example=d.get("example"),
|
|
196
|
+
description=d.get("description"),
|
|
197
|
+
tags=d.get("tags"),
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
user_schemas: List[KBUserSchema] = []
|
|
202
|
+
for u in raw_user_schemas:
|
|
203
|
+
if not u.get("pattern") or not u.get("user_type"):
|
|
204
|
+
continue
|
|
205
|
+
user_schemas.append(
|
|
206
|
+
KBUserSchema(
|
|
207
|
+
pattern=u.get("pattern", ""),
|
|
208
|
+
pattern_style=u.get("pattern_style", "regex"),
|
|
209
|
+
user_type=u.get("user_type", ""),
|
|
210
|
+
example=u.get("example"),
|
|
211
|
+
description=u.get("description"),
|
|
212
|
+
tags=u.get("tags"),
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return device_schemas, user_schemas
|
|
217
|
+
|
|
218
|
+
def _load_env_rules(self) -> Optional[KBEnvRules]:
|
|
219
|
+
data = self._load_json(os.path.join(self.client_env_dir, "env_rules.json"))
|
|
220
|
+
if not data:
|
|
221
|
+
return None
|
|
222
|
+
return KBEnvRules(
|
|
223
|
+
version=data.get("version"),
|
|
224
|
+
environment_types=data.get("environment_types"),
|
|
225
|
+
network_classification=data.get("network_classification"),
|
|
226
|
+
user_categories=data.get("user_categories"),
|
|
227
|
+
general_rules=data.get("general_rules"),
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# ------------------------------------------------------------------
|
|
231
|
+
# Summary builder
|
|
232
|
+
# ------------------------------------------------------------------
|
|
233
|
+
def _build_summary(
|
|
234
|
+
self,
|
|
235
|
+
client_name: str,
|
|
236
|
+
subnets: List[KBSubnet],
|
|
237
|
+
servers: List[KBServer],
|
|
238
|
+
users: List[KBUser],
|
|
239
|
+
device_schemas: List[KBDeviceSchema],
|
|
240
|
+
user_schemas: List[KBUserSchema],
|
|
241
|
+
) -> str:
|
|
242
|
+
"""Build a concise, human-readable summary of the client environment."""
|
|
243
|
+
|
|
244
|
+
total_subnets = len(subnets)
|
|
245
|
+
total_servers = len(servers)
|
|
246
|
+
total_users = len(users)
|
|
247
|
+
|
|
248
|
+
# Count by simple categories
|
|
249
|
+
internal_subnets = [
|
|
250
|
+
s for s in subnets if (s.network_type or "").lower() in {"internal", "lan"}
|
|
251
|
+
]
|
|
252
|
+
vpn_subnets = [
|
|
253
|
+
s for s in subnets
|
|
254
|
+
if "vpn" in (s.tags or []) or "vpn" in (s.description or "").lower()
|
|
255
|
+
]
|
|
256
|
+
guest_subnets = [
|
|
257
|
+
s for s in subnets
|
|
258
|
+
if (s.network_type or "").lower() == "guest"
|
|
259
|
+
or "guest" in (s.tags or [])
|
|
260
|
+
]
|
|
261
|
+
dmz_subnets = [
|
|
262
|
+
s for s in subnets
|
|
263
|
+
if (s.network_type or "").lower() == "dmz"
|
|
264
|
+
or "dmz" in (s.tags or [])
|
|
265
|
+
]
|
|
266
|
+
|
|
267
|
+
admin_users = [
|
|
268
|
+
u for u in users
|
|
269
|
+
if (u.account_type or "").lower() == "administrator"
|
|
270
|
+
or (u.privilege_level or "").lower() in {"admin", "high"}
|
|
271
|
+
]
|
|
272
|
+
contractor_users = [
|
|
273
|
+
u for u in users if (u.account_type or "").lower() == "contractor"
|
|
274
|
+
]
|
|
275
|
+
service_accounts = [
|
|
276
|
+
u for u in users if (u.account_type or "").lower() in {"service", "service_account"}
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
lines: List[str] = []
|
|
280
|
+
lines.append(f"Client '{client_name}' infrastructure overview:")
|
|
281
|
+
lines.append(
|
|
282
|
+
f"- {total_subnets} subnets "
|
|
283
|
+
f"({len(internal_subnets)} internal, "
|
|
284
|
+
f"{len(vpn_subnets)} VPN, "
|
|
285
|
+
f"{len(dmz_subnets)} DMZ, "
|
|
286
|
+
f"{len(guest_subnets)} guest)"
|
|
287
|
+
)
|
|
288
|
+
lines.append(
|
|
289
|
+
f"- {total_servers} servers (examples: "
|
|
290
|
+
+ ", ".join(s.hostname for s in servers[:3])
|
|
291
|
+
+ (", ..." if total_servers > 3 else ")")
|
|
292
|
+
)
|
|
293
|
+
lines.append(
|
|
294
|
+
f"- {total_users} user accounts "
|
|
295
|
+
f"({len(admin_users)} admin, "
|
|
296
|
+
f"{len(service_accounts)} service, "
|
|
297
|
+
f"{len(contractor_users)} contractor)"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
if device_schemas:
|
|
301
|
+
device_types = sorted({d.device_type for d in device_schemas})
|
|
302
|
+
lines.append(
|
|
303
|
+
"- Device naming schemas define types: " + ", ".join(device_types)
|
|
304
|
+
)
|
|
305
|
+
if user_schemas:
|
|
306
|
+
user_types = sorted({u.user_type for u in user_schemas})
|
|
307
|
+
lines.append(
|
|
308
|
+
"- User naming schemas define categories: " + ", ".join(user_types)
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
return "\n".join(lines)
|
|
312
|
+
|
|
313
|
+
|