workspaces-euc-mcp-server 0.1.5__tar.gz → 0.1.7__tar.gz
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.
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/CHANGELOG.md +22 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/DESIGN.md +3 -3
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/PKG-INFO +4 -4
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/README.md +3 -3
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/iam/tier0-diagnostics.json +3 -1
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/iam/tier1-cost.json +3 -1
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/iam/tier2-lifecycle.json +3 -1
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/iam/tier3-destructive.json +3 -1
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/pyproject.toml +1 -1
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/test_diagnostics.py +39 -0
- workspaces_euc_mcp_server-0.1.7/tests/test_secure_browser.py +144 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/__init__.py +1 -1
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/models.py +36 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/tools/diagnostics.py +14 -1
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/tools/secure_browser.py +114 -18
- workspaces_euc_mcp_server-0.1.5/tests/test_secure_browser.py +0 -85
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/.dockerignore +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/.github/workflows/ci.yml +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/.github/workflows/docker-publish.yml +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/.github/workflows/publish.yml +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/.gitignore +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/.pre-commit-config.yaml +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/Dockerfile +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/LICENSE +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/iam/README.md +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/scripts/smoke_readonly.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/__init__.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/test_clients.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/test_cost.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/test_destructive.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/test_governance.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/test_images.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/test_inventory.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/test_lifecycle.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/test_naming.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/test_no_embedded_secrets.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/test_performance.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/test_pricing.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/test_reporting.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/clients.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/consts.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/server.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/tools/__init__.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/tools/_common.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/tools/cost.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/tools/destructive.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/tools/governance.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/tools/images.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/tools/inventory.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/tools/lifecycle.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/tools/performance.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/tools/pricing.py +0 -0
- {workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/workspaces_euc_mcp_server/tools/reporting.py +0 -0
|
@@ -5,6 +5,28 @@ All notable changes to this project are documented here. The format is based on
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.1.7] - 2026-06-02
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `get_secure_browser_portal_usage` now reports **current active sessions live via
|
|
12
|
+
`workspaces-web:ListSessions`** (the same source as the console's active-sessions view) instead
|
|
13
|
+
of inferring "active" from CloudWatch. CloudWatch (`AWS/WorkSpacesWeb`) is now used **only for
|
|
14
|
+
historic** metrics, and the summary clearly separates live vs historic. Adds
|
|
15
|
+
`workspaces-web:ListSessions` to every IAM tier. The tool now returns a `SecureBrowserPortalUsage`
|
|
16
|
+
result (`active_session_count`, `active_sessions`, `historic_metrics`).
|
|
17
|
+
|
|
18
|
+
## [0.1.6] - 2026-06-02
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- `check_directory_health` now surfaces each directory's **registration properties** in its
|
|
22
|
+
signals — notably the target **OU** (`WorkspaceCreationProperties.DefaultOu`), plus custom
|
|
23
|
+
security group, local-admin / internet-access / maintenance-mode flags, and directory/workspace
|
|
24
|
+
type. (AD-backed directories carry an OU; WorkSpaces-managed ones return none.)
|
|
25
|
+
- `get_secure_browser_portal_details` now resolves the portal's **data-protection configuration**
|
|
26
|
+
when attached: the redacted built-in/custom inline-redaction patterns, global confidence level,
|
|
27
|
+
and enforced/exempt URLs — not just whether data protection is on. Adds
|
|
28
|
+
`workspaces-web:GetDataProtectionSettings` to every IAM tier.
|
|
29
|
+
|
|
8
30
|
## [0.1.5] - 2026-06-02
|
|
9
31
|
|
|
10
32
|
### Added
|
|
@@ -117,7 +117,7 @@ and return a synthesized result, not raw API passthroughs.
|
|
|
117
117
|
| `diagnose_workspace_connectivity` | Correlate a Personal WorkSpace's state + connection status + directory health + CloudWatch into a root-cause narrative | `workspaces:Describe*`, `ds:DescribeDirectories`, `cloudwatch:GetMetricData` |
|
|
118
118
|
| `diagnose_pool` | Why a Pool is unhealthy/queued — capacity status, sessions, errors, scaling | `workspaces:DescribeWorkspacesPool*`, `cloudwatch:GetMetricData` |
|
|
119
119
|
| `diagnose_application_fleet` | Fleet state, capacity, scaling activity, fleet errors | `appstream:DescribeFleets`, `appstream:ListAssociatedStacks`, `cloudwatch:GetMetricData`, `application-autoscaling:DescribeScalingActivities` |
|
|
120
|
-
| `check_directory_health` | Shared dependency: directory reachability/registration (skips `ds` for WorkSpaces-managed `wsd-` directories
|
|
120
|
+
| `check_directory_health` | Shared dependency: directory reachability/registration + registration properties (target OU, security group, flags); skips `ds` for WorkSpaces-managed `wsd-` directories | `ds:DescribeDirectories`, `workspaces:DescribeWorkspaceDirectories` |
|
|
121
121
|
|
|
122
122
|
**Cost, utilization & performance**
|
|
123
123
|
| Tool | Purpose | IAM actions |
|
|
@@ -134,8 +134,8 @@ and return a synthesized result, not raw API passthroughs.
|
|
|
134
134
|
**Secure Browser**
|
|
135
135
|
| Tool | Purpose | IAM actions |
|
|
136
136
|
|---|---|---|
|
|
137
|
-
| `get_secure_browser_portal_details` | Portal config + associated settings (browser/network/user/IP-access) | `workspaces-web:GetPortal`, `workspaces-web:List*`, `workspaces-web:Get*Settings` |
|
|
138
|
-
| `get_secure_browser_portal_usage` |
|
|
137
|
+
| `get_secure_browser_portal_details` | Portal config + associated settings (browser/network/user/IP-access) + resolved data-protection redaction config | `workspaces-web:GetPortal`, `workspaces-web:List*`, `workspaces-web:Get*Settings`, `workspaces-web:GetDataProtectionSettings` |
|
|
138
|
+
| `get_secure_browser_portal_usage` | Current active sessions (live, `ListSessions`) + historic CloudWatch metrics | `workspaces-web:ListPortals`, `workspaces-web:ListSessions`, `cloudwatch:GetMetricData` |
|
|
139
139
|
|
|
140
140
|
**Reporting & audit**
|
|
141
141
|
| Tool | Purpose | IAM actions |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: workspaces-euc-mcp-server
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
4
4
|
Summary: MCP server for administering the Amazon WorkSpaces family of End User Computing services (Personal, Pools, Applications, Secure Browser, Core).
|
|
5
5
|
Project-URL: Homepage, https://github.com/bengroeneveldsg/aws-workspaces-euc-mcp
|
|
6
6
|
Project-URL: Repository, https://github.com/bengroeneveldsg/aws-workspaces-euc-mcp
|
|
@@ -81,7 +81,7 @@ audit, and governance tools:
|
|
|
81
81
|
| `diagnose_application_fleet` | A WorkSpaces Applications fleet's health and capacity — fleet state, fleet errors, compute capacity, auto-scaling activity, and insufficient-capacity errors. |
|
|
82
82
|
| `diagnose_pool` | A WorkSpaces Pool's health — state, pool errors, user-session capacity, backing directory health, and session-utilization. |
|
|
83
83
|
| `get_application_fleet_usage` | A WorkSpaces Applications fleet's **usage history** — AWS/AppStream capacity/utilization time-series over a window, with a plain-language summary (e.g. idle running capacity) (Tier 0). |
|
|
84
|
-
| `check_directory_health` | Registration state
|
|
84
|
+
| `check_directory_health` | Registration state, AWS Directory Service stage, and **registration properties** (target **OU**, custom security group, local-admin / internet-access / maintenance-mode flags) for one or all WorkSpaces-registered directories. |
|
|
85
85
|
| `analyze_workspace_utilization` | Classifies WorkSpaces Personal desktops as unused / idle / active from the `UserConnected` metric (Tier 1). |
|
|
86
86
|
| `recommend_running_mode` | Flags AlwaysOn desktops with low usage as AutoStop candidates, with an **estimated $/mo saving** where the bundle price can be matched (Tier 1). |
|
|
87
87
|
| `get_workspace_performance` | Native CPU / memory / disk / GPU / latency / uptime metrics per desktop from `AWS/WorkSpaces` — no CloudWatch agent (Tier 0). |
|
|
@@ -94,8 +94,8 @@ audit, and governance tools:
|
|
|
94
94
|
| `audit_application_images` | Audits WorkSpaces Applications (AppStream 2.0) **images and image builders** — flags stale base images (unpatched OS), pinned/old AppStream agents, non-AVAILABLE/errored images, SHARED cross-account visibility, and **image builders left RUNNING** (cost + admin surface) (Tier 0). |
|
|
95
95
|
| `get_euc_audit_trail` | **"Who changed what"** — recent EUC management events from CloudTrail (last 90 days, no trail required) across all services; mutations-only by default, flags destructive actions and errors (e.g. AccessDenied) (Tier 0). |
|
|
96
96
|
| `get_euc_service_quotas` | **Service-quota limits + usage headroom** per EUC service; pairs limits with current usage (where AWS publishes a usage metric) to flag quotas approaching their limit — capacity planning (Tier 0). |
|
|
97
|
-
| `get_secure_browser_portal_details` | Resolves a Secure Browser portal's user settings (clipboard/print/download controls + timeouts), network,
|
|
98
|
-
| `get_secure_browser_portal_usage` | A Secure Browser portal's `AWS/WorkSpacesWeb`
|
|
97
|
+
| `get_secure_browser_portal_details` | Resolves a Secure Browser portal's user settings (clipboard/print/download controls + timeouts), network, attached policies, and — when configured — the **data-protection redaction config** (which built-in/custom patterns are redacted, confidence level, enforced/exempt URLs) (Tier 0). |
|
|
98
|
+
| `get_secure_browser_portal_usage` | A Secure Browser portal's **current active sessions** (live, via `ListSessions` — same as the console) plus **historic** `AWS/WorkSpacesWeb` metrics over a window (CloudWatch is historic-only; idle portals publish none) (Tier 0). |
|
|
99
99
|
| `list_unused_resources` | Unused WorkSpaces desktops and stopped/zero-capacity fleets worth reclaiming (Tier 0). |
|
|
100
100
|
|
|
101
101
|
Cost/utilization tools need the **Tier 1** IAM policy ([`iam/tier1-cost.json`](iam/tier1-cost.json));
|
|
@@ -51,7 +51,7 @@ audit, and governance tools:
|
|
|
51
51
|
| `diagnose_application_fleet` | A WorkSpaces Applications fleet's health and capacity — fleet state, fleet errors, compute capacity, auto-scaling activity, and insufficient-capacity errors. |
|
|
52
52
|
| `diagnose_pool` | A WorkSpaces Pool's health — state, pool errors, user-session capacity, backing directory health, and session-utilization. |
|
|
53
53
|
| `get_application_fleet_usage` | A WorkSpaces Applications fleet's **usage history** — AWS/AppStream capacity/utilization time-series over a window, with a plain-language summary (e.g. idle running capacity) (Tier 0). |
|
|
54
|
-
| `check_directory_health` | Registration state
|
|
54
|
+
| `check_directory_health` | Registration state, AWS Directory Service stage, and **registration properties** (target **OU**, custom security group, local-admin / internet-access / maintenance-mode flags) for one or all WorkSpaces-registered directories. |
|
|
55
55
|
| `analyze_workspace_utilization` | Classifies WorkSpaces Personal desktops as unused / idle / active from the `UserConnected` metric (Tier 1). |
|
|
56
56
|
| `recommend_running_mode` | Flags AlwaysOn desktops with low usage as AutoStop candidates, with an **estimated $/mo saving** where the bundle price can be matched (Tier 1). |
|
|
57
57
|
| `get_workspace_performance` | Native CPU / memory / disk / GPU / latency / uptime metrics per desktop from `AWS/WorkSpaces` — no CloudWatch agent (Tier 0). |
|
|
@@ -64,8 +64,8 @@ audit, and governance tools:
|
|
|
64
64
|
| `audit_application_images` | Audits WorkSpaces Applications (AppStream 2.0) **images and image builders** — flags stale base images (unpatched OS), pinned/old AppStream agents, non-AVAILABLE/errored images, SHARED cross-account visibility, and **image builders left RUNNING** (cost + admin surface) (Tier 0). |
|
|
65
65
|
| `get_euc_audit_trail` | **"Who changed what"** — recent EUC management events from CloudTrail (last 90 days, no trail required) across all services; mutations-only by default, flags destructive actions and errors (e.g. AccessDenied) (Tier 0). |
|
|
66
66
|
| `get_euc_service_quotas` | **Service-quota limits + usage headroom** per EUC service; pairs limits with current usage (where AWS publishes a usage metric) to flag quotas approaching their limit — capacity planning (Tier 0). |
|
|
67
|
-
| `get_secure_browser_portal_details` | Resolves a Secure Browser portal's user settings (clipboard/print/download controls + timeouts), network,
|
|
68
|
-
| `get_secure_browser_portal_usage` | A Secure Browser portal's `AWS/WorkSpacesWeb`
|
|
67
|
+
| `get_secure_browser_portal_details` | Resolves a Secure Browser portal's user settings (clipboard/print/download controls + timeouts), network, attached policies, and — when configured — the **data-protection redaction config** (which built-in/custom patterns are redacted, confidence level, enforced/exempt URLs) (Tier 0). |
|
|
68
|
+
| `get_secure_browser_portal_usage` | A Secure Browser portal's **current active sessions** (live, via `ListSessions` — same as the console) plus **historic** `AWS/WorkSpacesWeb` metrics over a window (CloudWatch is historic-only; idle portals publish none) (Tier 0). |
|
|
69
69
|
| `list_unused_resources` | Unused WorkSpaces desktops and stopped/zero-capacity fleets worth reclaiming (Tier 0). |
|
|
70
70
|
|
|
71
71
|
Cost/utilization tools need the **Tier 1** IAM policy ([`iam/tier1-cost.json`](iam/tier1-cost.json));
|
{workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/iam/tier0-diagnostics.json
RENAMED
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
"workspaces-web:ListUserSettings",
|
|
40
40
|
"workspaces-web:ListNetworkSettings",
|
|
41
41
|
"workspaces-web:GetUserSettings",
|
|
42
|
-
"workspaces-web:GetNetworkSettings"
|
|
42
|
+
"workspaces-web:GetNetworkSettings",
|
|
43
|
+
"workspaces-web:GetDataProtectionSettings",
|
|
44
|
+
"workspaces-web:ListSessions"
|
|
43
45
|
],
|
|
44
46
|
"Resource": "*"
|
|
45
47
|
},
|
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
"workspaces-web:ListUserSettings",
|
|
40
40
|
"workspaces-web:ListNetworkSettings",
|
|
41
41
|
"workspaces-web:GetUserSettings",
|
|
42
|
-
"workspaces-web:GetNetworkSettings"
|
|
42
|
+
"workspaces-web:GetNetworkSettings",
|
|
43
|
+
"workspaces-web:GetDataProtectionSettings",
|
|
44
|
+
"workspaces-web:ListSessions"
|
|
43
45
|
],
|
|
44
46
|
"Resource": "*"
|
|
45
47
|
},
|
{workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/iam/tier2-lifecycle.json
RENAMED
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
"workspaces-web:ListUserSettings",
|
|
40
40
|
"workspaces-web:ListNetworkSettings",
|
|
41
41
|
"workspaces-web:GetUserSettings",
|
|
42
|
-
"workspaces-web:GetNetworkSettings"
|
|
42
|
+
"workspaces-web:GetNetworkSettings",
|
|
43
|
+
"workspaces-web:GetDataProtectionSettings",
|
|
44
|
+
"workspaces-web:ListSessions"
|
|
43
45
|
],
|
|
44
46
|
"Resource": "*"
|
|
45
47
|
},
|
{workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/iam/tier3-destructive.json
RENAMED
|
@@ -39,7 +39,9 @@
|
|
|
39
39
|
"workspaces-web:ListUserSettings",
|
|
40
40
|
"workspaces-web:ListNetworkSettings",
|
|
41
41
|
"workspaces-web:GetUserSettings",
|
|
42
|
-
"workspaces-web:GetNetworkSettings"
|
|
42
|
+
"workspaces-web:GetNetworkSettings",
|
|
43
|
+
"workspaces-web:GetDataProtectionSettings",
|
|
44
|
+
"workspaces-web:ListSessions"
|
|
43
45
|
],
|
|
44
46
|
"Resource": "*"
|
|
45
47
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "workspaces-euc-mcp-server"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.7"
|
|
4
4
|
description = "MCP server for administering the Amazon WorkSpaces family of End User Computing services (Personal, Pools, Applications, Secure Browser, Core)."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
{workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/test_diagnostics.py
RENAMED
|
@@ -161,6 +161,45 @@ def test_directory_health_skips_ds_for_workspaces_managed_directory():
|
|
|
161
161
|
assert any("WorkSpaces-managed" in f.title for f in d.findings)
|
|
162
162
|
|
|
163
163
|
|
|
164
|
+
def test_directory_health_surfaces_registration_ou_and_properties():
|
|
165
|
+
# The registration OU (WorkspaceCreationProperties.DefaultOu) and related properties must be
|
|
166
|
+
# exposed in the diagnosis signals.
|
|
167
|
+
workspaces = types.SimpleNamespace(
|
|
168
|
+
describe_workspace_directories=lambda **_: {
|
|
169
|
+
"Directories": [
|
|
170
|
+
{
|
|
171
|
+
"DirectoryId": "d-0123456789",
|
|
172
|
+
"State": "REGISTERED",
|
|
173
|
+
"DirectoryType": "AD_CONNECTOR",
|
|
174
|
+
"WorkspaceType": "PERSONAL",
|
|
175
|
+
"WorkspaceCreationProperties": {
|
|
176
|
+
"DefaultOu": "OU=AmazonWorkspaces,OU=Singapore,DC=bg,DC=local",
|
|
177
|
+
"CustomSecurityGroupId": "sg-0abc",
|
|
178
|
+
"UserEnabledAsLocalAdministrator": True,
|
|
179
|
+
"EnableInternetAccess": False,
|
|
180
|
+
"EnableMaintenanceMode": True,
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
]
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
ds = types.SimpleNamespace(
|
|
187
|
+
describe_directories=lambda **_: {
|
|
188
|
+
"DirectoryDescriptions": [{"DirectoryId": "d-0123456789", "Stage": "Active"}]
|
|
189
|
+
},
|
|
190
|
+
)
|
|
191
|
+
factory = FakeFactory({consts.WORKSPACES_API: workspaces, consts.DIRECTORY_API: ds})
|
|
192
|
+
|
|
193
|
+
report = diagnostics.check_directory_health_core(factory, "d-0123456789", "us-east-1")
|
|
194
|
+
|
|
195
|
+
sig = report.directories[0].signals
|
|
196
|
+
assert sig["default_ou"] == "OU=AmazonWorkspaces,OU=Singapore,DC=bg,DC=local"
|
|
197
|
+
assert sig["directory_type"] == "AD_CONNECTOR"
|
|
198
|
+
assert sig["custom_security_group_id"] == "sg-0abc"
|
|
199
|
+
assert sig["user_enabled_as_local_administrator"] is True
|
|
200
|
+
assert sig["enable_maintenance_mode"] is True
|
|
201
|
+
|
|
202
|
+
|
|
164
203
|
def test_directory_health_flags_impaired_stage():
|
|
165
204
|
workspaces = types.SimpleNamespace(
|
|
166
205
|
describe_workspace_directories=lambda **_: {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Copyright bengroeneveldsg. Licensed under the Apache License, Version 2.0 (the "License").
|
|
2
|
+
# You may not use this file except in compliance with the License.
|
|
3
|
+
# A copy of the License is located at http://www.apache.org/licenses/LICENSE-2.0
|
|
4
|
+
"""Tests for the WorkSpaces Secure Browser tools."""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import types
|
|
9
|
+
|
|
10
|
+
from workspaces_euc_mcp_server import consts
|
|
11
|
+
from workspaces_euc_mcp_server.tools import secure_browser
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FakeFactory:
|
|
15
|
+
region = "us-east-1"
|
|
16
|
+
|
|
17
|
+
def __init__(self, clients: dict[str, object]) -> None:
|
|
18
|
+
self._clients = clients
|
|
19
|
+
|
|
20
|
+
def client(self, service_name: str, region: str | None = None):
|
|
21
|
+
if service_name not in self._clients:
|
|
22
|
+
raise AssertionError(f"unexpected client requested: {service_name}")
|
|
23
|
+
return self._clients[service_name]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_portal_details_resolves_settings():
|
|
27
|
+
web = types.SimpleNamespace(
|
|
28
|
+
get_portal=lambda **_: {
|
|
29
|
+
"portal": {
|
|
30
|
+
"portalArn": "arn:portal/1",
|
|
31
|
+
"displayName": "P1",
|
|
32
|
+
"authenticationType": "Standard",
|
|
33
|
+
"portalStatus": "Active",
|
|
34
|
+
"userSettingsArn": "arn:us/1",
|
|
35
|
+
"networkSettingsArn": "arn:ns/1",
|
|
36
|
+
"browserSettingsArn": "arn:bs/1",
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
get_user_settings=lambda **_: {
|
|
40
|
+
"userSettings": {
|
|
41
|
+
"copyAllowed": "Disabled",
|
|
42
|
+
"downloadAllowed": "Enabled",
|
|
43
|
+
"printAllowed": "Disabled",
|
|
44
|
+
"idleDisconnectTimeoutInMinutes": 15,
|
|
45
|
+
"associatedPortalArns": ["arn:portal/1"], # should be dropped
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
get_network_settings=lambda **_: {
|
|
49
|
+
"networkSettings": {
|
|
50
|
+
"vpcId": "vpc-1",
|
|
51
|
+
"subnetIds": ["s-1"],
|
|
52
|
+
"securityGroupIds": ["sg-1"],
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
factory = FakeFactory({consts.SECURE_BROWSER_API: web})
|
|
57
|
+
|
|
58
|
+
d = secure_browser.get_secure_browser_portal_details_core(factory, "arn:portal/1", "us-east-1")
|
|
59
|
+
|
|
60
|
+
assert d.display_name == "P1"
|
|
61
|
+
assert d.user_settings["downloadAllowed"] == "Enabled"
|
|
62
|
+
assert "associatedPortalArns" not in d.user_settings # bulky/identifying field dropped
|
|
63
|
+
assert d.network["vpcId"] == "vpc-1"
|
|
64
|
+
assert d.has_browser_policy is True
|
|
65
|
+
assert d.has_data_protection is False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_portal_details_resolves_data_protection_config():
|
|
69
|
+
web = types.SimpleNamespace(
|
|
70
|
+
get_portal=lambda **_: {
|
|
71
|
+
"portal": {
|
|
72
|
+
"portalArn": "arn:portal/2",
|
|
73
|
+
"displayName": "Okta",
|
|
74
|
+
"dataProtectionSettingsArn": "arn:dp/1",
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
get_data_protection_settings=lambda **_: {
|
|
78
|
+
"dataProtectionSettings": {
|
|
79
|
+
"displayName": "ip-address-dp",
|
|
80
|
+
"inlineRedactionConfiguration": {
|
|
81
|
+
"inlineRedactionPatterns": [
|
|
82
|
+
{"builtInPatternId": "ipAddr"},
|
|
83
|
+
{"builtInPatternId": "macAddr"},
|
|
84
|
+
{
|
|
85
|
+
"customPattern": {
|
|
86
|
+
"patternName": "EmployeeId",
|
|
87
|
+
"keywordRegex": "EMP-\\d+",
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
"globalEnforcedUrls": ["*"],
|
|
92
|
+
"globalConfidenceLevel": 2,
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
factory = FakeFactory({consts.SECURE_BROWSER_API: web})
|
|
98
|
+
|
|
99
|
+
d = secure_browser.get_secure_browser_portal_details_core(factory, "arn:portal/2", "us-east-1")
|
|
100
|
+
|
|
101
|
+
assert d.has_data_protection is True
|
|
102
|
+
dp = d.data_protection
|
|
103
|
+
assert dp["display_name"] == "ip-address-dp"
|
|
104
|
+
assert dp["redacted_pattern_count"] == 3
|
|
105
|
+
assert dp["builtin_patterns"] == ["ipAddr", "macAddr"]
|
|
106
|
+
assert dp["custom_patterns"][0]["name"] == "EmployeeId"
|
|
107
|
+
assert dp["global_confidence_level"] == 2
|
|
108
|
+
assert dp["global_enforced_urls"] == ["*"]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_portal_usage_returns_live_active_sessions_and_historic():
|
|
112
|
+
# Active sessions must come LIVE from ListSessions (status=Active), not from CloudWatch.
|
|
113
|
+
captured = {}
|
|
114
|
+
|
|
115
|
+
def list_sessions(**kwargs):
|
|
116
|
+
captured.update(kwargs)
|
|
117
|
+
return {
|
|
118
|
+
"sessions": [
|
|
119
|
+
{"sessionId": "s-1", "username": "alice", "status": "Active"},
|
|
120
|
+
{"sessionId": "s-2", "username": "bob", "status": "Active"},
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
web = types.SimpleNamespace(list_sessions=list_sessions)
|
|
125
|
+
cw = types.SimpleNamespace(get_metric_data=lambda **_: {"MetricDataResults": []}) # no historic
|
|
126
|
+
factory = FakeFactory({consts.SECURE_BROWSER_API: web, consts.CLOUDWATCH_API: cw})
|
|
127
|
+
|
|
128
|
+
usage = secure_browser.get_secure_browser_portal_usage_core(
|
|
129
|
+
factory, "arn:aws:workspaces-web:r:a:portal/abc123", "us-east-1"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Queried ListSessions for the portal id with status=Active.
|
|
133
|
+
assert captured["portalId"] == "abc123"
|
|
134
|
+
assert captured["status"] == "Active"
|
|
135
|
+
# Live active sessions populated; CloudWatch reserved for (empty) historic.
|
|
136
|
+
assert usage.active_session_count == 2
|
|
137
|
+
assert {s.username for s in usage.active_sessions} == {"alice", "bob"}
|
|
138
|
+
assert usage.historic_metrics == {}
|
|
139
|
+
assert "active session(s) right now" in (usage.summary or "")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_portal_id_extracted_from_arn():
|
|
143
|
+
assert secure_browser._portal_id("arn:aws:workspaces-web:r:a:portal/abc123") == "abc123"
|
|
144
|
+
assert secure_browser._portal_id("abc123") == "abc123"
|
|
@@ -156,6 +156,42 @@ class SecureBrowserPortalDetails(BaseModel):
|
|
|
156
156
|
network: dict[str, object] = Field(default_factory=dict)
|
|
157
157
|
has_browser_policy: bool = False
|
|
158
158
|
has_data_protection: bool = False
|
|
159
|
+
data_protection: dict[str, object] = Field(
|
|
160
|
+
default_factory=dict,
|
|
161
|
+
description=(
|
|
162
|
+
"Resolved inline-redaction configuration when data protection is attached: redacted "
|
|
163
|
+
"patterns (built-in + custom), global confidence level, and enforced/exempt URLs."
|
|
164
|
+
),
|
|
165
|
+
)
|
|
166
|
+
errors: list[ServiceError] = Field(default_factory=list)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class SecureBrowserSession(BaseModel):
|
|
170
|
+
"""A single Secure Browser portal session (from ListSessions)."""
|
|
171
|
+
|
|
172
|
+
session_id: str
|
|
173
|
+
username: str | None = None
|
|
174
|
+
status: str | None = None
|
|
175
|
+
start_time: str | None = None
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class SecureBrowserPortalUsage(BaseModel):
|
|
179
|
+
"""Live active sessions (ListSessions) plus historic metrics (CloudWatch) for a portal."""
|
|
180
|
+
|
|
181
|
+
portal: str
|
|
182
|
+
active_session_count: int = 0
|
|
183
|
+
active_sessions: list[SecureBrowserSession] = Field(
|
|
184
|
+
default_factory=list,
|
|
185
|
+
description="Sessions currently Active, retrieved live from ListSessions (like the "
|
|
186
|
+
"console).",
|
|
187
|
+
)
|
|
188
|
+
lookback_days: int
|
|
189
|
+
period_hours: int
|
|
190
|
+
historic_metrics: dict[str, FleetMetricSeries] = Field(
|
|
191
|
+
default_factory=dict,
|
|
192
|
+
description="Historic session metrics from CloudWatch (AWS/WorkSpacesWeb) over the window.",
|
|
193
|
+
)
|
|
194
|
+
summary: str | None = None
|
|
159
195
|
errors: list[ServiceError] = Field(default_factory=list)
|
|
160
196
|
|
|
161
197
|
|
|
@@ -279,9 +279,22 @@ def _diagnose_directory_into(
|
|
|
279
279
|
)
|
|
280
280
|
dirs = (reg or {}).get("Directories", [])
|
|
281
281
|
if dirs:
|
|
282
|
-
|
|
282
|
+
d0 = dirs[0]
|
|
283
|
+
reg_state = d0.get("State", "UNKNOWN")
|
|
283
284
|
if signals is not None:
|
|
284
285
|
signals[f"{signals_prefix}registration_state"] = reg_state
|
|
286
|
+
signals[f"{signals_prefix}directory_type"] = d0.get("DirectoryType")
|
|
287
|
+
signals[f"{signals_prefix}workspace_type"] = d0.get("WorkspaceType")
|
|
288
|
+
# Registration-level WorkspaceCreationProperties — notably the target OU. Only AD-backed
|
|
289
|
+
# directories carry a DefaultOu; WorkSpaces-managed (Entra/internal) ones return None.
|
|
290
|
+
wcp = d0.get("WorkspaceCreationProperties") or {}
|
|
291
|
+
signals[f"{signals_prefix}default_ou"] = wcp.get("DefaultOu")
|
|
292
|
+
signals[f"{signals_prefix}custom_security_group_id"] = wcp.get("CustomSecurityGroupId")
|
|
293
|
+
signals[f"{signals_prefix}user_enabled_as_local_administrator"] = wcp.get(
|
|
294
|
+
"UserEnabledAsLocalAdministrator"
|
|
295
|
+
)
|
|
296
|
+
signals[f"{signals_prefix}enable_internet_access"] = wcp.get("EnableInternetAccess")
|
|
297
|
+
signals[f"{signals_prefix}enable_maintenance_mode"] = wcp.get("EnableMaintenanceMode")
|
|
285
298
|
if reg_state != "REGISTERED":
|
|
286
299
|
findings.append(
|
|
287
300
|
Finding(
|
|
@@ -18,8 +18,13 @@ from typing import Any
|
|
|
18
18
|
|
|
19
19
|
from .. import consts
|
|
20
20
|
from ..clients import ClientFactory
|
|
21
|
-
from ..models import
|
|
22
|
-
|
|
21
|
+
from ..models import (
|
|
22
|
+
SecureBrowserPortalDetails,
|
|
23
|
+
SecureBrowserPortalUsage,
|
|
24
|
+
SecureBrowserSession,
|
|
25
|
+
ServiceError,
|
|
26
|
+
)
|
|
27
|
+
from ._common import paginate, read_only, try_call
|
|
23
28
|
from .performance import _fetch_metric_series
|
|
24
29
|
|
|
25
30
|
|
|
@@ -78,6 +83,19 @@ def get_secure_browser_portal_details_core(
|
|
|
78
83
|
if key in (ns or {}):
|
|
79
84
|
network[key] = ns[key]
|
|
80
85
|
|
|
86
|
+
data_protection: dict[str, object] = {}
|
|
87
|
+
if portal.get("dataProtectionSettingsArn"):
|
|
88
|
+
dps = try_call(
|
|
89
|
+
errors,
|
|
90
|
+
consts.PRODUCT_SECURE_BROWSER,
|
|
91
|
+
"GetDataProtectionSettings",
|
|
92
|
+
lambda: web.get_data_protection_settings(
|
|
93
|
+
dataProtectionSettingsArn=portal["dataProtectionSettingsArn"]
|
|
94
|
+
).get("dataProtectionSettings", {}),
|
|
95
|
+
default={},
|
|
96
|
+
)
|
|
97
|
+
data_protection = _summarize_data_protection(dps or {})
|
|
98
|
+
|
|
81
99
|
return SecureBrowserPortalDetails(
|
|
82
100
|
portal_arn=portal_arn,
|
|
83
101
|
display_name=portal.get("displayName"),
|
|
@@ -87,23 +105,93 @@ def get_secure_browser_portal_details_core(
|
|
|
87
105
|
network=network,
|
|
88
106
|
has_browser_policy=bool(portal.get("browserSettingsArn")),
|
|
89
107
|
has_data_protection=bool(portal.get("dataProtectionSettingsArn")),
|
|
108
|
+
data_protection=data_protection,
|
|
90
109
|
errors=errors,
|
|
91
110
|
)
|
|
92
111
|
|
|
93
112
|
|
|
113
|
+
def _summarize_data_protection(dps: dict[str, Any]) -> dict[str, Any]:
|
|
114
|
+
"""Reduce a data-protection settings object to the policy-relevant redaction configuration."""
|
|
115
|
+
inline: dict[str, Any] = dps.get("inlineRedactionConfiguration") or {}
|
|
116
|
+
patterns: list[dict[str, Any]] = inline.get("inlineRedactionPatterns") or []
|
|
117
|
+
builtin: list[str] = []
|
|
118
|
+
custom: list[dict[str, Any]] = []
|
|
119
|
+
for p in patterns:
|
|
120
|
+
if p.get("builtInPatternId"):
|
|
121
|
+
builtin.append(p["builtInPatternId"])
|
|
122
|
+
elif p.get("customPattern"):
|
|
123
|
+
cp = p["customPattern"]
|
|
124
|
+
custom.append(
|
|
125
|
+
{
|
|
126
|
+
"name": cp.get("patternName"),
|
|
127
|
+
"description": cp.get("patternDescription"),
|
|
128
|
+
"keyword_regex": cp.get("keywordRegex"),
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
return {
|
|
132
|
+
"display_name": dps.get("displayName"),
|
|
133
|
+
"redacted_pattern_count": len(patterns),
|
|
134
|
+
"builtin_patterns": builtin,
|
|
135
|
+
"custom_patterns": custom,
|
|
136
|
+
"global_confidence_level": inline.get("globalConfidenceLevel"),
|
|
137
|
+
"global_enforced_urls": inline.get("globalEnforcedUrls"),
|
|
138
|
+
"global_exempt_urls": inline.get("globalExemptUrls"),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
94
142
|
def _portal_id(portal: str) -> str:
|
|
95
143
|
"""Accept a portal ARN or id; the CloudWatch dimension uses the id (last ARN segment)."""
|
|
96
144
|
return portal.rsplit("/", 1)[-1] if "/" in portal else portal
|
|
97
145
|
|
|
98
146
|
|
|
147
|
+
def _list_active_sessions(
|
|
148
|
+
web: Any, portal_id: str, errors: list[ServiceError]
|
|
149
|
+
) -> list[SecureBrowserSession]:
|
|
150
|
+
"""Current Active sessions for a portal, live from ListSessions (what the console shows)."""
|
|
151
|
+
raw = try_call(
|
|
152
|
+
errors,
|
|
153
|
+
consts.PRODUCT_SECURE_BROWSER,
|
|
154
|
+
"ListSessions",
|
|
155
|
+
lambda: paginate(
|
|
156
|
+
web.list_sessions,
|
|
157
|
+
"sessions",
|
|
158
|
+
pagination_in="nextToken",
|
|
159
|
+
pagination_out="nextToken",
|
|
160
|
+
portalId=portal_id,
|
|
161
|
+
status="Active",
|
|
162
|
+
),
|
|
163
|
+
default=[],
|
|
164
|
+
)
|
|
165
|
+
sessions: list[SecureBrowserSession] = []
|
|
166
|
+
for s in raw or []:
|
|
167
|
+
start = s.get("startTime")
|
|
168
|
+
sessions.append(
|
|
169
|
+
SecureBrowserSession(
|
|
170
|
+
session_id=s.get("sessionId", ""),
|
|
171
|
+
username=s.get("username"),
|
|
172
|
+
status=s.get("status"),
|
|
173
|
+
start_time=start.isoformat() if hasattr(start, "isoformat") else None,
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
return sessions
|
|
177
|
+
|
|
178
|
+
|
|
99
179
|
def get_secure_browser_portal_usage_core(
|
|
100
180
|
factory: ClientFactory,
|
|
101
181
|
portal: str,
|
|
102
182
|
region: str | None,
|
|
103
183
|
lookback_days: int = 7,
|
|
104
184
|
period_hours: int = 24,
|
|
105
|
-
) ->
|
|
185
|
+
) -> SecureBrowserPortalUsage:
|
|
106
186
|
errors: list[ServiceError] = []
|
|
187
|
+
portal_id = _portal_id(portal)
|
|
188
|
+
web = factory.client(consts.SECURE_BROWSER_API, region=region)
|
|
189
|
+
|
|
190
|
+
# LIVE: current active sessions come from ListSessions (the real-time source the console uses),
|
|
191
|
+
# NOT from CloudWatch.
|
|
192
|
+
active = _list_active_sessions(web, portal_id, errors)
|
|
193
|
+
|
|
194
|
+
# HISTORIC: CloudWatch (AWS/WorkSpacesWeb) over the window — only meaningful for past activity.
|
|
107
195
|
cloudwatch = factory.client(consts.CLOUDWATCH_API, region=region)
|
|
108
196
|
metrics = try_call(
|
|
109
197
|
errors,
|
|
@@ -113,26 +201,30 @@ def get_secure_browser_portal_usage_core(
|
|
|
113
201
|
cloudwatch,
|
|
114
202
|
consts.SECURE_BROWSER_NAMESPACE,
|
|
115
203
|
consts.SECURE_BROWSER_PORTAL_DIMENSION,
|
|
116
|
-
|
|
204
|
+
portal_id,
|
|
117
205
|
consts.SECURE_BROWSER_SESSION_METRICS,
|
|
118
206
|
lookback_days,
|
|
119
207
|
period_hours,
|
|
120
208
|
),
|
|
121
209
|
default={},
|
|
122
210
|
)
|
|
211
|
+
|
|
212
|
+
hist = (
|
|
213
|
+
f"historic metrics over {lookback_days}d available (see historic_metrics)"
|
|
214
|
+
if metrics
|
|
215
|
+
else f"no historic session metrics in the last {lookback_days}d (idle portals publish "
|
|
216
|
+
"none to CloudWatch; enable the portal's Session Logger for detail)"
|
|
217
|
+
)
|
|
123
218
|
summary = (
|
|
124
|
-
"
|
|
125
|
-
"sessions occur (idle portals publish nothing); for detailed usage enable the portal's "
|
|
126
|
-
"Session Logger."
|
|
127
|
-
if not metrics
|
|
128
|
-
else f"Session metrics over {lookback_days}d: see series."
|
|
219
|
+
f"{len(active)} active session(s) right now (live via ListSessions). Historic: {hist}."
|
|
129
220
|
)
|
|
130
|
-
return
|
|
131
|
-
|
|
132
|
-
|
|
221
|
+
return SecureBrowserPortalUsage(
|
|
222
|
+
portal=portal,
|
|
223
|
+
active_session_count=len(active),
|
|
224
|
+
active_sessions=active,
|
|
133
225
|
lookback_days=lookback_days,
|
|
134
226
|
period_hours=period_hours,
|
|
135
|
-
|
|
227
|
+
historic_metrics=metrics or {},
|
|
136
228
|
summary=summary,
|
|
137
229
|
errors=errors,
|
|
138
230
|
)
|
|
@@ -147,8 +239,10 @@ def register(mcp: Any, factory: ClientFactory) -> None:
|
|
|
147
239
|
"""Resolve a WorkSpaces Secure Browser portal's settings (security-relevant).
|
|
148
240
|
|
|
149
241
|
Returns the portal's user settings (clipboard copy/paste, file download/upload, print
|
|
150
|
-
controls + timeouts), network (VPC/subnets/security groups),
|
|
151
|
-
and data
|
|
242
|
+
controls + timeouts), network (VPC/subnets/security groups), whether a browser policy is
|
|
243
|
+
attached, and — when data protection is configured — the resolved inline-redaction config
|
|
244
|
+
(which built-in/custom patterns are redacted, global confidence, enforced/exempt URLs).
|
|
245
|
+
Read-only.
|
|
152
246
|
|
|
153
247
|
Args:
|
|
154
248
|
portal_arn: The portal ARN (from get_euc_inventory_summary / generate_inventory_report).
|
|
@@ -165,10 +259,12 @@ def register(mcp: Any, factory: ClientFactory) -> None:
|
|
|
165
259
|
lookback_days: int = 7,
|
|
166
260
|
period_hours: int = 24,
|
|
167
261
|
) -> dict[str, Any]:
|
|
168
|
-
"""Get a Secure Browser portal's
|
|
262
|
+
"""Get a Secure Browser portal's CURRENT active sessions plus historic session metrics.
|
|
169
263
|
|
|
170
|
-
|
|
171
|
-
|
|
264
|
+
Active sessions are retrieved **live** from ListSessions (the same source as the console's
|
|
265
|
+
active-sessions view). Historic usage comes from CloudWatch (AWS/WorkSpacesWeb) over the
|
|
266
|
+
window — CloudWatch is only meaningful for *past* activity, and idle portals publish none.
|
|
267
|
+
Read-only.
|
|
172
268
|
|
|
173
269
|
Args:
|
|
174
270
|
portal: The portal id or ARN.
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
# Copyright bengroeneveldsg. Licensed under the Apache License, Version 2.0 (the "License").
|
|
2
|
-
# You may not use this file except in compliance with the License.
|
|
3
|
-
# A copy of the License is located at http://www.apache.org/licenses/LICENSE-2.0
|
|
4
|
-
"""Tests for the WorkSpaces Secure Browser tools."""
|
|
5
|
-
|
|
6
|
-
from __future__ import annotations
|
|
7
|
-
|
|
8
|
-
import types
|
|
9
|
-
|
|
10
|
-
from workspaces_euc_mcp_server import consts
|
|
11
|
-
from workspaces_euc_mcp_server.tools import secure_browser
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class FakeFactory:
|
|
15
|
-
region = "us-east-1"
|
|
16
|
-
|
|
17
|
-
def __init__(self, clients: dict[str, object]) -> None:
|
|
18
|
-
self._clients = clients
|
|
19
|
-
|
|
20
|
-
def client(self, service_name: str, region: str | None = None):
|
|
21
|
-
if service_name not in self._clients:
|
|
22
|
-
raise AssertionError(f"unexpected client requested: {service_name}")
|
|
23
|
-
return self._clients[service_name]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_portal_details_resolves_settings():
|
|
27
|
-
web = types.SimpleNamespace(
|
|
28
|
-
get_portal=lambda **_: {
|
|
29
|
-
"portal": {
|
|
30
|
-
"portalArn": "arn:portal/1",
|
|
31
|
-
"displayName": "P1",
|
|
32
|
-
"authenticationType": "Standard",
|
|
33
|
-
"portalStatus": "Active",
|
|
34
|
-
"userSettingsArn": "arn:us/1",
|
|
35
|
-
"networkSettingsArn": "arn:ns/1",
|
|
36
|
-
"browserSettingsArn": "arn:bs/1",
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
get_user_settings=lambda **_: {
|
|
40
|
-
"userSettings": {
|
|
41
|
-
"copyAllowed": "Disabled",
|
|
42
|
-
"downloadAllowed": "Enabled",
|
|
43
|
-
"printAllowed": "Disabled",
|
|
44
|
-
"idleDisconnectTimeoutInMinutes": 15,
|
|
45
|
-
"associatedPortalArns": ["arn:portal/1"], # should be dropped
|
|
46
|
-
}
|
|
47
|
-
},
|
|
48
|
-
get_network_settings=lambda **_: {
|
|
49
|
-
"networkSettings": {
|
|
50
|
-
"vpcId": "vpc-1",
|
|
51
|
-
"subnetIds": ["s-1"],
|
|
52
|
-
"securityGroupIds": ["sg-1"],
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
|
-
)
|
|
56
|
-
factory = FakeFactory({consts.SECURE_BROWSER_API: web})
|
|
57
|
-
|
|
58
|
-
d = secure_browser.get_secure_browser_portal_details_core(factory, "arn:portal/1", "us-east-1")
|
|
59
|
-
|
|
60
|
-
assert d.display_name == "P1"
|
|
61
|
-
assert d.user_settings["downloadAllowed"] == "Enabled"
|
|
62
|
-
assert "associatedPortalArns" not in d.user_settings # bulky/identifying field dropped
|
|
63
|
-
assert d.network["vpcId"] == "vpc-1"
|
|
64
|
-
assert d.has_browser_policy is True
|
|
65
|
-
assert d.has_data_protection is False
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def test_portal_usage_empty_explains_session_model():
|
|
69
|
-
cw = types.SimpleNamespace(
|
|
70
|
-
get_metric_data=lambda **_: {"MetricDataResults": []} # no data
|
|
71
|
-
)
|
|
72
|
-
factory = FakeFactory({consts.CLOUDWATCH_API: cw})
|
|
73
|
-
|
|
74
|
-
usage = secure_browser.get_secure_browser_portal_usage_core(
|
|
75
|
-
factory, "arn:aws:workspaces-web:r:a:portal/abc123", "us-east-1"
|
|
76
|
-
)
|
|
77
|
-
|
|
78
|
-
assert usage.target_type == consts.PRODUCT_SECURE_BROWSER
|
|
79
|
-
assert usage.target_id.endswith("portal/abc123")
|
|
80
|
-
assert "Session Logger" in (usage.summary or "")
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def test_portal_id_extracted_from_arn():
|
|
84
|
-
assert secure_browser._portal_id("arn:aws:workspaces-web:r:a:portal/abc123") == "abc123"
|
|
85
|
-
assert secure_browser._portal_id("abc123") == "abc123"
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/.github/workflows/ci.yml
RENAMED
|
File without changes
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/.github/workflows/publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/scripts/smoke_readonly.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/test_destructive.py
RENAMED
|
File without changes
|
{workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/test_governance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{workspaces_euc_mcp_server-0.1.5 → workspaces_euc_mcp_server-0.1.7}/tests/test_performance.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|