workspaces-euc-mcp-server 0.1.3__tar.gz → 0.1.5__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.3 → workspaces_euc_mcp_server-0.1.5}/CHANGELOG.md +32 -0
  2. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/DESIGN.md +22 -8
  3. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/PKG-INFO +7 -2
  4. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/README.md +6 -1
  5. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/iam/tier0-diagnostics.json +7 -2
  6. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/iam/tier1-cost.json +7 -2
  7. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/iam/tier2-lifecycle.json +7 -2
  8. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/iam/tier3-destructive.json +7 -2
  9. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/pyproject.toml +1 -1
  10. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/tests/test_cost.py +43 -0
  11. workspaces_euc_mcp_server-0.1.5/tests/test_governance.py +133 -0
  12. workspaces_euc_mcp_server-0.1.5/tests/test_images.py +115 -0
  13. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/workspaces_euc_mcp_server/__init__.py +1 -1
  14. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/workspaces_euc_mcp_server/consts.py +46 -0
  15. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/workspaces_euc_mcp_server/models.py +121 -0
  16. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/workspaces_euc_mcp_server/server.py +4 -0
  17. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/workspaces_euc_mcp_server/tools/cost.py +27 -1
  18. workspaces_euc_mcp_server-0.1.5/workspaces_euc_mcp_server/tools/governance.py +361 -0
  19. workspaces_euc_mcp_server-0.1.5/workspaces_euc_mcp_server/tools/images.py +222 -0
  20. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/.dockerignore +0 -0
  21. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/.github/workflows/ci.yml +0 -0
  22. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/.github/workflows/docker-publish.yml +0 -0
  23. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/.github/workflows/publish.yml +0 -0
  24. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/.gitignore +0 -0
  25. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/.pre-commit-config.yaml +0 -0
  26. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/Dockerfile +0 -0
  27. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/LICENSE +0 -0
  28. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/iam/README.md +0 -0
  29. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/scripts/smoke_readonly.py +0 -0
  30. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/tests/__init__.py +0 -0
  31. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/tests/test_clients.py +0 -0
  32. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/tests/test_destructive.py +0 -0
  33. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/tests/test_diagnostics.py +0 -0
  34. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/tests/test_inventory.py +0 -0
  35. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/tests/test_lifecycle.py +0 -0
  36. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/tests/test_naming.py +0 -0
  37. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/tests/test_no_embedded_secrets.py +0 -0
  38. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/tests/test_performance.py +0 -0
  39. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/tests/test_pricing.py +0 -0
  40. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/tests/test_reporting.py +0 -0
  41. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/tests/test_secure_browser.py +0 -0
  42. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/workspaces_euc_mcp_server/clients.py +0 -0
  43. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/workspaces_euc_mcp_server/tools/__init__.py +0 -0
  44. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/workspaces_euc_mcp_server/tools/_common.py +0 -0
  45. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/workspaces_euc_mcp_server/tools/destructive.py +0 -0
  46. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/workspaces_euc_mcp_server/tools/diagnostics.py +0 -0
  47. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/workspaces_euc_mcp_server/tools/inventory.py +0 -0
  48. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/workspaces_euc_mcp_server/tools/lifecycle.py +0 -0
  49. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/workspaces_euc_mcp_server/tools/performance.py +0 -0
  50. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/workspaces_euc_mcp_server/tools/pricing.py +0 -0
  51. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/workspaces_euc_mcp_server/tools/reporting.py +0 -0
  52. {workspaces_euc_mcp_server-0.1.3 → workspaces_euc_mcp_server-0.1.5}/workspaces_euc_mcp_server/tools/secure_browser.py +0 -0
@@ -5,6 +5,38 @@ All notable changes to this project are documented here. The format is based on
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [0.1.5] - 2026-06-02
9
+
10
+ ### Added
11
+ - `get_euc_audit_trail` — a new read-only (Tier 0) tool that reports recent EUC management activity
12
+ from CloudTrail (always-on `LookupEvents`, 90-day window, no trail required) across WorkSpaces
13
+ Personal/Pools/Core, WorkSpaces Applications, Secure Browser, and Core Managed Instances.
14
+ Mutations-only by default ("who created/modified/terminated what"); flags destructive actions and
15
+ errors (e.g. AccessDenied). Adds `cloudtrail:LookupEvents` to every IAM tier.
16
+ - `get_euc_service_quotas` — a new read-only (Tier 0) tool that reports Service Quotas limits per
17
+ EUC service and, where AWS publishes a linked usage metric (`AWS/Usage` `ResourceCount`), the
18
+ current usage and utilisation %, flagging quotas approaching their limit (capacity planning).
19
+ Adds `servicequotas:ListServiceQuotas` / `GetServiceQuota` to every IAM tier.
20
+ - `audit_application_images` — a new read-only (Tier 0) tool that audits WorkSpaces Applications
21
+ (AppStream 2.0) **images and image builders**: lists your PRIVATE/SHARED images (skipping PUBLIC
22
+ base images) and flags stale base images (likely unpatched OS), pinned/old AppStream agents,
23
+ non-AVAILABLE or errored images, SHARED cross-account visibility, and image builders left
24
+ **RUNNING** (per-hour cost + interactive admin surface). Adds `appstream:DescribeImages` and
25
+ `appstream:DescribeImageBuilders` to every IAM tier.
26
+
27
+ ### Docs
28
+ - README and DESIGN.md reconciled to the shipped state: 21 read-only tools (Tiers 0–1) + 10 write
29
+ (Tier 2) + 3 destructive (Tier 3); `tools/` layout, §5 tool catalog (image audit + governance),
30
+ and the Tier 0 IAM action list all updated.
31
+
32
+ ## [0.1.4] - 2026-06-02
33
+
34
+ ### Added
35
+ - `get_euc_cost_summary` now returns `by_period` — a per-bucket time series (one entry per day for
36
+ `DAILY`, per month for `MONTHLY`), each with its own per-service split. Previously the tool
37
+ collapsed all periods into per-service totals, so a `DAILY` request lost the daily breakdown and
38
+ clients had to query each day individually to chart trends.
39
+
8
40
  ## [0.1.3] - 2026-06-02
9
41
 
10
42
  ### Fixed
@@ -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)
@@ -137,8 +141,15 @@ and return a synthesized result, not raw API passthroughs.
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.3
3
+ Version: 0.1.5
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
  |------|-------------|
@@ -89,6 +91,9 @@ 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). |
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). |
92
97
  | `get_secure_browser_portal_details` | Resolves a Secure Browser portal's user settings (clipboard/print/download controls + timeouts), network, and attached policies (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). |
@@ -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
  |------|-------------|
@@ -59,6 +61,9 @@ 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). |
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). |
62
67
  | `get_secure_browser_portal_details` | Resolves a Secure Browser portal's user settings (clipboard/print/download controls + timeouts), network, and attached policies (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). |
@@ -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
  },
@@ -60,7 +62,10 @@
60
62
  "cloudwatch:GetMetricData",
61
63
  "cloudwatch:ListMetrics",
62
64
  "application-autoscaling:DescribeScalingActivities",
63
- "ec2:DescribeInstances"
65
+ "ec2:DescribeInstances",
66
+ "cloudtrail:LookupEvents",
67
+ "servicequotas:ListServiceQuotas",
68
+ "servicequotas:GetServiceQuota"
64
69
  ],
65
70
  "Resource": "*"
66
71
  }
@@ -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
  },
@@ -60,7 +62,10 @@
60
62
  "cloudwatch:GetMetricData",
61
63
  "cloudwatch:ListMetrics",
62
64
  "application-autoscaling:DescribeScalingActivities",
63
- "ec2:DescribeInstances"
65
+ "ec2:DescribeInstances",
66
+ "cloudtrail:LookupEvents",
67
+ "servicequotas:ListServiceQuotas",
68
+ "servicequotas:GetServiceQuota"
64
69
  ],
65
70
  "Resource": "*"
66
71
  },
@@ -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
  },
@@ -60,7 +62,10 @@
60
62
  "cloudwatch:GetMetricData",
61
63
  "cloudwatch:ListMetrics",
62
64
  "application-autoscaling:DescribeScalingActivities",
63
- "ec2:DescribeInstances"
65
+ "ec2:DescribeInstances",
66
+ "cloudtrail:LookupEvents",
67
+ "servicequotas:ListServiceQuotas",
68
+ "servicequotas:GetServiceQuota"
64
69
  ],
65
70
  "Resource": "*"
66
71
  },
@@ -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
  },
@@ -60,7 +62,10 @@
60
62
  "cloudwatch:GetMetricData",
61
63
  "cloudwatch:ListMetrics",
62
64
  "application-autoscaling:DescribeScalingActivities",
63
- "ec2:DescribeInstances"
65
+ "ec2:DescribeInstances",
66
+ "cloudtrail:LookupEvents",
67
+ "servicequotas:ListServiceQuotas",
68
+ "servicequotas:GetServiceQuota"
64
69
  ],
65
70
  "Resource": "*"
66
71
  },
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "workspaces-euc-mcp-server"
3
- version = "0.1.3"
3
+ version = "0.1.5"
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"
@@ -172,6 +172,49 @@ def test_cost_summary_keyword_matches_variants_and_excludes_noneuc():
172
172
  assert summary.total == 1701.82
173
173
 
174
174
 
175
+ def test_cost_summary_daily_returns_per_period_time_series():
176
+ # DAILY granularity must preserve the per-day breakdown in by_period (for charts), not just
177
+ # collapse everything into by_service totals.
178
+ ce = types.SimpleNamespace(
179
+ get_cost_and_usage=lambda **_: {
180
+ "ResultsByTime": [
181
+ {
182
+ "TimePeriod": {"Start": "2026-05-01", "End": "2026-05-02"},
183
+ "Groups": [
184
+ {
185
+ "Keys": ["Amazon WorkSpaces Applications"],
186
+ "Metrics": {"UnblendedCost": {"Amount": "30.00", "Unit": "USD"}},
187
+ }
188
+ ],
189
+ },
190
+ {
191
+ "TimePeriod": {"Start": "2026-05-02", "End": "2026-05-03"},
192
+ "Groups": [
193
+ {
194
+ "Keys": ["Amazon WorkSpaces Applications"],
195
+ "Metrics": {"UnblendedCost": {"Amount": "45.00", "Unit": "USD"}},
196
+ }
197
+ ],
198
+ },
199
+ ]
200
+ }
201
+ )
202
+ factory = FakeFactory({consts.COST_EXPLORER_API: ce})
203
+
204
+ summary = cost.get_euc_cost_summary_core(
205
+ factory, granularity="DAILY", start_date="2026-05-01", end_date="2026-05-03"
206
+ )
207
+
208
+ # Aggregate total preserved...
209
+ assert summary.total == 75.0
210
+ # ...and the daily series is available, ordered by date.
211
+ assert [(p.start, p.total) for p in summary.by_period] == [
212
+ ("2026-05-01", 30.0),
213
+ ("2026-05-02", 45.0),
214
+ ]
215
+ assert summary.by_period[0].by_service[0].service == "Amazon WorkSpaces Applications"
216
+
217
+
175
218
  def test_cost_summary_explicit_date_range_overrides_lookback():
176
219
  captured: dict = {}
177
220
 
@@ -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)
@@ -3,4 +3,4 @@
3
3
  # A copy of the License is located at http://www.apache.org/licenses/LICENSE-2.0
4
4
  """MCP server for administering the Amazon WorkSpaces End User Computing portfolio."""
5
5
 
6
- __version__ = "0.1.3"
6
+ __version__ = "0.1.5"
@@ -22,6 +22,8 @@ CLOUDWATCH_API = "cloudwatch" # Telemetry for diagnostics/cost tools.
22
22
  EC2_API = "ec2" # Used to enrich WorkSpaces Core Managed Instances with EC2 details.
23
23
  COST_EXPLORER_API = "ce" # Cost Explorer (global; account-wide, not region-scoped).
24
24
  PRICING_API = "pricing" # AWS Price List (global).
25
+ CLOUDTRAIL_API = "cloudtrail" # Management-event history (LookupEvents) for the audit trail.
26
+ SERVICE_QUOTAS_API = "service-quotas" # Quota limits + linked usage metrics for headroom.
25
27
 
26
28
  # Cost Explorer is a global endpoint served from us-east-1 regardless of the working region.
27
29
  COST_EXPLORER_REGION = "us-east-1"
@@ -44,6 +46,50 @@ PRODUCT_WORKSPACES_APPLICATIONS = "Amazon WorkSpaces Applications"
44
46
  PRODUCT_SECURE_BROWSER = "Amazon WorkSpaces Secure Browser"
45
47
  PRODUCT_WORKSPACES_CORE_INSTANCES = "Amazon WorkSpaces Core Managed Instances"
46
48
 
49
+ # --- Governance: CloudTrail event sources + Service Quotas codes per EUC service group ---
50
+ # CloudTrail EventSource -> human product label (workspaces source covers Personal/Pools/Core).
51
+ EUC_AUDIT_SOURCES = {
52
+ "workspaces.amazonaws.com": "Amazon WorkSpaces",
53
+ "appstream.amazonaws.com": PRODUCT_WORKSPACES_APPLICATIONS,
54
+ "workspaces-web.amazonaws.com": PRODUCT_SECURE_BROWSER,
55
+ "workspaces-instances.amazonaws.com": PRODUCT_WORKSPACES_CORE_INSTANCES,
56
+ }
57
+ # Friendly service filter -> the CloudTrail EventSource(s) it selects.
58
+ EUC_AUDIT_SERVICE_FILTER = {
59
+ "all": list(EUC_AUDIT_SOURCES),
60
+ "workspaces": ["workspaces.amazonaws.com"],
61
+ "applications": ["appstream.amazonaws.com"],
62
+ "secure-browser": ["workspaces-web.amazonaws.com"],
63
+ "core": ["workspaces-instances.amazonaws.com"],
64
+ }
65
+ # Service Quotas service code -> human product label.
66
+ EUC_QUOTA_SERVICE_CODES = {
67
+ "workspaces": "Amazon WorkSpaces",
68
+ "appstream2": PRODUCT_WORKSPACES_APPLICATIONS,
69
+ "workspaces-web": PRODUCT_SECURE_BROWSER,
70
+ "workspaces-instances": PRODUCT_WORKSPACES_CORE_INSTANCES,
71
+ }
72
+ # Friendly service filter -> the Service Quotas code(s) it selects.
73
+ EUC_QUOTA_SERVICE_FILTER = {
74
+ "all": list(EUC_QUOTA_SERVICE_CODES),
75
+ "workspaces": ["workspaces"],
76
+ "applications": ["appstream2"],
77
+ "secure-browser": ["workspaces-web"],
78
+ "core": ["workspaces-instances"],
79
+ }
80
+ # CloudTrail event-name prefixes treated as destructive/high-impact in audit findings.
81
+ AUDIT_DESTRUCTIVE_PREFIXES = (
82
+ "Terminate",
83
+ "Delete",
84
+ "Reboot",
85
+ "Rebuild",
86
+ "Restore",
87
+ "Revoke",
88
+ "Disassociate",
89
+ "Stop",
90
+ "Expire",
91
+ )
92
+
47
93
  # Legacy / former product names mapped to their current official name. Accept these as INPUT
48
94
  # (users will keep saying them) but always emit the current name in output. This is surfaced to the
49
95
  # MCP client model via the server instructions and tool descriptions so a query about, say,