// Pure, framework-free helpers for the GE-Enforce manifest editor. // // ManifestEditor.vue imports these directly (the component no longer keeps its // own copies), so the unit tests in entryForm.spec.js exercise the shipped // code path. Change the editor logic HERE. // // Everything here is a plain function of its inputs. No Vue, no reactivity, // no network. That is the whole point - deterministic logic we can pin down. export const ENTRY_TYPES = ['MSI', 'EXE', 'CMD', 'BAT', 'PS1', 'INF', 'File', 'Registry'] export const REG_TYPES = ['String', 'DWord', 'QWord', 'MultiString', 'ExpandString', 'Binary'] export const DETECTION_METHODS = ['Registry', 'File', 'FileVersion', 'Hash', 'MarkerFile', 'ValueMatches', 'pnputil', 'Always'] export const INUSE_BEHAVIORS = ['Defer', 'CloseAndReopen', 'ForceClose', 'ScheduleForReboot'] // Plain-language description of each detection method, shown under the Detection // method dropdown so a first-time site admin understands what "present" means // for the method they picked. Key '' is the no-detection case. export const DETECTION_METHOD_HINTS = { '': 'No detection rule: the action runs every cycle.', Registry: 'Already correct if the registry value at Detection path/name exists (and equals Detection value when one is set).', File: 'Already correct if the file at Detection path exists.', FileVersion: 'Already correct if the file at Detection path is at Detection value or newer. This target feeds the Compliance panel.', Hash: 'Already correct if the file at Detection path matches the SHA256 in Detection value. Re-copies when the file changed.', MarkerFile: 'Already correct if the marker file at Detection path exists. Installs once, then the marker suppresses reruns.', ValueMatches: 'Already correct if the registry value at Detection path/name equals Detection value exactly.', pnputil: 'Already correct if a driver matching Detection pattern is staged in the Windows driver store. For INF entries.', Always: 'Never counts as present, so the action runs every cycle. Same effect as no detection rule.', } // Description for the currently selected detection method, or '' if unknown. export function detectionMethodHint(method) { return DETECTION_METHOD_HINTS[method || ''] || '' } // One blank entry form, matching the shape ManifestEditor seeds for a new entry. export function blankEntry() { return { Type: 'MSI', DetectionMethod: '', RegType: 'String', inuseBehavior: '', inuseProcesses: [], appid: null, PreEnrollment: false, KillAfterDetection: false, PCTypesStrict: false } } // Split a comma list into trimmed non-empty parts. export function splitList(value) { return (value || '').split(',').map(item => item.trim()).filter(Boolean) } // Turn an edit-form object into the API body sent to create/update an entry. // // Rules that matter (and that the specs pin): // - appid is always present (null unlinks). It is shopdb metadata; the backend // keeps it OFF the manifest JSON, so it is NOT one of the manifest scalars. // - empty/undefined/null scalars are dropped. // - Registry DWord/QWord values become real numbers; other reg types stay strings. // - comma lists become arrays, and are dropped when empty. // - preinstall flags only appear when truthy. // - InUseCheck carries per-process Name, optional ExePath, optional numeric timeout. export function buildEntryPayload(form) { // appid is shopdb metadata, always sent (null unlinks); the backend keeps it // off the manifest JSON. const out = { Name: form.Name, Type: form.Type, appid: form.appid ?? null } const scalars = ['Installer', 'InstallArgs', 'Script', 'Args', 'Source', 'Destination', 'RegPath', 'RegName', 'RegType', 'DetectionPath', 'DetectionName', 'DetectionValue', 'DetectionPattern', '_CmmVersion', 'LogFile', 'ApplyMode', 'UpdateWindow', '_comment'] for (const key of scalars) { if (form[key] !== undefined && form[key] !== '' && form[key] !== null) out[key] = form[key] } if (form.DetectionMethod) out.DetectionMethod = form.DetectionMethod if (form.WaitTimeoutSec) out.WaitTimeoutSec = form.WaitTimeoutSec if (form.Type === 'Registry' && form.RegValue !== undefined && form.RegValue !== '') { out.RegValue = ['DWord', 'QWord'].includes(form.RegType) ? Number(form.RegValue) : form.RegValue } const pctypes = splitList(form.PCTypes) if (pctypes.length) out.PCTypes = pctypes const hostnames = splitList(form.TargetHostnames) if (hostnames.length) out.TargetHostnames = hostnames const machinenumbers = splitList(form.TargetMachineNumbers) if (machinenumbers.length) out.TargetMachineNumbers = machinenumbers for (const flag of ['PreEnrollment', 'KillAfterDetection', 'PCTypesStrict']) { if (form[flag]) out[flag] = true } if (form.inuseBehavior) { out.InUseCheck = { Behavior: form.inuseBehavior, Processes: (form.inuseProcesses || []).filter(process => process.name).map(process => { const processData = { Name: process.name } if (process.exepath) processData.ExePath = process.exepath if (process.timeout !== null && process.timeout !== '' && process.timeout !== undefined) { processData.GracefulCloseTimeoutSec = Number(process.timeout) } return processData }), } } return out } // Which entry types the type dropdown offers, given the scope phase. Preinstall // supports MSI/EXE only, but keeps a current out-of-set value visible. export function availableEntryTypes(isPreinstall, currentType) { if (!isPreinstall) return ENTRY_TYPES const allowed = ['MSI', 'EXE'] return currentType && !allowed.includes(currentType) ? [...allowed, currentType] : allowed } // Which detection methods the dropdown offers. Preinstall supports Registry/File // only, but keeps a current out-of-set value visible. export function availableDetectionMethods(isPreinstall, currentMethod) { if (!isPreinstall) return DETECTION_METHODS const allowed = ['Registry', 'File'] return currentMethod && !allowed.includes(currentMethod) ? [...allowed, currentMethod] : allowed } // Which targeting gates a scope actually surfaces by default (before "Show all"). export function targetingGates(scope) { if (!scope) { return { pctypes: true, cmmversion: false, machinenumbers: false, hostnames: false } } const name = (scope.scopename || '').toLowerCase() const entries = scope.entries || [] const fleetwide = scope.iscommon || scope.phase === 'preinstall' || name === 'common' return { pctypes: fleetwide || entries.some(e => (e.PCTypes || []).length), cmmversion: /cmm/.test(name) || entries.some(e => e._CmmVersion), // Data-driven (a loose name match would wrongly flag 'nocollections'). machinenumbers: entries.some(e => (e.TargetMachineNumbers || []).length), hostnames: entries.some(e => (e.TargetHostnames || []).length), } } // The contextual hint under the Targeting header. export function targetingHint(scope) { if (!scope) return '' if (scope.iscommon || scope.scopename === 'common') { return 'Fleet-wide manifest: use PC types to target which types get this entry.' } if (scope.phase === 'preinstall') { return 'Preinstall manifest: use PC types to target; preinstall flags apply here.' } return `This manifest already runs only on ${scope.scopename} PCs. ` + 'Targeting below narrows within that (version, bay, or subtype).' } // One-line summary of what a scope installs (installer entries only). export function scopeSummary(scope) { if (!scope || !scope.entries || !scope.entries.length) return '' const installers = scope.entries .filter(e => ['MSI', 'EXE', 'CMD', 'BAT'].includes(e.Type)) .map(e => e.Name) const shown = installers.slice(0, 6).join(', ') const more = installers.length > 6 ? `, +${installers.length - 6} more` : '' const apps = installers.length ? `Installs ${shown}${more}. ` : '' return `${apps}${scope.entries.length} entries; runs after common.` } // Verb table for describeEntry. export const ACTION_VERB = { MSI: 'Installs', EXE: 'Installs', CMD: 'Runs', BAT: 'Runs', PS1: 'Runs a script for', INF: 'Installs a driver for', File: 'Copies a file for', Registry: 'Sets a registry value for', } // Plain-English one-liner: what an entry does, when it self-heals, who it hits. export function describeEntry(entry) { let text = `${ACTION_VERB[entry.Type] || 'Applies'} ${entry.Name}` const method = entry.DetectionMethod if (!method || method === 'Always') text += '; runs every cycle' else if (method === 'FileVersion') text += `; reinstalls unless version is ${entry.DetectionValue || 'set'}` else if (method === 'Hash') text += '; re-copies if the file changed' else if (method === 'MarkerFile') text += '; installs once' else text += '; reinstalls if not detected' if (entry._CmmVersion) text += `; CMM ${entry._CmmVersion} bays only` else if (entry.TargetMachineNumbers && entry.TargetMachineNumbers.length) { text += `; ${entry.TargetMachineNumbers.length} specific bay(s)` } else if (entry.TargetHostnames && entry.TargetHostnames.length) { text += '; specific hostname(s)' } else if (entry.PCTypes && entry.PCTypes.length) { text += `; ${entry.PCTypes.length} PC type(s)` } if (entry.appname) text += `; tracked: ${entry.appname}` return text }