workspaces-euc-mcp-server 0.1.4__tar.gz → 0.1.6__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.
Files changed (52) hide show
  1. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/CHANGELOG.md +36 -0
  2. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/DESIGN.md +24 -10
  3. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/PKG-INFO +9 -4
  4. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/README.md +8 -3
  5. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/iam/tier0-diagnostics.json +9 -3
  6. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/iam/tier1-cost.json +9 -3
  7. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/iam/tier2-lifecycle.json +9 -3
  8. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/iam/tier3-destructive.json +9 -3
  9. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/pyproject.toml +1 -1
  10. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/tests/test_diagnostics.py +39 -0
  11. workspaces_euc_mcp_server-0.1.6/tests/test_governance.py +133 -0
  12. workspaces_euc_mcp_server-0.1.6/tests/test_images.py +115 -0
  13. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/tests/test_secure_browser.py +43 -0
  14. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/workspaces_euc_mcp_server/__init__.py +1 -1
  15. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/workspaces_euc_mcp_server/consts.py +46 -0
  16. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/workspaces_euc_mcp_server/models.py +112 -0
  17. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/workspaces_euc_mcp_server/server.py +4 -0
  18. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/workspaces_euc_mcp_server/tools/diagnostics.py +14 -1
  19. workspaces_euc_mcp_server-0.1.6/workspaces_euc_mcp_server/tools/governance.py +361 -0
  20. workspaces_euc_mcp_server-0.1.6/workspaces_euc_mcp_server/tools/images.py +222 -0
  21. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/workspaces_euc_mcp_server/tools/secure_browser.py +47 -2
  22. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/.dockerignore +0 -0
  23. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/.github/workflows/ci.yml +0 -0
  24. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/.github/workflows/docker-publish.yml +0 -0
  25. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/.github/workflows/publish.yml +0 -0
  26. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/.gitignore +0 -0
  27. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/.pre-commit-config.yaml +0 -0
  28. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/Dockerfile +0 -0
  29. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/LICENSE +0 -0
  30. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/iam/README.md +0 -0
  31. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/scripts/smoke_readonly.py +0 -0
  32. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/tests/__init__.py +0 -0
  33. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/tests/test_clients.py +0 -0
  34. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/tests/test_cost.py +0 -0
  35. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/tests/test_destructive.py +0 -0
  36. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/tests/test_inventory.py +0 -0
  37. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/tests/test_lifecycle.py +0 -0
  38. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/tests/test_naming.py +0 -0
  39. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/tests/test_no_embedded_secrets.py +0 -0
  40. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/tests/test_performance.py +0 -0
  41. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/tests/test_pricing.py +0 -0
  42. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/tests/test_reporting.py +0 -0
  43. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/workspaces_euc_mcp_server/clients.py +0 -0
  44. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/workspaces_euc_mcp_server/tools/__init__.py +0 -0
  45. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/workspaces_euc_mcp_server/tools/_common.py +0 -0
  46. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/workspaces_euc_mcp_server/tools/cost.py +0 -0
  47. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/workspaces_euc_mcp_server/tools/destructive.py +0 -0
  48. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/workspaces_euc_mcp_server/tools/inventory.py +0 -0
  49. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/workspaces_euc_mcp_server/tools/lifecycle.py +0 -0
  50. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/workspaces_euc_mcp_server/tools/performance.py +0 -0
  51. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/workspaces_euc_mcp_server/tools/pricing.py +0 -0
  52. {workspaces_euc_mcp_server-0.1.4 → workspaces_euc_mcp_server-0.1.6}/workspaces_euc_mcp_server/tools/reporting.py +0 -0
@@ -5,6 +5,42 @@ All notable changes to this project are documented here. The format is based on
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.1.6] - 2026-06-02
9
+
10
+ ### Added
11
+ - `check_directory_health` now surfaces each directory's **registration properties** in its
12
+ signals — notably the target **OU** (`WorkspaceCreationProperties.DefaultOu`), plus custom
13
+ security group, local-admin / internet-access / maintenance-mode flags, and directory/workspace
14
+ type. (AD-backed directories carry an OU; WorkSpaces-managed ones return none.)
15
+ - `get_secure_browser_portal_details` now resolves the portal's **data-protection configuration**
16
+ when attached: the redacted built-in/custom inline-redaction patterns, global confidence level,
17
+ and enforced/exempt URLs — not just whether data protection is on. Adds
18
+ `workspaces-web:GetDataProtectionSettings` to every IAM tier.
19
+
20
+ ## [0.1.5] - 2026-06-02
21
+
22
+ ### Added
23
+ - `get_euc_audit_trail` — a new read-only (Tier 0) tool that reports recent EUC management activity
24
+ from CloudTrail (always-on `LookupEvents`, 90-day window, no trail required) across WorkSpaces
25
+ Personal/Pools/Core, WorkSpaces Applications, Secure Browser, and Core Managed Instances.
26
+ Mutations-only by default ("who created/modified/terminated what"); flags destructive actions and
27
+ errors (e.g. AccessDenied). Adds `cloudtrail:LookupEvents` to every IAM tier.
28
+ - `get_euc_service_quotas` — a new read-only (Tier 0) tool that reports Service Quotas limits per
29
+ EUC service and, where AWS publishes a linked usage metric (`AWS/Usage` `ResourceCount`), the
30
+ current usage and utilisation %, flagging quotas approaching their limit (capacity planning).
31
+ Adds `servicequotas:ListServiceQuotas` / `GetServiceQuota` to every IAM tier.
32
+ - `audit_application_images` — a new read-only (Tier 0) tool that audits WorkSpaces Applications
33
+ (AppStream 2.0) **images and image builders**: lists your PRIVATE/SHARED images (skipping PUBLIC
34
+ base images) and flags stale base images (likely unpatched OS), pinned/old AppStream agents,
35
+ non-AVAILABLE or errored images, SHARED cross-account visibility, and image builders left
36
+ **RUNNING** (per-hour cost + interactive admin surface). Adds `appstream:DescribeImages` and
37
+ `appstream:DescribeImageBuilders` to every IAM tier.
38
+
39
+ ### Docs
40
+ - README and DESIGN.md reconciled to the shipped state: 21 read-only tools (Tiers 0–1) + 10 write
41
+ (Tier 2) + 3 destructive (Tier 3); `tools/` layout, §5 tool catalog (image audit + governance),
42
+ and the Tier 0 IAM action list all updated.
43
+
8
44
  ## [0.1.4] - 2026-06-02
9
45
 
10
46
  ### Added
@@ -4,12 +4,14 @@
4
4
  > inventory, troubleshooting, cost/utilization optimization, and guarded lifecycle management
5
5
  > across the Amazon WorkSpaces family of End User Computing services.
6
6
  >
7
- > **Shipped:** 18 read-only tools (Tier 0/1) + 10 guarded write tools (Tier 2) + 3 destructive
7
+ > **Shipped:** 21 read-only tools (Tier 0/1) + 10 guarded write tools (Tier 2) + 3 destructive
8
8
  > tools (Tier 3), all behind opt-in flags with dry-run/confirm/blast-radius/typed-acknowledgement
9
- > guards. All four IAM policy tiers ship in `iam/`. CI (ruff/format/pyright/bandit/pytest on
10
- > Py 3.11–3.13) is green; published to PyPI and GHCR. **`README.md` is the source of truth for the
11
- > live tool catalog and exact tool names** §5 below records the original design intent and is
12
- > kept reconciled with what shipped. See `CHANGELOG.md` for per-version detail.
9
+ > guards. Read-only coverage now includes image auditing and governance (CloudTrail audit trail +
10
+ > Service Quotas headroom). All four IAM policy tiers ship in `iam/`. CI
11
+ > (ruff/format/pyright/bandit/pytest on Py 3.11–3.13) is green; published to PyPI and GHCR.
12
+ > **`README.md` is the source of truth for the live tool catalog and exact tool names** — §5 below
13
+ > records the original design intent and is kept reconciled with what shipped. See `CHANGELOG.md`
14
+ > for per-version detail.
13
15
 
14
16
  ## 1. Principles
15
17
 
@@ -72,6 +74,8 @@
72
74
  reporting.py
73
75
  secure_browser.py
74
76
  pricing.py # AWS Price List lookups for $ estimates
77
+ images.py # WorkSpaces Applications image / image-builder audit
78
+ governance.py # CloudTrail audit trail + Service Quotas headroom
75
79
  lifecycle.py # Tier 2 (guarded writes)
76
80
  destructive.py # Tier 3 (terminate/rebuild/restore)
77
81
  iam/ # shippable least-privilege policy docs per tier (0–3)
@@ -113,7 +117,7 @@ and return a synthesized result, not raw API passthroughs.
113
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` |
114
118
  | `diagnose_pool` | Why a Pool is unhealthy/queued — capacity status, sessions, errors, scaling | `workspaces:DescribeWorkspacesPool*`, `cloudwatch:GetMetricData` |
115
119
  | `diagnose_application_fleet` | Fleet state, capacity, scaling activity, fleet errors | `appstream:DescribeFleets`, `appstream:ListAssociatedStacks`, `cloudwatch:GetMetricData`, `application-autoscaling:DescribeScalingActivities` |
116
- | `check_directory_health` | Shared dependency: directory reachability/registration (skips `ds` for WorkSpaces-managed `wsd-` directories) | `ds:DescribeDirectories`, `workspaces:DescribeWorkspaceDirectories` |
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` |
117
121
 
118
122
  **Cost, utilization & performance**
119
123
  | Tool | Purpose | IAM actions |
@@ -130,15 +134,22 @@ and return a synthesized result, not raw API passthroughs.
130
134
  **Secure Browser**
131
135
  | Tool | Purpose | IAM actions |
132
136
  |---|---|---|
133
- | `get_secure_browser_portal_details` | Portal config + associated settings (browser/network/user/IP-access) | `workspaces-web:GetPortal`, `workspaces-web:List*`, `workspaces-web:Get*Settings` |
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` |
134
138
  | `get_secure_browser_portal_usage` | Portal session/usage metrics | `workspaces-web:ListPortals`, `cloudwatch:GetMetricData` |
135
139
 
136
140
  **Reporting & audit**
137
141
  | Tool | Purpose | IAM actions |
138
142
  |---|---|---|
139
143
  | `audit_security_posture` | Encryption at rest, IP access control groups, directory config, portal policies | `workspaces:Describe*`, `workspaces-web:Get*/List*`, `appstream:Describe*` |
144
+ | `audit_application_images` | WorkSpaces Applications image/image-builder audit — stale base, pinned agent, errored/SHARED images, RUNNING builders | `appstream:DescribeImages`, `appstream:DescribeImageBuilders` |
140
145
  | `list_unused_resources` | Idle desktops / empty fleets / orphaned resources | describes + `cloudwatch:GetMetricData` |
141
146
 
147
+ **Governance** (cross-service)
148
+ | Tool | Purpose | IAM actions |
149
+ |---|---|---|
150
+ | `get_euc_audit_trail` | "Who changed what" — recent EUC management events (mutations by default), 90-day CloudTrail history | `cloudtrail:LookupEvents` |
151
+ | `get_euc_service_quotas` | Quota limits + usage headroom per EUC service (capacity planning) | `servicequotas:ListServiceQuotas`, `servicequotas:GetServiceQuota`, `cloudwatch:GetMetricData` |
152
+
142
153
  ### Phase 2 — Guarded lifecycle (writes; Tier 2, `--enable-writes`)
143
154
  All support `dry_run`, return a plan + blast-radius before acting, and honor `--max-bulk-targets`.
144
155
 
@@ -162,9 +173,12 @@ All support `dry_run`, return a plan + blast-radius before acting, and honor `--
162
173
 
163
174
  We ship a managed policy document per tier so customers grant exactly what they enable.
164
175
 
165
- - **Tier 0 — Diagnostics (read-only):** `workspaces:Describe*`, `appstream:Describe*`,
166
- `workspaces-web:Get*`/`List*`, `ds:DescribeDirectories`, `cloudwatch:GetMetricData`,
167
- `application-autoscaling:DescribeScalingActivities`.
176
+ - **Tier 0 — Diagnostics (read-only):** `workspaces:Describe*`, `appstream:Describe*` (incl.
177
+ `DescribeImages`/`DescribeImageBuilders`), `workspaces-web:Get*`/`List*`,
178
+ `workspaces-instances:List*`/`Get*`, `ds:DescribeDirectories`,
179
+ `cloudwatch:GetMetricData`/`ListMetrics`, `application-autoscaling:DescribeScalingActivities`,
180
+ `ec2:DescribeInstances`, `cloudtrail:LookupEvents`,
181
+ `servicequotas:ListServiceQuotas`/`GetServiceQuota`.
168
182
  - **Tier 1 — Cost/optimization (read-only):** Tier 0 + `ce:GetCostAndUsage`,
169
183
  `ce:GetDimensionValues`, `pricing:GetProducts`.
170
184
  - **Tier 2 — Lifecycle (writes):** Tier 1 + `workspaces:Start/Stop/Reboot/ModifyWorkspace*`,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: workspaces-euc-mcp-server
3
- Version: 0.1.4
3
+ Version: 0.1.6
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
@@ -70,7 +70,9 @@ diagnosis, a right-sizing recommendation) instead of returning raw API output. S
70
70
 
71
71
  ## Status
72
72
 
73
- **Phase 1 (in progress)** — read-only inventory and troubleshooting tools:
73
+ **Shipped** (published to PyPI + GHCR) — **21 read-only tools** (Tiers 0–1) plus opt-in **10 write**
74
+ (Tier 2) and **3 destructive** (Tier 3) tools. The read-only inventory, troubleshooting, cost,
75
+ audit, and governance tools:
74
76
 
75
77
  | Tool | Description |
76
78
  |------|-------------|
@@ -79,7 +81,7 @@ diagnosis, a right-sizing recommendation) instead of returning raw API output. S
79
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. |
80
82
  | `diagnose_pool` | A WorkSpaces Pool's health — state, pool errors, user-session capacity, backing directory health, and session-utilization. |
81
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). |
82
- | `check_directory_health` | Registration state and AWS Directory Service stage for one or all WorkSpaces-registered directories. |
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. |
83
85
  | `analyze_workspace_utilization` | Classifies WorkSpaces Personal desktops as unused / idle / active from the `UserConnected` metric (Tier 1). |
84
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). |
85
87
  | `get_workspace_performance` | Native CPU / memory / disk / GPU / latency / uptime metrics per desktop from `AWS/WorkSpaces` — no CloudWatch agent (Tier 0). |
@@ -89,7 +91,10 @@ diagnosis, a right-sizing recommendation) instead of returning raw API output. S
89
91
  | `get_euc_cost_summary` | EUC spend by service over a window (or an explicit `start_date`/`end_date` calendar month) via Cost Explorer, account-wide. Services are matched by keyword so naming variants (e.g. AppStream 2.0) aren't dropped; note Cost Explorer bills WorkSpaces Personal/Pools/Core together as one "Amazon WorkSpaces" line (Tier 1). |
90
92
  | `generate_inventory_report` | Detailed per-resource inventory (desktops **with assigned user / computer name / IP**, pools, fleets, **stacks + their associated fleets**, portals) with key attributes (Tier 0). |
91
93
  | `audit_security_posture` | Cross-service: flags unencrypted WorkSpace volumes, directories without IP access control groups, and **Secure Browser portals / Applications stacks that allow data egress** (clipboard/download/print) (Tier 0). |
92
- | `get_secure_browser_portal_details` | Resolves a Secure Browser portal's user settings (clipboard/print/download controls + timeouts), network, and attached policies (Tier 0). |
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
+ | `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
+ | `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, attached policies, and — when configured — the **data-protection redaction config** (which built-in/custom patterns are redacted, confidence level, enforced/exempt URLs) (Tier 0). |
93
98
  | `get_secure_browser_portal_usage` | A Secure Browser portal's `AWS/WorkSpacesWeb` session metrics over a window (empty until the portal has sessions; Session Logger gives detail) (Tier 0). |
94
99
  | `list_unused_resources` | Unused WorkSpaces desktops and stopped/zero-capacity fleets worth reclaiming (Tier 0). |
95
100
 
@@ -40,7 +40,9 @@ diagnosis, a right-sizing recommendation) instead of returning raw API output. S
40
40
 
41
41
  ## Status
42
42
 
43
- **Phase 1 (in progress)** — read-only inventory and troubleshooting tools:
43
+ **Shipped** (published to PyPI + GHCR) — **21 read-only tools** (Tiers 0–1) plus opt-in **10 write**
44
+ (Tier 2) and **3 destructive** (Tier 3) tools. The read-only inventory, troubleshooting, cost,
45
+ audit, and governance tools:
44
46
 
45
47
  | Tool | Description |
46
48
  |------|-------------|
@@ -49,7 +51,7 @@ diagnosis, a right-sizing recommendation) instead of returning raw API output. S
49
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. |
50
52
  | `diagnose_pool` | A WorkSpaces Pool's health — state, pool errors, user-session capacity, backing directory health, and session-utilization. |
51
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). |
52
- | `check_directory_health` | Registration state and AWS Directory Service stage for one or all WorkSpaces-registered directories. |
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. |
53
55
  | `analyze_workspace_utilization` | Classifies WorkSpaces Personal desktops as unused / idle / active from the `UserConnected` metric (Tier 1). |
54
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). |
55
57
  | `get_workspace_performance` | Native CPU / memory / disk / GPU / latency / uptime metrics per desktop from `AWS/WorkSpaces` — no CloudWatch agent (Tier 0). |
@@ -59,7 +61,10 @@ diagnosis, a right-sizing recommendation) instead of returning raw API output. S
59
61
  | `get_euc_cost_summary` | EUC spend by service over a window (or an explicit `start_date`/`end_date` calendar month) via Cost Explorer, account-wide. Services are matched by keyword so naming variants (e.g. AppStream 2.0) aren't dropped; note Cost Explorer bills WorkSpaces Personal/Pools/Core together as one "Amazon WorkSpaces" line (Tier 1). |
60
62
  | `generate_inventory_report` | Detailed per-resource inventory (desktops **with assigned user / computer name / IP**, pools, fleets, **stacks + their associated fleets**, portals) with key attributes (Tier 0). |
61
63
  | `audit_security_posture` | Cross-service: flags unencrypted WorkSpace volumes, directories without IP access control groups, and **Secure Browser portals / Applications stacks that allow data egress** (clipboard/download/print) (Tier 0). |
62
- | `get_secure_browser_portal_details` | Resolves a Secure Browser portal's user settings (clipboard/print/download controls + timeouts), network, and attached policies (Tier 0). |
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
+ | `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
+ | `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, attached policies, and — when configured — the **data-protection redaction config** (which built-in/custom patterns are redacted, confidence level, enforced/exempt URLs) (Tier 0). |
63
68
  | `get_secure_browser_portal_usage` | A Secure Browser portal's `AWS/WorkSpacesWeb` session metrics over a window (empty until the portal has sessions; Session Logger gives detail) (Tier 0). |
64
69
  | `list_unused_resources` | Unused WorkSpaces desktops and stopped/zero-capacity fleets worth reclaiming (Tier 0). |
65
70
 
@@ -23,7 +23,9 @@
23
23
  "appstream:DescribeStacks",
24
24
  "appstream:ListAssociatedFleets",
25
25
  "appstream:ListAssociatedStacks",
26
- "appstream:DescribeSessions"
26
+ "appstream:DescribeSessions",
27
+ "appstream:DescribeImages",
28
+ "appstream:DescribeImageBuilders"
27
29
  ],
28
30
  "Resource": "*"
29
31
  },
@@ -37,7 +39,8 @@
37
39
  "workspaces-web:ListUserSettings",
38
40
  "workspaces-web:ListNetworkSettings",
39
41
  "workspaces-web:GetUserSettings",
40
- "workspaces-web:GetNetworkSettings"
42
+ "workspaces-web:GetNetworkSettings",
43
+ "workspaces-web:GetDataProtectionSettings"
41
44
  ],
42
45
  "Resource": "*"
43
46
  },
@@ -60,7 +63,10 @@
60
63
  "cloudwatch:GetMetricData",
61
64
  "cloudwatch:ListMetrics",
62
65
  "application-autoscaling:DescribeScalingActivities",
63
- "ec2:DescribeInstances"
66
+ "ec2:DescribeInstances",
67
+ "cloudtrail:LookupEvents",
68
+ "servicequotas:ListServiceQuotas",
69
+ "servicequotas:GetServiceQuota"
64
70
  ],
65
71
  "Resource": "*"
66
72
  }
@@ -23,7 +23,9 @@
23
23
  "appstream:DescribeStacks",
24
24
  "appstream:ListAssociatedFleets",
25
25
  "appstream:ListAssociatedStacks",
26
- "appstream:DescribeSessions"
26
+ "appstream:DescribeSessions",
27
+ "appstream:DescribeImages",
28
+ "appstream:DescribeImageBuilders"
27
29
  ],
28
30
  "Resource": "*"
29
31
  },
@@ -37,7 +39,8 @@
37
39
  "workspaces-web:ListUserSettings",
38
40
  "workspaces-web:ListNetworkSettings",
39
41
  "workspaces-web:GetUserSettings",
40
- "workspaces-web:GetNetworkSettings"
42
+ "workspaces-web:GetNetworkSettings",
43
+ "workspaces-web:GetDataProtectionSettings"
41
44
  ],
42
45
  "Resource": "*"
43
46
  },
@@ -60,7 +63,10 @@
60
63
  "cloudwatch:GetMetricData",
61
64
  "cloudwatch:ListMetrics",
62
65
  "application-autoscaling:DescribeScalingActivities",
63
- "ec2:DescribeInstances"
66
+ "ec2:DescribeInstances",
67
+ "cloudtrail:LookupEvents",
68
+ "servicequotas:ListServiceQuotas",
69
+ "servicequotas:GetServiceQuota"
64
70
  ],
65
71
  "Resource": "*"
66
72
  },
@@ -23,7 +23,9 @@
23
23
  "appstream:DescribeStacks",
24
24
  "appstream:ListAssociatedFleets",
25
25
  "appstream:ListAssociatedStacks",
26
- "appstream:DescribeSessions"
26
+ "appstream:DescribeSessions",
27
+ "appstream:DescribeImages",
28
+ "appstream:DescribeImageBuilders"
27
29
  ],
28
30
  "Resource": "*"
29
31
  },
@@ -37,7 +39,8 @@
37
39
  "workspaces-web:ListUserSettings",
38
40
  "workspaces-web:ListNetworkSettings",
39
41
  "workspaces-web:GetUserSettings",
40
- "workspaces-web:GetNetworkSettings"
42
+ "workspaces-web:GetNetworkSettings",
43
+ "workspaces-web:GetDataProtectionSettings"
41
44
  ],
42
45
  "Resource": "*"
43
46
  },
@@ -60,7 +63,10 @@
60
63
  "cloudwatch:GetMetricData",
61
64
  "cloudwatch:ListMetrics",
62
65
  "application-autoscaling:DescribeScalingActivities",
63
- "ec2:DescribeInstances"
66
+ "ec2:DescribeInstances",
67
+ "cloudtrail:LookupEvents",
68
+ "servicequotas:ListServiceQuotas",
69
+ "servicequotas:GetServiceQuota"
64
70
  ],
65
71
  "Resource": "*"
66
72
  },
@@ -23,7 +23,9 @@
23
23
  "appstream:DescribeStacks",
24
24
  "appstream:ListAssociatedFleets",
25
25
  "appstream:ListAssociatedStacks",
26
- "appstream:DescribeSessions"
26
+ "appstream:DescribeSessions",
27
+ "appstream:DescribeImages",
28
+ "appstream:DescribeImageBuilders"
27
29
  ],
28
30
  "Resource": "*"
29
31
  },
@@ -37,7 +39,8 @@
37
39
  "workspaces-web:ListUserSettings",
38
40
  "workspaces-web:ListNetworkSettings",
39
41
  "workspaces-web:GetUserSettings",
40
- "workspaces-web:GetNetworkSettings"
42
+ "workspaces-web:GetNetworkSettings",
43
+ "workspaces-web:GetDataProtectionSettings"
41
44
  ],
42
45
  "Resource": "*"
43
46
  },
@@ -60,7 +63,10 @@
60
63
  "cloudwatch:GetMetricData",
61
64
  "cloudwatch:ListMetrics",
62
65
  "application-autoscaling:DescribeScalingActivities",
63
- "ec2:DescribeInstances"
66
+ "ec2:DescribeInstances",
67
+ "cloudtrail:LookupEvents",
68
+ "servicequotas:ListServiceQuotas",
69
+ "servicequotas:GetServiceQuota"
64
70
  ],
65
71
  "Resource": "*"
66
72
  },
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "workspaces-euc-mcp-server"
3
- version = "0.1.4"
3
+ version = "0.1.6"
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"
@@ -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,133 @@
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 governance tools (audit trail + service quotas)."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import types
10
+ from datetime import UTC, datetime
11
+
12
+ from workspaces_euc_mcp_server import consts
13
+ from workspaces_euc_mcp_server.tools import governance
14
+
15
+
16
+ class FakeFactory:
17
+ region = "ap-southeast-1"
18
+
19
+ def __init__(self, clients: dict[str, object]) -> None:
20
+ self._clients = clients
21
+
22
+ def client(self, service_name: str, region: str | None = None):
23
+ assert service_name in self._clients, f"unexpected client: {service_name}"
24
+ return self._clients[service_name]
25
+
26
+
27
+ def _event(name, source, user, *, read_only=False, error=None, resource=None):
28
+ detail = {"sourceIPAddress": "203.0.113.5", "awsRegion": "ap-southeast-1"}
29
+ if error:
30
+ detail["errorCode"] = error
31
+ return {
32
+ "EventName": name,
33
+ "EventSource": source,
34
+ "Username": user,
35
+ "ReadOnly": read_only,
36
+ "EventTime": datetime(2026, 5, 30, 9, 0, tzinfo=UTC),
37
+ "Resources": [{"ResourceName": resource}] if resource else [],
38
+ "CloudTrailEvent": json.dumps(detail),
39
+ }
40
+
41
+
42
+ def test_audit_trail_filters_euc_and_flags_destructive():
43
+ events = [
44
+ _event("TerminateWorkspaces", "workspaces.amazonaws.com", "admin", resource="ws-abc"),
45
+ _event("CreateFleet", "appstream.amazonaws.com", "ops"),
46
+ _event("RunInstances", "ec2.amazonaws.com", "someone"), # non-EUC, must be dropped
47
+ ]
48
+ trail = types.SimpleNamespace(lookup_events=lambda **_: {"Events": events})
49
+ factory = FakeFactory({consts.CLOUDTRAIL_API: trail})
50
+
51
+ report = governance.get_euc_audit_trail_core(factory, "ap-southeast-1", lookback_days=7)
52
+
53
+ names = {e.event_name for e in report.events}
54
+ assert names == {"TerminateWorkspaces", "CreateFleet"} # EC2 dropped
55
+ assert report.total_events == 2
56
+ assert any(
57
+ "Destructive" in f.issue and "TerminateWorkspaces" in f.issue for f in report.findings
58
+ )
59
+ # Service label resolved from the event source.
60
+ assert any(e.service == "Amazon WorkSpaces" for e in report.events)
61
+
62
+
63
+ def test_audit_trail_flags_access_denied():
64
+ events = [_event("DeletePortal", "workspaces-web.amazonaws.com", "x", error="AccessDenied")]
65
+ trail = types.SimpleNamespace(lookup_events=lambda **_: {"Events": events})
66
+ factory = FakeFactory({consts.CLOUDTRAIL_API: trail})
67
+
68
+ report = governance.get_euc_audit_trail_core(factory, "ap-southeast-1")
69
+
70
+ assert any("AccessDenied" in f.issue and f.severity == "warning" for f in report.findings)
71
+
72
+
73
+ def test_audit_trail_lookback_capped_at_90():
74
+ trail = types.SimpleNamespace(lookup_events=lambda **_: {"Events": []})
75
+ factory = FakeFactory({consts.CLOUDTRAIL_API: trail})
76
+ report = governance.get_euc_audit_trail_core(factory, "ap-southeast-1", lookback_days=365)
77
+ assert report.lookback_days == 90
78
+
79
+
80
+ def test_service_quotas_computes_headroom_and_flags():
81
+ quotas = {
82
+ "Quotas": [
83
+ {
84
+ "QuotaName": "WorkSpaces",
85
+ "QuotaCode": "L-1",
86
+ "Value": 200.0,
87
+ "Adjustable": True,
88
+ "UsageMetric": {
89
+ "MetricNamespace": "AWS/Usage",
90
+ "MetricName": "ResourceCount",
91
+ "MetricDimensions": {"Service": "WorkSpaces", "Resource": "WorkSpace"},
92
+ "MetricStatisticRecommendation": "Maximum",
93
+ },
94
+ },
95
+ {
96
+ "QuotaName": "WorkSpaces Pools",
97
+ "QuotaCode": "L-2",
98
+ "Value": 10.0,
99
+ "Adjustable": False,
100
+ "UsageMetric": {
101
+ "MetricNamespace": "AWS/Usage",
102
+ "MetricName": "ResourceCount",
103
+ "MetricDimensions": {"Service": "WorkSpaces", "Resource": "Pool"},
104
+ "MetricStatisticRecommendation": "Maximum",
105
+ },
106
+ },
107
+ {"QuotaName": "Zero thing", "QuotaCode": "L-3", "Value": 0.0, "Adjustable": True},
108
+ ]
109
+ }
110
+ sq = types.SimpleNamespace(list_service_quotas=lambda **_: quotas)
111
+ # u0 -> WorkSpaces (14/200 = 7%), u1 -> Pools (9/10 = 90% -> flagged, not adjustable)
112
+ cw = types.SimpleNamespace(
113
+ get_metric_data=lambda **_: {
114
+ "MetricDataResults": [
115
+ {"Id": "u0", "Values": [14.0]},
116
+ {"Id": "u1", "Values": [9.0]},
117
+ ]
118
+ }
119
+ )
120
+ factory = FakeFactory({consts.SERVICE_QUOTAS_API: sq, consts.CLOUDWATCH_API: cw})
121
+
122
+ report = governance.get_euc_service_quotas_core(
123
+ factory, "ap-southeast-1", service="workspaces", approaching_pct=80.0
124
+ )
125
+
126
+ by_name = {q.quota_name: q for q in report.quotas}
127
+ assert "Zero thing" not in by_name # zero-limit hidden by default
128
+ assert by_name["WorkSpaces"].utilization_pct == 7.0
129
+ assert by_name["WorkSpaces Pools"].utilization_pct == 90.0
130
+ # Only the Pools quota (90% >= 80) is flagged, and noted as non-adjustable.
131
+ assert len(report.findings) == 1
132
+ assert "WorkSpaces Pools" in report.findings[0].target
133
+ assert "NOT adjustable" in report.findings[0].issue
@@ -0,0 +1,115 @@
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 Applications image-audit tool, using fake boto3 clients."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import types
9
+ from datetime import UTC, datetime, timedelta
10
+
11
+ from workspaces_euc_mcp_server import consts
12
+ from workspaces_euc_mcp_server.tools import images
13
+
14
+
15
+ class FakeFactory:
16
+ region = "ap-southeast-1"
17
+
18
+ def __init__(self, client: object) -> None:
19
+ self._client = client
20
+
21
+ def client(self, service_name: str, region: str | None = None):
22
+ assert service_name == consts.APPSTREAM_API
23
+ return self._client
24
+
25
+
26
+ def _appstream(images_by_type: dict[str, list[dict]], builders: list[dict]):
27
+ def describe_images(**kwargs):
28
+ return {"Images": images_by_type.get(kwargs.get("Type"), [])}
29
+
30
+ def describe_image_builders(**_):
31
+ return {"ImageBuilders": builders}
32
+
33
+ return types.SimpleNamespace(
34
+ describe_images=describe_images, describe_image_builders=describe_image_builders
35
+ )
36
+
37
+
38
+ def test_audit_flags_stale_base_pinned_agent_and_running_builder():
39
+ old_base = datetime.now(UTC) - timedelta(days=400)
40
+ fresh_base = datetime.now(UTC) - timedelta(days=10)
41
+ client = _appstream(
42
+ {
43
+ "PRIVATE": [
44
+ {
45
+ "Name": "StaleImage",
46
+ "Visibility": "PRIVATE",
47
+ "Platform": "WINDOWS_SERVER_2022",
48
+ "State": "AVAILABLE",
49
+ "AppstreamAgentVersion": "10-02-2025", # pinned, not LATEST
50
+ "Applications": [{"Name": "chrome", "Enabled": True}],
51
+ "PublicBaseImageReleasedDate": old_base,
52
+ "CreatedTime": old_base,
53
+ },
54
+ {
55
+ "Name": "GoodImage",
56
+ "Visibility": "PRIVATE",
57
+ "Platform": "WINDOWS_SERVER_2022",
58
+ "State": "AVAILABLE",
59
+ "AppstreamAgentVersion": "LATEST",
60
+ "Applications": [{"Name": "vscode", "Enabled": True}],
61
+ "PublicBaseImageReleasedDate": fresh_base,
62
+ "CreatedTime": fresh_base,
63
+ },
64
+ ],
65
+ "SHARED": [],
66
+ },
67
+ builders=[
68
+ {"Name": "Builder-Idle", "State": "STOPPED", "Platform": "WINDOWS_SERVER_2022"},
69
+ {"Name": "Builder-Live", "State": "RUNNING", "Platform": "WINDOWS_SERVER_2025"},
70
+ ],
71
+ )
72
+ report = images.audit_application_images_core(FakeFactory(client), "ap-southeast-1")
73
+
74
+ assert report.image_count == 2
75
+ assert report.image_builder_count == 2
76
+ assert report.running_image_builders == 1
77
+
78
+ issues = {(f.target, f.issue) for f in report.findings}
79
+ # Stale base + pinned agent on StaleImage; running builder flagged; GoodImage clean.
80
+ assert any(t == "StaleImage" and "base image released" in i for t, i in issues)
81
+ assert any(t == "StaleImage" and "pinned" in i for t, i in issues)
82
+ assert any(t == "Builder-Live" and "RUNNING" in i for t, i in issues)
83
+ assert not any(t == "GoodImage" for t, _ in issues)
84
+
85
+
86
+ def test_audit_flags_shared_visibility_and_records_errors():
87
+ from botocore.exceptions import ClientError
88
+
89
+ def describe_images(**kwargs):
90
+ if kwargs.get("Type") == "SHARED":
91
+ return {
92
+ "Images": [
93
+ {
94
+ "Name": "SharedIn",
95
+ "Visibility": "SHARED",
96
+ "State": "AVAILABLE",
97
+ "AppstreamAgentVersion": "LATEST",
98
+ }
99
+ ]
100
+ }
101
+ return {"Images": []}
102
+
103
+ def describe_image_builders(**_):
104
+ raise ClientError(
105
+ {"Error": {"Code": "AccessDeniedException", "Message": "no"}},
106
+ "DescribeImageBuilders",
107
+ )
108
+
109
+ client = types.SimpleNamespace(
110
+ describe_images=describe_images, describe_image_builders=describe_image_builders
111
+ )
112
+ report = images.audit_application_images_core(FakeFactory(client), "ap-southeast-1")
113
+
114
+ assert any(f.target == "SharedIn" and "SHARED" in f.issue for f in report.findings)
115
+ assert any(e.operation == "DescribeImageBuilders" for e in report.errors)