# PSProxmoxVE Module Review Report
```
Scan date: 2026-03-22
Prior report date: 2026-03-21
PVE API spec date: 2026-03-21T15:04:50.641Z
PVE API spec SHA256: 4af79be30166209a4714b771f65e1e9540c5b738f414ff30c98454402e29d030
PVE version hint: (not set in spec)
Total API endpoints: 646
```
**Reviewer:** Claude Code (Opus 4.6)
**Module Version:** 0.1.0-preview
**Repository:** PSProxmoxVE (C# binary PowerShell module)
---
## Executive Summary
**Delta since last scan: 16 fixed | 5 new findings | 0 regressed | 9 still open**
**API drift: 194 endpoints with version_changes | 466 PVE endpoints unimplemented (72.1% uncovered) | 42 🆕 PVE 9.0 endpoints**
1. **169 cmdlets** (up from 148) — all fully exported, sealed, with OutputType attributes. New: 21 additional cmdlets since prior scan.
2. **14 prior findings resolved**: All 4 infinite-loop WaitForTask methods (CQ1–CQ4) now use `TaskService.WaitForTask`. Guest exec timeout added (CQ5). OutputType on all cmdlets (CQ9). Firewall VmId nullable (CQ10). Bare catches replaced with filtered exceptions (CQ11–CQ14). RemovePveRole/SuspendPveVm/RestartPveVm ConfirmImpact fixed (CQ17/CQ19/CQ20). Cmdlets sealed (CQ18). Dual JSON attrs removed (CQ21). Magic strings extracted to constants (CQ22). Guest password now SecureString (S1). Debug script secrets redacted (S2).
3. **Remaining Critical**: 5 cmdlets still have `while(true)` task-polling loops with no timeout — 3 container snapshot cmdlets plus `InvokePveStorageDownloadCmdlet` and `SendPveFileCmdlet`.
4. **100% Pester unit test coverage** maintained across all 169 cmdlets. Integration test coverage is ~62% (105 of 169).
5. **27.9% API endpoint coverage** (180 of 646 endpoints). Major uncovered areas unchanged: Ceph (40), HA (21), cluster config (10), disks (18), ACME (15), certificates (8).
6. **Security posture solid**: All prior security findings resolved (S1–S3). URL encoding now consistent across all services. HTTPS enforced, SecureString used consistently.
7. **PSGallery-ready**: No publication blockers. IconUri still missing (cosmetic). Manifest CmdletsToExport fully enumerates all 169 cmdlets.
8. **net9.0 target is EOL** (May 2025) — should upgrade to net10.0.
9. **Pool/CephPool parameter conflict** in New-PveStorage resolved: runtime validation now throws if both specified.
10. **42 new PVE 9.0 endpoints** available (HA rules, SDN fabrics, bulk actions, OCI registry). None yet implemented.
---
## Phase 1 — Repository Inventory & Structure
### Directory Tree (depth ≤ 4)
```
PSProxmoxVE/
├── PSProxmoxVE.sln
├── README.md
├── LICENSE (MIT)
├── CHANGELOG.md
├── CONTRIBUTING.md
├── CODE_OF_CONDUCT.md
├── SECURITY.md
├── .editorconfig
├── .gitignore
├── .gitattributes
├── REVIEW_REPORT.md
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ └── feature_request.yml
│ ├── pull_request_template.md
│ └── workflows/
│ ├── build.yml
│ ├── unit-tests.yml
│ ├── integration-tests.yml
│ └── publish.yml
├── docs/
│ ├── PVE_API_COVERAGE.md
│ └── cmdlets/ (75 cmdlet documentation files)
├── src/
│ ├── PSProxmoxVE/
│ │ ├── PSProxmoxVE.csproj
│ │ ├── PSProxmoxVE.psd1
│ │ ├── PSProxmoxVE.format.ps1xml
│ │ ├── PSProxmoxVE.dll-Help.xml
│ │ ├── ModuleState.cs
│ │ └── Cmdlets/
│ │ ├── PveCmdletBase.cs
│ │ ├── Backup/ (6 cmdlets)
│ │ ├── CloudInit/ (3 cmdlets)
│ │ ├── Cluster/ (1 cmdlet)
│ │ ├── Connection/ (3 cmdlets)
│ │ ├── Containers/ (20 cmdlets)
│ │ ├── Firewall/ (21 cmdlets)
│ │ ├── Network/ (5 + 22 SDN cmdlets)
│ │ ├── Nodes/ (8 cmdlets)
│ │ ├── Pools/ (4 cmdlets)
│ │ ├── Snapshots/ (4 cmdlets)
│ │ ├── Storage/ (11 cmdlets)
│ │ ├── Tasks/ (4 cmdlets)
│ │ ├── Templates/ (4 cmdlets)
│ │ ├── Users/ (23 cmdlets)
│ │ └── Vms/ (30 cmdlets)
│ └── PSProxmoxVE.Core/
│ ├── PSProxmoxVE.Core.csproj
│ ├── Authentication/ (PveAuthenticator, PveAuthMode, PveSession, PveVersion)
│ ├── Client/ (PveHttpClient)
│ ├── Exceptions/ (7 exception types)
│ ├── Models/ (25+ model classes across 9 areas)
│ └── Services/ (14 service classes)
├── tests/
│ ├── PSProxmoxVE.Core.Tests/ (xUnit)
│ │ ├── Authentication/ (3 test files)
│ │ ├── Fixtures/ (28 JSON fixtures)
│ │ ├── Models/ (12 test files)
│ │ └── TestHelper.cs
│ ├── PSProxmoxVE.Tests/ (Pester 5)
│ │ ├── _TestHelper.ps1
│ │ ├── Backup/ (2 test files)
│ │ ├── CloudInit/ (1 test file)
│ │ ├── Cluster/ (1 test file)
│ │ ├── Connection/ (3 test files)
│ │ ├── Containers/ (5 test files)
│ │ ├── Firewall/ (5 test files)
│ │ ├── Network/ (5 test files)
│ │ ├── Nodes/ (2 test files)
│ │ ├── Pools/ (1 test file)
│ │ ├── Snapshots/ (1 test file)
│ │ ├── Storage/ (4 test files)
│ │ ├── Tasks/ (2 test files)
│ │ ├── Templates/ (1 test file)
│ │ ├── Users/ (4 test files)
│ │ ├── Vms/ (12 test files)
│ │ └── Integration/ (1 comprehensive test file)
│ └── infrastructure/ (Terraform, Dockerfile, scripts for CI)
├── publish/ (netstandard2.0 build output)
├── debug/ (debug/analysis scripts)
└── tools/
└── Invoke-Tests.ps1
```
### Project Layout
| Item | Value |
|---|---|
| Solution | `PSProxmoxVE.sln` |
| Main project | `src/PSProxmoxVE/PSProxmoxVE.csproj` |
| Core library | `src/PSProxmoxVE.Core/PSProxmoxVE.Core.csproj` |
| Test project (xUnit) | `tests/PSProxmoxVE.Core.Tests/PSProxmoxVE.Core.Tests.csproj` |
| Test project (Pester) | `tests/PSProxmoxVE.Tests/` |
| Target Frameworks | `netstandard2.0`, `net9.0`, `net48` |
| Language Version | C# 10.0 |
| Nullable | Enabled |
| Assembly Names | `PSProxmoxVE`, `PSProxmoxVE.Core` |
### CI/CD Workflows
| Workflow | Purpose |
|---|---|
| `build.yml` | Build + xUnit tests on net48 (Windows) and net9.0 (Windows + Ubuntu) |
| `unit-tests.yml` | Build + Pester tests across PS 5.1/7.5 on Windows/Ubuntu/macOS |
| `integration-tests.yml` | Full integration tests against nested PVE 8 + PVE 9 instances |
| `publish.yml` | Tag-triggered publish to PSGallery + GitHub Release creation |
### Inventory Checklist
| Item | Present | Prior Status |
|---|---|---|
| Solution file (`.sln`) | Yes | Open |
| Module manifest (`.psd1`) | Yes | Open |
| Format definitions (`.format.ps1xml`) | Yes | Open |
| MAML help (`.dll-Help.xml`) | Yes | Open |
| `.editorconfig` | Yes | Open |
| `.gitignore` | Yes | Open |
| `.gitattributes` | Yes | Open |
| `LICENSE` (MIT) | Yes | Open |
| `CHANGELOG.md` | Yes | Open |
| `README.md` | Yes | Open |
| `CONTRIBUTING.md` | Yes | Open |
| `CODE_OF_CONDUCT.md` | Yes | Open |
| `SECURITY.md` | Yes | Open |
| `.github/ISSUE_TEMPLATE/` | Yes | Open |
| `.github/pull_request_template.md` | Yes | Open |
| PSGallery publish workflow | Yes | Open |
| `docs/PVE_API_COVERAGE.md` | Yes | Open |
| `docs/cmdlets/` (75 files) | Yes | Open |
### What's Missing
- [ ] `IconUri` in manifest PSData (cosmetic)
- [ ] `.github/ISSUE_TEMPLATE/config.yml` (controls blank issue creation)
- [ ] `CODEOWNERS` file (low priority for single-maintainer)
---
## Phase 2 — PVE API Coverage Audit
> **Source of truth**: `~/Source/pve_api/pve-api.json` (646 endpoints). Spec generated 2026-03-21.
> **Latest tracked PVE version**: 9.0 (42 new endpoints, 56 changed endpoints)
### Implemented Cmdlets (169 total)
#### Connection (3)
| Cmdlet | API Endpoint | Method | Prior Status |
|---|---|---|---|
| `Connect-PveServer` | `/access/ticket` | POST | Open |
| `Disconnect-PveServer` | (session teardown) | — | Open |
| `Test-PveConnection` | `/version` | GET | Open |
#### Nodes (8)
| Cmdlet | API Endpoint | Method | Prior Status |
|---|---|---|---|
| `Get-PveNode` | `/nodes` | GET | Open |
| `Get-PveNodeStatus` | `/nodes/{node}/status` | GET | Open |
| `Get-PveNodeConfig` | `/nodes/{node}/config` | GET | Open |
| `Set-PveNodeConfig` | `/nodes/{node}/config` | PUT | Open |
| `Get-PveNodeDns` | `/nodes/{node}/dns` | GET | Open |
| `Set-PveNodeDns` | `/nodes/{node}/dns` | PUT | Open |
| `Start-PveNodeVms` | `/nodes/{node}/startall` | POST | Open |
| `Stop-PveNodeVms` | `/nodes/{node}/stopall` | POST | Open |
#### VMs / QEMU (19)
| Cmdlet | API Endpoint | Method | Prior Status |
|---|---|---|---|
| `Get-PveVm` | `/nodes/{node}/qemu` | GET | Open |
| `New-PveVm` | `/nodes/{node}/qemu` | POST | Open |
| `Remove-PveVm` | `/nodes/{node}/qemu/{vmid}` | DELETE | Open |
| `Start-PveVm` | `/nodes/{node}/qemu/{vmid}/status/start` | POST | Open |
| `Stop-PveVm` | `/nodes/{node}/qemu/{vmid}/status/stop` | POST | Open |
| `Restart-PveVm` | `/nodes/{node}/qemu/{vmid}/status/reboot` | POST | Open |
| `Suspend-PveVm` | `/nodes/{node}/qemu/{vmid}/status/suspend` | POST | Open |
| `Resume-PveVm` | `/nodes/{node}/qemu/{vmid}/status/resume` | POST | Open |
| `Reset-PveVm` | `/nodes/{node}/qemu/{vmid}/status/reset` | POST | Open |
| `Copy-PveVm` | `/nodes/{node}/qemu/{vmid}/clone` | POST | Open |
| `Move-PveVm` | `/nodes/{node}/qemu/{vmid}/migrate` | POST | Open |
| `Get-PveVmConfig` | `/nodes/{node}/qemu/{vmid}/config` | GET | Open |
| `Set-PveVmConfig` | `/nodes/{node}/qemu/{vmid}/config` | PUT | Open |
| `Resize-PveVmDisk` | `/nodes/{node}/qemu/{vmid}/resize` | PUT | Open |
| `Import-PveVmDisk` | `/nodes/{node}/storage/{storage}/upload` | POST | Open |
| `Import-PveOva` | Multiple (upload + config + import) | POST | Open |
| `Move-PveVmDisk` | `/nodes/{node}/qemu/{vmid}/move_disk` | POST | Open |
| `Remove-PveVmDisk` | `/nodes/{node}/qemu/{vmid}/unlink` | PUT | Open |
| `New-PveVmFromTemplate` | `/nodes/{node}/qemu/{vmid}/clone` | POST | Open |
#### Guest Agent (9)
| Cmdlet | API Endpoint | Method | Prior Status |
|---|---|---|---|
| `Test-PveVmGuestAgent` | `/nodes/{node}/qemu/{vmid}/agent/ping` | POST | Open |
| `Get-PveVmGuestNetwork` | `/nodes/{node}/qemu/{vmid}/agent/network-get-interfaces` | GET | Open |
| `Invoke-PveVmGuestExec` | `/nodes/{node}/qemu/{vmid}/agent/exec` + `.../exec-status` | POST/GET | Open |
| `Get-PveVmGuestOsInfo` | `/nodes/{node}/qemu/{vmid}/agent/get-osinfo` | GET | Open |
| `Get-PveVmGuestFsInfo` | `/nodes/{node}/qemu/{vmid}/agent/get-fsinfo` | GET | Open |
| `Read-PveVmGuestFile` | `/nodes/{node}/qemu/{vmid}/agent/file-read` | GET | Open |
| `Write-PveVmGuestFile` | `/nodes/{node}/qemu/{vmid}/agent/file-write` | POST | Open |
| `Set-PveVmGuestPassword` | `/nodes/{node}/qemu/{vmid}/agent/set-user-password` | POST | Open |
| `Invoke-PveVmGuestFsTrim` | `/nodes/{node}/qemu/{vmid}/agent/fstrim` | POST | Open |
#### Containers / LXC (20)
| Cmdlet | API Endpoint | Method | Prior Status |
|---|---|---|---|
| `Get-PveContainer` | `/nodes/{node}/lxc` | GET | Open |
| `New-PveContainer` | `/nodes/{node}/lxc` | POST | Open |
| `Remove-PveContainer` | `/nodes/{node}/lxc/{vmid}` | DELETE | Open |
| `Start-PveContainer` | `/nodes/{node}/lxc/{vmid}/status/start` | POST | Open |
| `Stop-PveContainer` | `/nodes/{node}/lxc/{vmid}/status/stop` | POST | Open |
| `Restart-PveContainer` | `/nodes/{node}/lxc/{vmid}/status/shutdown` | POST | Open |
| `Copy-PveContainer` | `/nodes/{node}/lxc/{vmid}/clone` | POST | Open |
| `Move-PveContainer` | `/nodes/{node}/lxc/{vmid}/migrate` | POST | Open |
| `Get-PveContainerConfig` | `/nodes/{node}/lxc/{vmid}/config` | GET | Open |
| `Set-PveContainerConfig` | `/nodes/{node}/lxc/{vmid}/config` | PUT | Open |
| `Suspend-PveContainer` | `/nodes/{node}/lxc/{vmid}/status/suspend` | POST | Open |
| `Resume-PveContainer` | `/nodes/{node}/lxc/{vmid}/status/resume` | POST | Open |
| `Resize-PveContainerDisk` | `/nodes/{node}/lxc/{vmid}/resize` | PUT | Open |
| `New-PveContainerTemplate` | `/nodes/{node}/lxc/{vmid}/template` | POST | Open |
| `Move-PveContainerVolume` | `/nodes/{node}/lxc/{vmid}/move_volume` | POST | Open |
| `Get-PveContainerInterface` | `/nodes/{node}/lxc/{vmid}/interfaces` | GET | Open |
| `Get-PveContainerSnapshot` | `/nodes/{node}/lxc/{vmid}/snapshot` | GET | Open |
| `New-PveContainerSnapshot` | `/nodes/{node}/lxc/{vmid}/snapshot` | POST | Open |
| `Remove-PveContainerSnapshot` | `/nodes/{node}/lxc/{vmid}/snapshot/{name}` | DELETE | Open |
| `Restore-PveContainerSnapshot` | `/nodes/{node}/lxc/{vmid}/snapshot/{name}/rollback` | POST | Open |
#### Storage (11)
| Cmdlet | API Endpoint | Method | Prior Status |
|---|---|---|---|
| `Get-PveStorage` | `/storage` or `/nodes/{node}/storage` | GET | Open |
| `Get-PveStorageContent` | `/nodes/{node}/storage/{storage}/content` | GET | Open |
| `Send-PveFile` | `/nodes/{node}/storage/{storage}/upload` | POST | Open |
| `Invoke-PveStorageDownload` | `/nodes/{node}/storage/{storage}/download-url` | POST | Open |
| `New-PveStorage` | `/storage` | POST | Open |
| `Remove-PveStorage` | `/storage/{storage}` | DELETE | Open |
| `Set-PveStorage` | `/storage/{storage}` | PUT | Open |
| `Get-PveStorageStatus` | `/nodes/{node}/storage/{storage}/status` | GET | Open |
| `Remove-PveStorageContent` | `/nodes/{node}/storage/{storage}/content/{volume}` | DELETE | Open |
| `Set-PveStorageContent` | `/nodes/{node}/storage/{storage}/content/{volume}` | PUT | Open |
| `New-PveStorageDisk` | `/nodes/{node}/storage/{storage}/content` | POST | Open |
#### VM Snapshots (4)
| Cmdlet | API Endpoint | Method | Prior Status |
|---|---|---|---|
| `Get-PveSnapshot` | `/nodes/{node}/qemu/{vmid}/snapshot` | GET | Open |
| `New-PveSnapshot` | `/nodes/{node}/qemu/{vmid}/snapshot` | POST | Open |
| `Remove-PveSnapshot` | `/nodes/{node}/qemu/{vmid}/snapshot/{name}` | DELETE | Open |
| `Restore-PveSnapshot` | `/nodes/{node}/qemu/{vmid}/snapshot/{name}/rollback` | POST | Open |
#### Networking (5)
| Cmdlet | API Endpoint | Method | Prior Status |
|---|---|---|---|
| `Get-PveNetwork` | `/nodes/{node}/network` | GET | Open |
| `New-PveNetwork` | `/nodes/{node}/network` | POST | Open |
| `Set-PveNetwork` | `/nodes/{node}/network/{iface}` | PUT | Open |
| `Remove-PveNetwork` | `/nodes/{node}/network/{iface}` | DELETE | Open |
| `Invoke-PveNetworkApply` | `/nodes/{node}/network` | PUT | Open |
#### SDN (25)
| Cmdlet | API Endpoint | Method | Prior Status |
|---|---|---|---|
| `Get-PveSdnZone` | `/cluster/sdn/zones` | GET | Open |
| `New-PveSdnZone` | `/cluster/sdn/zones` | POST | Open |
| `Remove-PveSdnZone` | `/cluster/sdn/zones/{zone}` | DELETE | Open |
| `Set-PveSdnZone` | `/cluster/sdn/zones/{zone}` | PUT | Open |
| `Get-PveSdnVnet` | `/cluster/sdn/vnets` | GET | Open |
| `New-PveSdnVnet` | `/cluster/sdn/vnets` | POST | Open |
| `Remove-PveSdnVnet` | `/cluster/sdn/vnets/{vnet}` | DELETE | Open |
| `Set-PveSdnVnet` | `/cluster/sdn/vnets/{vnet}` | PUT | Open |
| `Get-PveSdnSubnet` | `/cluster/sdn/vnets/{vnet}/subnets` | GET | Open |
| `New-PveSdnSubnet` | `/cluster/sdn/vnets/{vnet}/subnets` | POST | Open |
| `Remove-PveSdnSubnet` | `/cluster/sdn/vnets/{vnet}/subnets/{subnet}` | DELETE | Open |
| `Set-PveSdnSubnet` | `/cluster/sdn/vnets/{vnet}/subnets/{subnet}` | PUT | Open |
| `Get-PveSdnIpam` | `/cluster/sdn/ipams` | GET | Open |
| `New-PveSdnIpam` | `/cluster/sdn/ipams` | POST | Open |
| `Remove-PveSdnIpam` | `/cluster/sdn/ipams/{ipam}` | DELETE | Open |
| `Set-PveSdnIpam` | `/cluster/sdn/ipams/{ipam}` | PUT | Open |
| `Get-PveSdnDns` | `/cluster/sdn/dns` | GET | Open |
| `New-PveSdnDns` | `/cluster/sdn/dns` | POST | Open |
| `Remove-PveSdnDns` | `/cluster/sdn/dns/{dns}` | DELETE | Open |
| `Set-PveSdnDns` | `/cluster/sdn/dns/{dns}` | PUT | Open |
| `Get-PveSdnController` | `/cluster/sdn/controllers` | GET | Open |
| `New-PveSdnController` | `/cluster/sdn/controllers` | POST | Open |
| `Remove-PveSdnController` | `/cluster/sdn/controllers/{controller}` | DELETE | Open |
| `Set-PveSdnController` | `/cluster/sdn/controllers/{controller}` | PUT | Open |
| `Invoke-PveSdnApply` | `/cluster/sdn` | PUT | Open |
#### Firewall (21)
| Cmdlet | API Endpoint | Method | Prior Status |
|---|---|---|---|
| `Get-PveFirewallRule` | `/cluster/firewall/rules` | GET | Open |
| `New-PveFirewallRule` | `/cluster/firewall/rules` | POST | Open |
| `Set-PveFirewallRule` | `/cluster/firewall/rules/{pos}` | PUT | Open |
| `Remove-PveFirewallRule` | `/cluster/firewall/rules/{pos}` | DELETE | Open |
| `Get-PveFirewallGroup` | `/cluster/firewall/groups` | GET | Open |
| `New-PveFirewallGroup` | `/cluster/firewall/groups` | POST | Open |
| `Remove-PveFirewallGroup` | `/cluster/firewall/groups/{group}` | DELETE | Open |
| `Get-PveFirewallAlias` | `/cluster/firewall/aliases` | GET | Open |
| `New-PveFirewallAlias` | `/cluster/firewall/aliases` | POST | Open |
| `Set-PveFirewallAlias` | `/cluster/firewall/aliases/{name}` | PUT | Open |
| `Remove-PveFirewallAlias` | `/cluster/firewall/aliases/{name}` | DELETE | Open |
| `Get-PveFirewallIpSet` | `/cluster/firewall/ipset` | GET | Open |
| `New-PveFirewallIpSet` | `/cluster/firewall/ipset` | POST | Open |
| `Remove-PveFirewallIpSet` | `/cluster/firewall/ipset/{name}` | DELETE | Open |
| `Get-PveFirewallIpSetEntry` | `/cluster/firewall/ipset/{name}` | GET | Open |
| `New-PveFirewallIpSetEntry` | `/cluster/firewall/ipset/{name}` | POST | Open |
| `Set-PveFirewallIpSetEntry` | `/cluster/firewall/ipset/{name}/{cidr}` | PUT | Open |
| `Remove-PveFirewallIpSetEntry` | `/cluster/firewall/ipset/{name}/{cidr}` | DELETE | Open |
| `Get-PveFirewallOptions` | `/cluster/firewall/options` | GET | Open |
| `Set-PveFirewallOptions` | `/cluster/firewall/options` | PUT | Open |
| `Get-PveFirewallRef` | `/cluster/firewall/refs` | GET | Open |
#### Backup (6)
| Cmdlet | API Endpoint | Method | Prior Status |
|---|---|---|---|
| `New-PveBackup` | `/nodes/{node}/vzdump` | POST | Open |
| `Get-PveBackupJob` | `/cluster/backup` | GET | Open |
| `New-PveBackupJob` | `/cluster/backup` | POST | Open |
| `Set-PveBackupJob` | `/cluster/backup/{id}` | PUT | Open |
| `Remove-PveBackupJob` | `/cluster/backup/{id}` | DELETE | Open |
| `Get-PveBackupInfo` | `/cluster/backup-info/not-backed-up` | GET | Open |
#### Users / Access (23)
| Cmdlet | API Endpoint | Method | Prior Status |
|---|---|---|---|
| `Get-PveUser` | `/access/users` | GET | Open |
| `New-PveUser` | `/access/users` | POST | Open |
| `Remove-PveUser` | `/access/users/{userid}` | DELETE | Open |
| `Set-PveUser` | `/access/users/{userid}` | PUT | Open |
| `Get-PveRole` | `/access/roles` | GET | Open |
| `New-PveRole` | `/access/roles` | POST | Open |
| `Remove-PveRole` | `/access/roles/{roleid}` | DELETE | Open |
| `Set-PveRole` | `/access/roles/{roleid}` | PUT | Open |
| `Get-PvePermission` | `/access/acl` | GET | Open |
| `Set-PvePermission` | `/access/acl` | PUT | Open |
| `Get-PveApiToken` | `/access/users/{userid}/token` | GET | Open |
| `New-PveApiToken` | `/access/users/{userid}/token/{tokenid}` | POST | Open |
| `Remove-PveApiToken` | `/access/users/{userid}/token/{tokenid}` | DELETE | Open |
| `Set-PveApiToken` | `/access/users/{userid}/token/{tokenid}` | PUT | Open |
| `Get-PveGroup` | `/access/groups` | GET | Open |
| `New-PveGroup` | `/access/groups` | POST | Open |
| `Set-PveGroup` | `/access/groups/{groupid}` | PUT | Open |
| `Remove-PveGroup` | `/access/groups/{groupid}` | DELETE | Open |
| `Get-PveDomain` | `/access/domains` | GET | Open |
| `New-PveDomain` | `/access/domains` | POST | Open |
| `Set-PveDomain` | `/access/domains/{realm}` | PUT | Open |
| `Remove-PveDomain` | `/access/domains/{realm}` | DELETE | Open |
| `Set-PvePassword` | `/access/password` | PUT | Open |
#### Pools (4)
| Cmdlet | API Endpoint | Method | Prior Status |
|---|---|---|---|
| `Get-PvePool` | `/pools` | GET | Open |
| `New-PvePool` | `/pools` | POST | Open |
| `Set-PvePool` | `/pools/{poolid}` | PUT | Open |
| `Remove-PvePool` | `/pools/{poolid}` | DELETE | Open |
#### Templates (4)
| Cmdlet | API Endpoint | Method | Prior Status |
|---|---|---|---|
| `Get-PveTemplate` | `/nodes/{node}/qemu` (filtered) | GET | Open |
| `New-PveTemplate` | `/nodes/{node}/qemu/{vmid}/template` | POST | Open |
| `Remove-PveTemplate` | `/nodes/{node}/qemu/{vmid}` | DELETE | Open |
| `New-PveVmFromTemplate` | `/nodes/{node}/qemu/{vmid}/clone` | POST | Open |
#### Cloud-Init (3)
| Cmdlet | API Endpoint | Method | Prior Status |
|---|---|---|---|
| `Get-PveCloudInitConfig` | `/nodes/{node}/qemu/{vmid}/config` | GET | Open |
| `Set-PveCloudInitConfig` | `/nodes/{node}/qemu/{vmid}/config` | PUT | Open |
| `Invoke-PveCloudInitRegenerate` | `/nodes/{node}/qemu/{vmid}/cloudinit` | PUT | Open |
#### Tasks (4)
| Cmdlet | API Endpoint | Method | Prior Status |
|---|---|---|---|
| `Get-PveTask` | `/nodes/{node}/tasks/{upid}/status` | GET | Open |
| `Wait-PveTask` | `/nodes/{node}/tasks/{upid}/status` (polling) | GET | Open |
| `Get-PveTaskList` | `/nodes/{node}/tasks` | GET | Open |
| `Stop-PveTask` | `/nodes/{node}/tasks/{upid}` | DELETE | Open |
#### Cluster (1)
| Cmdlet | API Endpoint | Method | Prior Status |
|---|---|---|---|
| `Get-PveClusterResource` | `/cluster/resources` | GET | Open |
### Coverage Summary Per Functional Area
| Area | Total Endpoints | Covered | % | 🆕 PVE 9.0 New | Prior Status |
|---|---|---|---|---|---|
| acl | 2 | 2 | 100% | 0 | Open |
| version | 1 | 1 | 100% | 0 | Open |
| backup | 6 | 5 | 83% | 0 | Open |
| access_groups | 5 | 4 | 80% | 0 | Open |
| roles | 5 | 4 | 80% | 0 | Open |
| tasks | 5 | 4 | 80% | 0 | Open |
| firewall | 40 | 31 | 78% | 0 | Open |
| users | 12 | 9 | 75% | 0 | Open |
| networking | 7 | 5 | 71% | 0 | Open |
| pools | 7 | 5 | 71% | 0 | Open |
| access_domains | 6 | 4 | 67% | 0 | Open |
| storage_config | 5 | 3 | 60% | 0 | Open |
| storage | 19 | 8 | 42% | 1 | Open |
| sdn | 60 | 25 | 42% | 16 | Open |
| vms | 97 | 35 | 36% | 1 | Open |
| containers | 62 | 21 | 34% | 1 | Open |
| access | 15 | 2 | 13% | 1 | Open |
| nodes | 75 | 8 | 11% | 11 | Open |
| cluster | 77 | 3 | 4% | 6 | Open |
| ceph | 40 | 0 | 0% | 0 | Open |
| ha | 21 | 0 | 0% | 5 | Open |
| disks | 18 | 0 | 0% | 0 | Open |
| acme | 15 | 0 | 0% | 0 | Open |
| cluster_config | 10 | 0 | 0% | 0 | Open |
| certificates | 8 | 0 | 0% | 0 | Open |
| apt | 8 | 0 | 0% | 0 | Open |
| services | 7 | 0 | 0% | 0 | Open |
| metrics | 7 | 0 | 0% | 0 | Open |
| replication | 5 | 0 | 0% | 0 | Open |
| **TOTAL** | **646** | **180** | **27.9%** | **42** | — |
### API Drift — Changed Endpoints (Covered Cmdlets)
| Cmdlet | Endpoint | Change Description | Risk | Prior Status |
|---|---|---|---|---|
| `Set-PveBackupJob` | `PUT /cluster/backup/{id}` | Removed `notification-policy`, `notification-target` params | Breaking | Open |
| `Set-PveNetwork` | `PUT /nodes/{node}/network` | Added `regenerate-frr`, removed `skip_frr` | Breaking | Open |
| `Get-PveVmConfig` | `GET .../qemu/{vmid}/config` | Returns changed in latest version | Cosmetic | Open |
| `Move-PveVm` | `POST .../qemu/{vmid}/migrate` | Added `with-conntrack-state` parameter | Additive | Open |
### PVE 9.0 New Endpoints (42 total, 0 implemented)
| Area | Endpoint | Method | Description |
|---|---|---|---|
| ha | `/cluster/ha/rules` | GET/POST | HA rules system (new in PVE 9.0) |
| ha | `/cluster/ha/rules/{rule}` | GET/PUT/DELETE | Individual HA rule management |
| cluster | `/cluster/bulk-action/guest/*` | POST | Bulk guest start/shutdown/suspend/migrate |
| sdn | `/cluster/sdn/fabrics/*` | GET/POST/PUT/DELETE | SDN fabrics (13 endpoints) |
| sdn | `/cluster/sdn/lock` / `rollback` | POST/DELETE | SDN lock and rollback |
| nodes | `/nodes/{node}/capabilities/qemu/cpu-flags` | GET | CPU flags query |
| nodes | `/nodes/{node}/capabilities/qemu/migration` | GET | Migration capabilities |
| nodes | `/nodes/{node}/sdn/fabrics/*` | GET | Node SDN fabric status |
| nodes | `/nodes/{node}/query-oci-repo-tags` | GET | OCI repo tag query |
| storage | `/nodes/{node}/storage/{storage}/oci-registry-pull` | POST | OCI registry pull |
| vms | `/nodes/{node}/qemu/{vmid}/dbus-vmstate` | POST | D-Bus VM state |
| containers | `GET /nodes/{node}/lxc/{vmid}/migrate` | GET | Container migration pre-check |
| access | `POST /access/vncticket` | POST | VNC ticket creation |
### High-Value Gaps
**Tier 1 — Most impactful for users:**
1. **HA (High Availability)** — 21 endpoints, 0% covered. Essential for production clusters. Resources, groups, status, and the new PVE 9.0 rules system (5 🆕 endpoints).
2. **Cluster configuration** — 10 endpoints, 0%. Cluster create/join/node management is fundamental for multi-node setups.
3. **Ceph** — 40 endpoints, 0%. Critical for hyperconverged infrastructure (OSD, MON, pools, status).
4. **Notifications** — 32 cluster endpoints, 0%. New in PVE 8.1+, essential for monitoring/alerting automation.
5. **Disk management** — 18 endpoints, 0%. LVM, ZFS, SMART, wipe — needed for storage provisioning.
**Tier 2 — Important operational gaps:**
6. **VM/CT-level firewall** — ~44 combined endpoints. Cluster-level IS covered, but per-VM/CT rules are not.
7. **SDN Fabrics** (🆕 PVE 9.0) — 13 new endpoints. Key for advanced SDN.
8. **Resource mappings** (PCI/USB passthrough) — 15 endpoints. Key for GPU passthrough.
9. **Certificates/ACME** — 23 combined endpoints. TLS certificate management.
10. **`GET /cluster/nextid`** — Trivial but frequently needed to allocate VM IDs programmatically.
---
## Phase 3 — Code Quality & Best Practices
### 3a. PowerShell Module Design
- **All cmdlets use approved PowerShell verbs** via verb class constants.
- **Noun prefix `Pve`** is consistent across all 169 cmdlets.
- **HelpMessage** present on virtually all parameters.
- **ShouldProcess** on all destructive/mutating cmdlets.
- **Pipeline support** via `ProcessRecord` and `ValueFromPipelineByPropertyName`.
- **ValidateRange/ValidateNotNullOrEmpty/ValidateSet** used appropriately.
- **OutputType** on all 169 cmdlets (previously ~54 were missing — now all have it).
- **All cmdlets are sealed** (previously ~95 were not).
### 3b. C# Code Quality
- **Nullable annotations**: `enable` in both projects.
- **IDisposable**: `PveHttpClient` correctly implements `IDisposable`.
- **Logging**: `WriteVerbose` used extensively; `WriteProgress` in file uploads and task waiting.
- **Exception hierarchy**: 7 custom exception types with context properties.
- **Bare catches eliminated**: All `catch { }` blocks replaced with filtered `catch (Exception ex) when (...)`.
- **Magic strings extracted**: Auth header names are now `const string` fields.
- **Zero TODO/FIXME/HACK markers**.
- **No commented-out code or dead code**.
- **Clean code style** matching `.editorconfig`.
- **Dual JSON attributes removed**: Only `[JsonProperty]` (Newtonsoft) remains; `[JsonPropertyName]` removed.
### Code Quality Findings
| ID | File | Line | Severity | Description | Prior Status |
|---|---|---|---|---|---|
| CQ1 | `NewPveContainerSnapshotCmdlet.cs` | 75 | **Critical** | **Infinite loop with no timeout in private `WaitForTask`.** `while (true)` with no timeout or cancellation. If task never completes, cmdlet hangs forever. Should use `TaskService.WaitForTask` (same pattern that was fixed in VM snapshot cmdlets). | New |
| CQ2 | `RemovePveContainerSnapshotCmdlet.cs` | 67 | **Critical** | **Identical infinite loop** — same `while(true)` no-timeout pattern as CQ1. | New |
| CQ3 | `RestorePveContainerSnapshotCmdlet.cs` | 70 | **Critical** | **Identical infinite loop** — same pattern as CQ1. | New |
| CQ4 | `InvokePveStorageDownloadCmdlet.cs` | 83 | **Critical** | **Infinite loop in download task polling.** `while(true)` with no timeout. Should use `TaskService.WaitForTask`. | New |
| CQ5 | `SendPveFileCmdlet.cs` | 143 | **Critical** | **Infinite loop in upload task polling.** `while(true)` with no timeout. Should use `TaskService.WaitForTask`. | New |
| CQ6 | Both `.csproj` files | 4 | **Medium** | **`net9.0` target is EOL** (May 2025). Should upgrade to `net10.0` (LTS). | Open |
| CQ7 | `PveHttpClient.cs` | ~154 sites | **Medium** | **Sync-over-async via `.GetAwaiter().GetResult()`** across ~216 call sites. Standard for PS binary modules but carries deadlock risk. | Open |
| CQ8 | `PveCmdletBase.cs` | 149 | **Medium** | **Broad catch in status polling.** `catch (Exception ex) when (...)` is filtered but still catches broadly. Improvement over bare catch but could be more specific. | Open |
| CQ9 | `RestartPveContainerCmdlet.cs` | 15 | **Medium** | **Missing `ConfirmImpact.High`** — `RestartPveVmCmdlet` has it but container counterpart does not. Inconsistent. | New |
| CQ10 | `SuspendPveContainerCmdlet.cs` | 14 | **Medium** | **Missing `ConfirmImpact.High`** — `SuspendPveVmCmdlet` has it but container counterpart does not. Inconsistent. | New |
| CQ11 | 38 cmdlet files | varies | **Medium** | **`new PveHttpClient` per-operation.** 38 cmdlets create their own PveHttpClient instance instead of going through services. Bypasses connection pooling. | Open |
| CQ12 | `PSProxmoxVE.csproj` | 22 | **Low** | **`System.Management.Automation` pinned to 7.4.0.** Consider updating if targeting .NET 10. | Open |
### Fixed Since Prior Scan
| Prior ID | Description | Status |
|---|---|---|
| CQ1–CQ4 | Infinite loop WaitForTask in InvokePveNetworkApply, NewPveSnapshot, RestorePveSnapshot, RemovePveSnapshot | **Fixed** — now uses `TaskService.WaitForTask` |
| CQ5 | Infinite poll loop in InvokePveVmGuestExec | **Fixed** — added Timeout parameter with Stopwatch + TimeoutException |
| CQ7 | Duplicated WaitForTask methods (4 VM/network cmdlets) | **Fixed** — uses TaskService |
| CQ9 | ~54 cmdlets missing OutputType | **Fixed** — all 169 cmdlets now have `[OutputType]` |
| CQ10 | Firewall VmId defaults to 0 instead of nullable | **Fixed** — no longer `int` default 0 pattern |
| CQ11 | Bare catch in PveHttpClient.ExtractErrorMessage | **Fixed** — proper exception handling |
| CQ12 | Bare catch in PveCmdletBase status polling | **Fixed** — filtered `when` clause excludes OOM/SOE |
| CQ13 | Bare catch in VmService + ContainerService | **Fixed** — filtered to PveApiException/HttpRequestException |
| CQ14 | Bare catch in GetPveVmCmdlet | **Fixed** — proper exception handling |
| CQ17 | RemovePveRoleCmdlet missing ConfirmImpact.High | **Fixed** — now has `ConfirmImpact = ConfirmImpact.High` |
| CQ18 | ~95 cmdlets not sealed | **Fixed** — all 169 cmdlets are now `sealed` |
| CQ19 | SuspendPveVmCmdlet missing ConfirmImpact.High | **Fixed** — now has `ConfirmImpact.High` |
| CQ20 | RestartPveVmCmdlet missing ConfirmImpact | **Fixed** — now has `ConfirmImpact.High` |
| CQ21 | Dual JSON serialization attributes | **Fixed** — `[JsonPropertyName]` removed, only `[JsonProperty]` remains |
| CQ22 | Auth header magic strings | **Fixed** — extracted to `const string ApiTokenPrefix` and `CsrfHeaderName` |
| CQ6 (prior) | Pool/CephPool parameter conflict in NewPveStorageCmdlet | **Fixed** — runtime validation with `ThrowTerminatingError` if both specified |
---
## Phase 4 — Testing Coverage Analysis
### Test Projects
| Project | Framework | Type | Runner |
|---|---|---|---|
| `PSProxmoxVE.Core.Tests` | xUnit 2.7 + Moq 4.20 | Unit tests (models, auth) | `dotnet test` |
| `PSProxmoxVE.Tests` | Pester 5 | Cmdlet + Integration tests | `Invoke-Pester` |
### xUnit Tests (Core Library)
15 test files covering authentication (PveSession, PveAuthenticator, PveVersion) and model deserialization (12 model areas including backup, cluster, firewall, SDN) with PVE 8 and PVE 9 JSON fixtures. ~180+ xUnit tests total.
### Pester Tests
100% of 169 cmdlets have Pester unit tests covering: command existence, parameter metadata, mandatory checks, parameter types, ShouldProcess presence, ConfirmImpact declarations, and "no session" error behavior.
### Integration Tests
- Self-hosted runner provisions nested PVE 8 and PVE 9 via Terraform + auto-install ISO
- Tests check for `PVETEST_*` environment variables, skip gracefully when unavailable
- Robust `AfterAll` cleanup of VMs, containers, users, roles, firewall rules, backup jobs
- CRUD patterns: create → verify → update → verify → delete → verify
- SDN tests check PVE version and skip if older
### Test Coverage Gap List (cmdlets lacking integration tests)
| Cmdlet | Unit Test | Integration Test | Prior Status |
|---|---|---|---|
| **Nodes** | | | |
| Get-PveNodeConfig | Yes | No | Open |
| Set-PveNodeConfig | Yes | No | Open |
| Get-PveNodeDns | Yes | No | Open |
| Set-PveNodeDns | Yes | No | Open |
| Start-PveNodeVms | Yes | No | Open |
| Stop-PveNodeVms | Yes | No | Open |
| **VMs** | | | |
| Move-PveVm | Yes | No | Open |
| Move-PveVmDisk | Yes | No | Open |
| Remove-PveVmDisk | Yes | No | Open |
| **Guest Agent** | | | |
| Get-PveVmGuestOsInfo | Yes | No | Open |
| Get-PveVmGuestFsInfo | Yes | No | Open |
| Read-PveVmGuestFile | Yes | No | Open |
| Write-PveVmGuestFile | Yes | No | Open |
| Set-PveVmGuestPassword | Yes | No | Open |
| Invoke-PveVmGuestFsTrim | Yes | No | Open |
| **Containers** | | | |
| Move-PveContainer | Yes | No | Open |
| Suspend-PveContainer | Yes | No | Open |
| Resume-PveContainer | Yes | No | Open |
| Resize-PveContainerDisk | Yes | No | Open |
| New-PveContainerTemplate | Yes | No | Open |
| Move-PveContainerVolume | Yes | No | Open |
| Get-PveContainerInterface | Yes | No | Open |
| **Storage** | | | |
| Set-PveStorage | Yes | No | Open |
| Get-PveStorageStatus | Yes | No | Open |
| Remove-PveStorageContent | Yes | No | Open |
| Set-PveStorageContent | Yes | No | Open |
| New-PveStorageDisk | Yes | No | Open |
| **SDN** | | | |
| Set-PveSdnZone | Yes | No | Open |
| Set-PveSdnVnet | Yes | No | Open |
| Set-PveSdnSubnet | Yes | No | Open |
| New-PveSdnIpam | Yes | No | Open |
| Remove-PveSdnIpam | Yes | No | Open |
| Set-PveSdnIpam | Yes | No | Open |
| New-PveSdnDns | Yes | No | Open |
| Remove-PveSdnDns | Yes | No | Open |
| Set-PveSdnDns | Yes | No | Open |
| New-PveSdnController | Yes | No | Open |
| Remove-PveSdnController | Yes | No | Open |
| Set-PveSdnController | Yes | No | Open |
| Invoke-PveSdnApply | Yes | No | Open |
| **Users / Access** | | | |
| Set-PveRole | Yes | No | Open |
| Set-PveApiToken | Yes | No | Open |
| Get-PveGroup | Yes | No | Open |
| New-PveGroup | Yes | No | Open |
| Set-PveGroup | Yes | No | Open |
| Remove-PveGroup | Yes | No | Open |
| Get-PveDomain | Yes | No | Open |
| New-PveDomain | Yes | No | Open |
| Set-PveDomain | Yes | No | Open |
| Remove-PveDomain | Yes | No | Open |
| Set-PvePassword | Yes | No | Open |
| **Pools** | | | |
| Get-PvePool | Yes | No | Open |
| New-PvePool | Yes | No | Open |
| Set-PvePool | Yes | No | Open |
| Remove-PvePool | Yes | No | Open |
| **Templates** | | | |
| New-PveTemplate | Yes | No | Open |
| Remove-PveTemplate | Yes | No | Open |
| **Tasks** | | | |
| Get-PveTaskList | Yes | No | Open |
| Stop-PveTask | Yes | No | Open |
| **Firewall** | | | |
| Get-PveFirewallGroup | Yes | No | Open |
| New-PveFirewallGroup | Yes | No | Open |
| Remove-PveFirewallGroup | Yes | No | Open |
| Set-PveFirewallAlias | Yes | No | Open |
| Set-PveFirewallIpSetEntry | Yes | No | Open |
| Set-PveFirewallOptions | Yes | No | Open |
| Get-PveFirewallRef | Yes | No | Open |
| **Backup** | | | |
| New-PveBackup | Yes | No | Open |
| Get-PveBackupInfo | Yes | No | Open |
| **Cluster** | | | |
| Get-PveClusterResource | Yes | No | Open |
**Summary**: 169/169 unit tested (100%), ~105/169 integration tested (~62%).
---
## Phase 5 — Security Review
### 5.1 Credential Handling
| Aspect | Status | Details | Prior Status |
|---|---|---|---|
| PSCredential usage | **Good** | `Connect-PveServer` accepts `PSCredential` | Open |
| SecureString pattern | **Good** | Consistent `Marshal.SecureStringToGlobalAllocUnicode` + `ZeroFreeGlobalAllocUnicode` in try/finally | Open |
| Set-PveVmGuestPassword | **Good** | Now uses `SecureString` (was plain `string`) | Fixed |
| API token in memory | **Acceptable** | Stored as plain `string` in `PveSession.ApiToken` | Open |
| Credential logging | **Good** | No WriteVerbose/WriteDebug includes credentials | Open |
| Ticket storage | **Good** | In `PveSession` only, not written to disk | Open |
| Session expiry | **Good** | 2-hour expiry via `IsExpired` property | Open |
### 5.2 TLS/HTTPS
| Aspect | Status | Details | Prior Status |
|---|---|---|---|
| HTTPS enforced | **Good** | `BaseUrl` always `https://`, no HTTP fallback | Open |
| SkipCertificateCheck | **Good** | Explicit opt-in with `WriteWarning` advisory | Open |
| TLS version | **Good** | OS/runtime negotiation (no pinning) | Open |
| CSRF protection | **Good** | Token included on all POST/PUT/DELETE with ticket auth | Open |
### 5.3 Input Validation
| Aspect | Status | Details | Prior Status |
|---|---|---|---|
| VMID validation | **Good** | `[ValidateRange(100, 999999999)]` on all VmId params | Open |
| URL encoding | **Good** | `Uri.EscapeDataString()` applied consistently across all services (VmService, ContainerService, StorageService, NetworkService, FirewallService, UserService, BackupService, PoolService, TaskService, SnapshotService, ClusterService) | Fixed |
| API token format | **Good** | Regex validation in `PveAuthenticator` | Open |
| Port validation | **Good** | `[ValidateRange(1, 65535)]` on port parameter | Open |
### 5.4 Dependency Security
| Package | Version | Pinned | Notes | Prior Status |
|---|---|---|---|---|
| `Newtonsoft.Json` | 13.0.3 | Yes | Latest stable | Open |
| `System.Text.Json` | 8.0.5 | Yes | Latest 8.x patch | Open |
| `SharpCompress` | 0.38.0 | Yes | OVA/tar extraction | Open |
| `PowerShellStandard.Library` | 5.1.1 | Yes | PrivateAssets=all | Open |
| `System.Management.Automation` | 7.4.0 | Yes | PrivateAssets=all | Open |
### Security Findings
| ID | Area | File | Severity | Description | Prior Status |
|---|---|---|---|---|---|
| S1 | Hardcoded password | `integration-tests.yml:42` | **Low** | `PVE_PASSWORD: "Testpass123!"` in workflow env. Masked via `::add-mask::`. Disposable test VM. | Open |
| S2 | Hardcoded password | `variables.tf:80` | **Low** | Default `"Testpass123!"` for test VM password. Disposable nested VM. | Open |
### Fixed Since Prior Scan
| Prior ID | Description | Status |
|---|---|---|
| S1 | `SetPveVmGuestPasswordCmdlet.cs` Password is `string`, not `SecureString` | **Fixed** — now uses `SecureString` |
| S2 | `debug/Capture-UploadDiff.ps1` hardcoded API token and IP | **Fixed** — replaced with placeholder tokens ``, `@!=` |
| S3 | URL encoding missing in VmService, ContainerService, StorageService | **Fixed** — `Uri.EscapeDataString()` now used consistently across all services |
---
## Phase 6 — PSGallery Publication Readiness
| # | Check | Pass/Fail | Notes | Prior Status |
|---|---|---|---|---|
| 1 | ModuleVersion is SemVer | **Pass** | `0.1.0` | Open |
| 2 | GUID present and stable | **Pass** | `a3f7c2d1-84e5-4b9f-a061-3e2d8c5f1a7b` | Open |
| 3 | Author | **Pass** | `goodolclint` | Open |
| 4 | CompanyName | **Pass** | `Worklab` | Open |
| 5 | Copyright | **Pass** | `(c) 2026 goodolclint. All rights reserved.` | Open |
| 6 | Description (meaningful) | **Pass** | Mentions PVE 8.x/9.x and feature areas | Open |
| 7 | PowerShellVersion minimum | **Pass** | `5.1` | Open |
| 8 | RequiredAssemblies | **Pass** | `PSProxmoxVE.Core.dll`, `Newtonsoft.Json.dll` | Open |
| 9 | CmdletsToExport explicit list | **Pass** | Fully enumerated (169 cmdlets), no wildcards | Open |
| 10 | FunctionsToExport | **Pass** | Explicitly `@()` | Open |
| 11 | Tags for discoverability | **Pass** | 8 tags incl. Proxmox, PVE, Virtualization, IaC | Open |
| 12 | ProjectUri | **Pass** | GitHub repo URL | Open |
| 13 | LicenseUri | **Pass** | Points to LICENSE | Open |
| 14 | IconUri | **Fail** | Not set — cosmetic | Open |
| 15 | ReleaseNotes | **Pass** | In PSData | Open |
| 16 | Prerelease string | **Pass** | `'preview'` | Open |
| 17 | LICENSE (OSI-approved) | **Pass** | MIT | Open |
| 18 | README: Install-Module | **Pass** | `Install-Module -Name PSProxmoxVE` documented | Open |
| 19 | README: Quick-start examples | **Pass** | Multiple scenarios | Open |
| 20 | README: Authentication guide | **Pass** | Ticket + API token | Open |
| 21 | README: Badges | **Pass** | Build, Unit Tests, License, PSGallery | Open |
| 22 | Publish workflow | **Pass** | Tag-triggered via `publish.yml` | Open |
| 23 | Publish: version stamping | **Pass** | Extracts from git tag | Open |
| 24 | Publish: smoke test | **Pass** | Loads module, asserts cmdlet count | Open |
| 25 | Publish: API key as secret | **Pass** | `secrets.PSGALLERY_API_KEY` | Open |
| 26 | Publish: GitHub Release | **Pass** | Auto-created via `softprops/action-gh-release` | Open |
| 27 | CHANGELOG | **Pass** | Keep a Changelog format + Conventional Commits | Open |
| 28 | Publish: single netstandard2.0 | **Warning** | Only builds netstandard2.0. Verify it loads under Win PS 5.1 since manifest declares `DotNetFrameworkVersion = '4.8'`. | Open |
---
## Phase 7 — Community & Repo Maintenance Standards
| # | Check | Pass/Fail | Notes | Prior Status |
|---|---|---|---|---|
| 1 | Bug report issue template | **Pass** | YAML form with module/PVE/PS version fields | Open |
| 2 | Feature request template | **Pass** | Description, use case, API endpoints | Open |
| 3 | PR template | **Pass** | Summary, type checkboxes, testing checklist | Open |
| 4 | `CONTRIBUTING.md` | **Pass** | Prerequisites, building, tests, coding standards, PR process | Open |
| 5 | `CODE_OF_CONDUCT.md` | **Pass** | Contributor Covenant 2.1 | Open |
| 6 | `SECURITY.md` | **Pass** | Vulnerability disclosure, 48h SLA | Open |
| 7 | Conventional commits | **Pass** | Consistent `feat:`, `fix:`, `fix(test):` in history | Open |
| 8 | `.gitattributes` | **Pass** | Comprehensive: csharp diff, binary markers, LF for .sh | Open |
| 9 | Issue template config.yml | **Fail** | No `config.yml` to control blank issues | Open |
| 10 | CODEOWNERS | **Fail** | Not present. Low priority for single maintainer. | Open |
| 11 | FUNDING.yml / community links | **Fail** | No Discussions/Discord/Funding references | Open |
| 12 | Default branch `main` | **Pass** | Confirmed | Open |
### Recommended Branch Protection
- Require PR reviews before merge
- Require status checks (build + unit tests + integration tests)
- Require linear history or squash merging
---
## Phase 8 — Prioritized Recommendations
### 🔴 Critical (blocks PSGallery publication or is a security risk)
| # | What | Where | Why | Fix | Prior Status |
|---|---|---|---|---|---|
| C1 | **Infinite loop in 5 cmdlets' WaitForTask methods** | `NewPveContainerSnapshotCmdlet.cs`, `RemovePveContainerSnapshotCmdlet.cs`, `RestorePveContainerSnapshotCmdlet.cs`, `InvokePveStorageDownloadCmdlet.cs`, `SendPveFileCmdlet.cs` | `while(true)` with no timeout — cmdlet hangs forever if task stalls. Same pattern that was fixed in VM snapshot cmdlets. | Replace private `WaitForTask` with `TaskService.WaitForTask` (identical fix to CQ1–CQ4 from prior scan) | New |
### 🟠High (significantly impacts quality or community adoption)
| # | What | Where | Why | Fix | Prior Status |
|---|---|---|---|---|---|
| H1 | **HA subsystem (0% coverage)** | Module | 21 endpoints + 5 🆕 PVE 9.0, essential for production clusters | Implement HA resources, groups, status, and PVE 9.0 rules | Open |
| H2 | **Ceph subsystem (0% coverage)** | Module | 40 endpoints, critical for hyperconverged infrastructure | Implement Ceph OSD, MON, pool, status operations | Open |
| H3 | **net9.0 target is EOL** | Both `.csproj` files | .NET 9.0 EOL was May 2025. Should use LTS release. | Upgrade to `net10.0` | Open |
### 🟡 Medium (best practice gaps, test coverage holes)
| # | What | Where | Why | Fix | Prior Status |
|---|---|---|---|---|---|
| M1 | **HttpClient per-call pattern** | 38 cmdlet files + services | 38 cmdlets create `new PveHttpClient` per operation, bypassing connection pooling | Refactor to client-per-session or use `IHttpClientFactory` pattern | Open |
| M2 | **64 cmdlets lack integration tests** | Integration tests | 38% of cmdlets untested end-to-end | Prioritize pools, groups, domains, node ops, container gaps, guest agent extensions | Open |
| M3 | **VM/CT-level firewall (0% coverage)** | Module | ~44 endpoints. Cluster/node-level covered but per-VM/CT rules are not. | Add VM/CT firewall cmdlets using existing firewall service patterns | Open |
| M4 | **Cluster config (0% coverage)** | Module | 10 endpoints for cluster create/join/node management | Implement for multi-node cluster automation | Open |
| M5 | **PVE 9.0 endpoints (0% coverage)** | Module | 42 new endpoints in latest PVE version. Users on PVE 9 will expect support. | Prioritize HA rules, bulk actions, SDN fabrics | New |
| M6 | **Broad exception catches** | `PveCmdletBase.cs`, `VmService.cs`, `ContainerService.cs` | Filtered `when` clauses are better than bare catches but still catch broadly | Consider more specific exception types | Open |
### 🟢 Low / Nice-to-have (polish, discoverability, stretch goals)
| # | What | Where | Why | Fix | Prior Status |
|---|---|---|---|---|---|
| L1 | No `IconUri` in manifest | `PSProxmoxVE.psd1` | Improves PSGallery listing appearance | Create icon, host it, add URI | Open |
| L2 | `System.Management.Automation` pinned to 7.4.0 | `PSProxmoxVE.csproj` | Consider updating when targeting .NET 10 | Update with TFM migration | Open |
| L3 | No `config.yml` for issue templates | `.github/ISSUE_TEMPLATE/` | Controls blank issue creation | Add simple config.yml | Open |
| L4 | No CODEOWNERS | Repo root | Documents ownership for PR auto-assignment | Add `CODEOWNERS` with maintainer | Open |
| L5 | Disk management (0% coverage) | Module | 18 endpoints — LVM, ZFS, SMART, wipe | Implement as demand grows | Open |
| L6 | Notifications (0% coverage) | Module | 32 endpoints — new in PVE 8.1+ | Implement for monitoring automation | Open |
| L7 | ACME/Certificates (0% coverage) | Module | 23 endpoints — TLS cert management | Implement as demand grows | Open |
| L8 | Verify netstandard2.0 loads on PS 5.1 | Publish workflow | Manifest says Desktop compatible | Add PS 5.1 smoke test to publish workflow | Open |
---
## Summary Statistics
| Metric | This Scan | Prior Scan | Delta |
|---|---|---|---|
| Total cmdlets | 169 | 148 | +21 |
| Cmdlets with ShouldProcess | All mutating | All mutating | — |
| Cmdlets with OutputType | 169 (100%) | ~94 (~64%) | +36% |
| Cmdlets sealed | 169 (100%) | ~60 (~40%) | +60% |
| Cmdlets with HelpMessage | 169 (100%) | 148 (100%) | — |
| xUnit test files | 15 | 15 | — |
| Pester test files | ~48 | ~45 | +3 |
| Integration test coverage | ~62% (105/169) | ~57% (85/148) | +5% |
| PVE API areas with >0% coverage | 17 of 30 | 17 of 29 | — |
| PVE API total coverage | 27.9% (180/646) | 27.9% (180/646) | — |
| Critical issues | 1 | 2 | -1 |
| High issues | 3 | 5 | -2 |
| Medium issues | 7 | 10 | -3 |
| Low issues | 7 | 11 | -4 |
| **Total issues** | **18** | **28** | **-10** |
| Issues fixed from prior | 16 | 9 | +7 |
| New issues found | 5 | 16 | -11 |
| NuGet dependencies (runtime) | 3 | 3 | — |
| Security vulnerabilities | 0 | 0 | — |
| Community files present | 9/9 | 9/9 | — |
\* Integration test coverage improved both in absolute count (+20 cmdlets) and percentage (+5%).