diff --git a/README.md b/README.md index 6ee01b0..7d1a57c 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,7 @@ client/src/ The backend already follows route/controller/model/form/service separation. The frontend now keeps top-level pages in `client/src/views` and uses reusable table, modal, dashboard-widget, and config-section components for repeated UI patterns. Future large screens should become view components first, with smaller domain-specific pieces under folders such as `components/scripts`, `components/hosts`, and `components/runplans`. -Tailwind CSS is installed through the Vite plugin and imported from `client/src/style.css`. Vuetify setup is centralized in `client/src/plugins/vuetify.js` so Material-style defaults, theme roles, density, icons, and component behavior are controlled in one place instead of scattered through views. +Tailwind CSS is installed through the Vite plugin and imported from `client/src/style.css`. Vuetify setup is centralized in `client/src/plugins/vuetify.js` so Material-style defaults, theme roles, density, icons, and component behavior are controlled in one place instead of scattered through views. The profile theme picker currently supports `dashtreme`, `terminal`, `aurora`, `greyscale`, and `macos`; row-level data-table actions use icon-only Material-style buttons with labels/tooltips, while page-level create/save commands keep text labels for clarity. Dashboard widgets are intentionally split: @@ -163,6 +163,14 @@ Important environment variables: | `DEFAULT_ADMIN_NAME` | First-run admin display name. | | `POWERSHELL_BIN` | Initial/default PowerShell executable. Defaults to `pwsh`; admins can override the live runtime value with Config -> Application Config -> Execution -> PowerShell Binary. | | `ALLOW_SCRIPT_EXECUTION` | Set `false` to disable actual RunPlan execution. | +| `MYRTILLE_ENABLED` | Enables browser RDP launch through an external Myrtille gateway. Defaults to `false`. | +| `MYRTILLE_GATEWAY_URL` | Base URL to the Myrtille web app, for example `https://rdp-gateway.contoso.local/Myrtille/`. | +| `MYRTILLE_USE_PASSWORD_HASH` | When `true`, POSHManager asks Myrtille for a gateway-specific `passwordHash` before returning the launch URL. Defaults to `true`. | +| `MYRTILLE_ALLOW_PLAIN_PASSWORD` | Allows the legacy Myrtille `password` URL parameter when hash mode is disabled or fails. Defaults to `false`; use only for isolated HTTPS gateways. | +| `MYRTILLE_HASH_ENDPOINT` | Relative hash endpoint on the Myrtille gateway. Defaults to `GetHash.aspx`. | +| `MYRTILLE_DEFAULT_WIDTH` | Initial Myrtille desktop width. Defaults to `1280`. | +| `MYRTILLE_DEFAULT_HEIGHT` | Initial Myrtille desktop height. Defaults to `800`. | +| `MYRTILLE_REQUEST_TIMEOUT_MS` | Timeout for server-side Myrtille hash requests. Defaults to `15000`. | | `VCENTER_ENABLED` | Enables VMware vCenter VM discovery and Host import workflows. | | `VCENTER_BASE_URL` | vCenter REST API root, for example `https://vcenter.contoso.local`. | | `VCENTER_USERNAME` | vCenter account used by the backend to create API sessions. | @@ -274,9 +282,12 @@ Graph environments store Microsoft Entra app registration details used to call M | `POST` | `/api/psadt/intune/deployments/:id/promote` | User | Advance a rollout ring's assignment intent (e.g. available → required). | | `GET` | `/api/psadt/intune/builder/status` | User | Whether this host can build `.intunewin` (Windows + tool, or external packager). | | `POST` | `/api/psadt/intune/deployments/:id/build` | User | Build a `.intunewin` from a source folder and ingest it into the Asset Library. | +| `GET` | `/api/psadt/intune/deployments/:id/datasheet` | User | Auto-generate a package datasheet (metadata, install/uninstall commands, detection, return codes, assignments, requirements, source/version). `?format=md\|html` (default `md`); `?download=1` to force a file download. | | `GET` | `/api/packaging/installer-types` | User | List known installer technologies and their silent-switch catalog. | | `POST` | `/api/packaging/analyze` | User | Detect installer technology from a file name and recommend install/uninstall commands. | | `POST` | `/api/packaging/detection` | User | Generate an Intune detection rule (MSI product code, file+version, or registry). | +| `GET` | `/api/packaging/recipes` | User | Browse/search (`?q=`) the curated application recipe library. | +| `POST` | `/api/packaging/recipes/:id/apply` | User | Scaffold a draft package (PSADT profile + Intune deployment + tracked app) from a recipe. | ### Installer intelligence (packaging) @@ -289,6 +300,21 @@ generates a detection rule in the deployment's `detectionType`/`detectionRule` shape — an MSI product-code rule, or an Intune-style custom PowerShell script for file-version or registry detection. The deployment modal's **Analyze installer** control fills the install/uninstall commands directly. + +### Application recipe library + +The **Recipe Library** panel in the PSADT workbench is a curated catalog of ~30 +common apps (browsers, Office/Microsoft 365 Apps, comms, developer tools, remote +access/virtualization, and security/enterprise agents such as CrowdStrike Falcon, +Splunk Universal Forwarder, SAP GUI, and Cisco Umbrella). Each recipe carries the +known-good silent switches, a detection footprint, and (where one exists) a +winget id. "Create package" (`POST /api/packaging/recipes/:id/apply`) scaffolds a +draft PSADT profile — with the raw silent install as an install task — plus an +Intune deployment (PSADT v4 wrapper and a generated detection rule), and, for +recipes with a winget id, a catalog application wired to the version watcher so +the app auto-tracks updates. Vendor-managed agents that need a CID, licence key, +or config file carry `` arguments as explicit refine-me prompts; the +result is always a draft to review before publishing. | `GET` | `/api/psadt/intune/deployments/:id/graph/audit` | User | Return the audit trail of tenant-changing Graph actions for this deployment. | | `GET` | `/api/intune/applications` | User | List catalog applications with computed update state. | | `POST` | `/api/intune/applications` | User | Create a catalog application (groups versioned deployments). | @@ -447,6 +473,10 @@ Theme payload: { "theme": "dashtreme" } ``` +Supported theme IDs are `dashtreme`, `terminal`, `aurora`, `greyscale`, and +`macos`. The macOS theme is a desktop-style graphite/frosted mode with +toolbar-style chrome; greyscale is a neutral high-contrast enterprise mode. + ### Users And Groups Admin-only routes create users and groups. Groups are used for group-visible scripts, credentials, and RunPlans. @@ -512,7 +542,7 @@ Payload: | --- | --- | --- | --- | | `GET` | `/api/hosts` | User | List host library. | | `POST` | `/api/hosts` | User | Create host. | -| `POST` | `/api/hosts/:id/rdp-session` | User | Create a short-lived browser RDP launch session for a visible Windows host with an assigned visible username/password credential. Returns a same-origin `/rdp/:token` URL. | +| `POST` | `/api/hosts/:id/rdp-session` | User | Create a Myrtille browser RDP launch URL for a visible Windows host with an assigned visible username/password credential. | | `GET` | `/api/hosts/import/vcenter/status` | User | Return legacy effective vCenter status plus visible saved VMware connection records. | | `GET` | `/api/hosts/import/vcenter/connections` | User | List visible saved VMware connections. | | `POST` | `/api/hosts/import/vcenter/connections` | User | Create a VMware connection record using a Credential Vault `credentialId`. Use `targetType: "vcenter"` for vCenter inventory or `targetType: "host"` for standalone ESXi Web Services inventory. | @@ -565,12 +595,16 @@ guest identity when available; unknown guests are imported as `other`. Windows hosts with an assigned visible username/password Credential Vault entry show an RDP icon in the Hosts table. Clicking it calls -`POST /api/hosts/:id/rdp-session`, opens a same-origin `/rdp/:token` -tab, and renders a browser RDP canvas through `mstsc.js`. The browser receives -only a short-lived opaque launch token; POSHManager resolves the host address -and decrypts the assigned credential server-side. The RDP gateway uses the -default RDP port `3389`; PowerShell remoting `port` metadata remains separate -from RDP launch behavior. +`POST /api/hosts/:id/rdp-session`, which validates host/credential visibility, +decrypts the assigned credential server-side, and returns a Myrtille launch URL +for the configured gateway. POSHManager does not run the RDP protocol stack in +Node; Myrtille owns the browser session. By default POSHManager requests a +gateway-specific `passwordHash` from Myrtille and places that hash in the launch +URL instead of returning the plaintext vault password to the browser. The +plaintext Myrtille `password` parameter is available only when +`myrtille_allow_plain_password` / `MYRTILLE_ALLOW_PLAIN_PASSWORD` is explicitly +enabled. The RDP gateway uses the default RDP port `3389`; PowerShell remoting +`port` metadata remains separate from RDP launch behavior. Host Groups let operators aggregate manual, imported, and VMware-sourced hosts without duplicating RunPlans. Manual groups store an explicit list of visible @@ -957,6 +991,23 @@ The PSADT Workbench models the PSAppDeployToolkit deployment lifecycle as first- | `PUT` | `/api/psadt/profiles/:id` | User | Update a visible PSADT profile. | | `DELETE` | `/api/psadt/profiles/:id` | User | Delete a visible PSADT profile. | | `GET` | `/api/psadt/profiles/:id/render` | User | Render a profile into script scaffold and command-line metadata. | +| `POST` | `/api/psadt/profiles/:id/package` | User | Assemble a portable PSADT v4 package (rendered `Invoke-AppDeployToolkit.ps1` + `Files/`, `SupportFiles/`, `Assets/` from linked assets), zip it, and ingest the zip into the Asset Library. Pass `build: true` to also produce a `.intunewin` from the assembled tree. | + +The package builder lays out a PSADT v4 tree from a profile and its linked Asset +Library items: the rendered entry script at the root, and each linked asset placed +at its link `packagePath` or, when none is set, under the folder implied by its +link role (`installer`/`package-file` → `Files/`, `support-file`/`detection-script`/`reference` +→ `SupportFiles/`, `icon` → `Assets/`). The result is zipped (no archiver +dependency; a built-in deflate ZIP writer) and stored as an archive asset you can +download via `GET /api/assets/:id/download`. With `build: true` and an installer +in the package, the assembled folder is also handed to the `.intunewin` builder +(`IntuneWinAppUtil.exe` on Windows, or `INTUNEWIN_BUILD_COMMAND`); the response +returns both assets plus a manifest and any warnings. + +```jsonc +// POST /api/psadt/profiles/:id/package +{ "build": true, "setupFile": "Files/setup.exe" } +``` Profile payload: @@ -1357,7 +1408,10 @@ Settings update: "server_fqdn": "https://poshmanager.example.com", "trusted_origins": "https://poshmanager.example.com", "allow_script_execution": "true", - "entra_enabled": "false" + "entra_enabled": "false", + "myrtille_enabled": "true", + "myrtille_gateway_url": "https://rdp-gateway.contoso.local/Myrtille/", + "myrtille_use_password_hash": "true" } ``` @@ -1389,7 +1443,7 @@ on the target platform before broad execution. - SQLite access uses prepared statements through `node:sqlite`. - Credential secrets are encrypted with AES-256-GCM and are not returned by API reads. -- Browser RDP sessions use `mstsc.js` with short-lived POSHManager launch tokens; host passwords stay in the API process and are never embedded in the Vue app or launch URL. The upstream `mstsc.js` dependency is legacy and currently brings transitive npm audit findings, so expose `/rdp` only through authenticated POSHManager/reverse-proxy paths and restrict network access to trusted operators. +- Browser RDP sessions are brokered to an external Myrtille gateway. POSHManager validates the host and Credential Vault entry, decrypts the password server-side, and by default requests a Myrtille `passwordHash` so the browser receives a hash-based launch URL rather than the plaintext vault secret. Keep `MYRTILLE_ALLOW_PLAIN_PASSWORD=false` unless the gateway is isolated, HTTPS-only, and explicitly approved for plaintext Myrtille URL launches. - Request logs redact authorization and cookie headers. - Protected routes use JWT auth. - Admin-only APIs are guarded by `requireAdmin`. diff --git a/client/src/App.vue b/client/src/App.vue index 93a0648..0404877 100644 --- a/client/src/App.vue +++ b/client/src/App.vue @@ -256,6 +256,7 @@ :installer-analysis="installerAnalysis" :assets="assets" :applications="catalogApplications" + :recipes="packagingRecipes" :change-requests="changeRequests" :reporting-overview="reportingOverview" :validation-result="psadtValidationResult" @@ -266,6 +267,7 @@ @delete-profile="deletePsadtProfile" @render-profile="previewPsadtProfile" @create-script="createScriptFromPsadtProfile" + @package-profile="packagePsadtProfile" @insert-snippet="insertPsadtSnippet" @validate-psadt="validatePsadtTarget" @plan-migration="planPsadtMigration" @@ -286,6 +288,7 @@ @check-drift="checkDrift" @reconcile-graph-app="reconcileGraphApp" @new-version="createDeploymentVersion" + @generate-datasheet="generateDeploymentDatasheet" @build-intunewin="buildIntunewin" @save-application="saveApplication" @delete-application="deleteApplication" @@ -295,6 +298,7 @@ @approve-change-request="approveChangeRequest" @reject-change-request="rejectChangeRequest" @analyze-installer="analyzeInstaller" + @apply-recipe="applyRecipe" />
@@ -406,7 +410,13 @@ - +
@@ -446,8 +456,12 @@ @@ -1209,6 +1223,51 @@ const settingMetadata = { description: 'Master safety switch that controls whether RunPlans can execute scripts on target hosts.', section: 'Execution' }, + myrtille_enabled: { + label: 'Myrtille RDP Gateway', + description: 'Enable browser RDP launch through an external Myrtille gateway for Windows hosts.', + section: 'Integrations' + }, + myrtille_gateway_url: { + label: 'Myrtille Gateway URL', + description: 'Base URL to the Myrtille web app. POSHManager opens generated launch URLs here.', + placeholder: 'https://rdp-gateway.contoso.local/Myrtille/', + section: 'Integrations' + }, + myrtille_use_password_hash: { + label: 'Use Myrtille Password Hash', + description: 'Ask Myrtille to generate a gateway-specific passwordHash so plaintext passwords are not returned to the browser.', + section: 'Integrations' + }, + myrtille_allow_plain_password: { + label: 'Allow Plaintext RDP URL Fallback', + description: 'Permit the legacy Myrtille password URL parameter if passwordHash generation is disabled or fails. Keep this off unless the gateway is isolated and HTTPS-only.', + section: 'Integrations' + }, + myrtille_hash_endpoint: { + label: 'Myrtille Hash Endpoint', + description: 'Relative Myrtille endpoint used by the API to create passwordHash values.', + placeholder: 'GetHash.aspx', + section: 'Integrations' + }, + myrtille_default_width: { + label: 'Myrtille Default Width', + description: 'Initial remote desktop width passed to Myrtille launch URLs.', + placeholder: '1280', + section: 'Integrations' + }, + myrtille_default_height: { + label: 'Myrtille Default Height', + description: 'Initial remote desktop height passed to Myrtille launch URLs.', + placeholder: '800', + section: 'Integrations' + }, + myrtille_request_timeout_ms: { + label: 'Myrtille Request Timeout', + description: 'Maximum time the API waits for Myrtille passwordHash generation before failing or falling back.', + placeholder: '15000', + section: 'Integrations' + }, host_group_sync_interval_minutes: { label: 'Host Group Sync Interval', description: 'Minutes between background sync runs for dynamic host groups. Set 0 to disable the scheduler.', @@ -1286,6 +1345,7 @@ const psadtCatalog = ref({ module: {}, sources: [], supportMatrix: [], structure const psadtProfiles = ref([]); const psadtIntuneDeployments = ref([]); const catalogApplications = ref([]); +const packagingRecipes = ref([]); const changeRequests = ref([]); const reportingOverview = ref(null); const graphConnections = ref([]); @@ -1437,6 +1497,18 @@ const availableThemes = [ name: 'Aurora Glass', description: 'Cool blue and violet gradients with quiet contrast.', colors: ['#5de7ff', '#7c5cff', '#f5f7ff'] + }, + { + id: 'greyscale', + name: 'Greyscale Pro', + description: 'Neutral graphite surfaces with high-contrast enterprise controls.', + colors: ['#f4f4f5', '#9ca3af', '#18181b'] + }, + { + id: 'macos', + name: 'macOS Desktop', + description: 'Classic desktop chrome, frosted panels, and calmer toolbar controls.', + colors: ['#f5f5f7', '#d1d5db', '#007aff'] } ]; @@ -2238,18 +2310,20 @@ async function deleteCustomVariable(id) { } async function refreshPsadt() { - const [catalogRows, profileRows, intuneRows, graphRows, applicationRows] = await Promise.all([ + const [catalogRows, profileRows, intuneRows, graphRows, applicationRows, recipeRows] = await Promise.all([ api.get('/api/psadt/catalog'), api.get('/api/psadt/profiles'), api.get('/api/psadt/intune/deployments'), api.get('/api/graph/connections'), - api.get('/api/intune/applications') + api.get('/api/intune/applications'), + api.get('/api/packaging/recipes').catch(() => []) ]); psadtCatalog.value = catalogRows; psadtProfiles.value = profileRows; psadtIntuneDeployments.value = intuneRows; graphConnections.value = graphRows; catalogApplications.value = applicationRows; + packagingRecipes.value = recipeRows; await refreshGovernance(true); builderStatus.value = await api.get('/api/psadt/intune/builder/status').catch(() => null); notify('PSADT Workbench refreshed'); @@ -2264,6 +2338,23 @@ async function analyzeInstaller(payload) { } } +async function applyRecipe(payload) { + try { + const result = await api.post(`/api/packaging/recipes/${payload.id}/apply`, {}); + const [profileRows, intuneRows, applicationRows] = await Promise.all([ + api.get('/api/psadt/profiles'), + api.get('/api/psadt/intune/deployments'), + api.get('/api/intune/applications') + ]); + psadtProfiles.value = profileRows; + psadtIntuneDeployments.value = intuneRows; + catalogApplications.value = applicationRows; + notify(`Created draft package for ${result.profile?.name || 'recipe'}`); + } catch (err) { + notify(err.message, 'error'); + } +} + async function buildIntunewin(payload) { if (!payload.setupFile) return notify('Enter the setup file to build', 'error'); try { @@ -2374,6 +2465,41 @@ async function previewPsadtProfile(id) { notify('PSADT profile rendered into the editor'); } +async function packagePsadtProfile(id) { + try { + notify('Assembling PSADT package...'); + const result = await api.post(`/api/psadt/profiles/${id}/package`, {}); + const blob = await fetchAuthedBlob(`/api/assets/${result.asset.id}/download`); + const href = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = href; + link.download = result.asset.originalName; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(href); + await refreshAssets?.(); + const fileCount = result.manifest?.fileCount ?? 0; + notify(`Packaged ${result.asset.originalName} (${fileCount} linked file${fileCount === 1 ? '' : 's'})`); + for (const warning of result.warnings || []) notify(warning, 'error'); + } catch (err) { + notify(err.message, 'error'); + } +} + +async function generateDeploymentDatasheet(id) { + try { + notify('Generating package datasheet...'); + const blob = await fetchAuthedBlob(`/api/psadt/intune/deployments/${id}/datasheet?format=html`); + const href = URL.createObjectURL(blob); + window.open(href, '_blank', 'noopener'); + setTimeout(() => URL.revokeObjectURL(href), 60000); + notify('Package datasheet opened in a new tab'); + } catch (err) { + notify(err.message, 'error'); + } +} + async function createScriptFromPsadtProfile(id) { const rendered = await api.get(`/api/psadt/profiles/${id}/render`); const saved = await api.post('/api/scripts', { @@ -2638,7 +2764,7 @@ function writeRdpLaunchPopup(popup, hostName, state = 'loading', message = '') { - ${escapePreviewHtml(isError ? 'RDP launch failed' : `Opening RDP - ${hostName}`)} + ${escapePreviewHtml(isError ? 'RDP launch failed' : `Opening Myrtille RDP - ${hostName}`)} diff --git a/client/src/views/SystemJobsView.vue b/client/src/views/SystemJobsView.vue index f9c0e16..93c39a1 100644 --- a/client/src/views/SystemJobsView.vue +++ b/client/src/views/SystemJobsView.vue @@ -89,11 +89,11 @@