import { psadtValidationRules } from '../data/psadtValidationRules.js'; const ruleById = new Map(psadtValidationRules.map((rule) => [rule.id, rule])); const legacyFunctions = [ 'Execute-Process', 'Execute-MSI', 'Execute-MSP', 'Show-InstallationWelcome', 'Show-InstallationPrompt', 'Show-InstallationProgress', 'Show-InstallationRestartPrompt', 'Write-Log', 'Remove-MSIApplications', 'Invoke-HKCURegistrySettingsForAllUsers' ]; export function validationRuleCatalog() { return psadtValidationRules; } export function validatePsadtScript(content = '', context = {}) { const text = String(content || ''); const findings = [ ...checkPowerShellRequirement(text), ...checkRequireAdminConfig(text), ...checkRemovedDetectionConfig(text), ...checkAppProcessesToClose(text), ...checkSessionCleanup(text), ...checkModuleVersionDrift(text), ...checkDeployModeAuto(text), ...checkLegacyFunctions(text), ...checkLegacyDeploymentNaming(text), ...checkLegacyDeploymentVariables(text), ...checkEnvironmentVariables(text), ...checkServiceUi(text), ...checkDeferRunInterval(text, context), ...checkPromptIcon(text), ...checkRawMsiExec(text), ...checkStartProcessWait(text), ...checkExitCodes(text), ...checkGlobalModuleImport(text), ...checkDeprecatedObjectProperty(text), ...checkUserContextRequireAdmin(text, context) ].map((finding, index) => ({ id: `${finding.ruleId}-${index + 1}`, ...ruleById.get(finding.ruleId), ...finding })); return { ok: findings.every((finding) => !['critical', 'error'].includes(finding.severity)), score: scoreFindings(findings), counts: countFindings(findings), findings, rules: psadtValidationRules, analyzedAt: new Date().toISOString() }; } function add(ruleId, text, match, extra = {}) { return { ruleId, line: lineNumber(text, match.index ?? 0), excerpt: lineText(text, match.index ?? 0), ...extra }; } function checkPowerShellRequirement(text) { const findings = []; for (const match of text.matchAll(/#requires\s+-version\s+([0-9.]+)/gi)) { const version = Number.parseFloat(match[1]); if (Number.isFinite(version) && version < 5.1) findings.push(add('REQ-PSVERSION-MINIMUM', text, match, { detected: match[0] })); } return findings; } function checkRequireAdminConfig(text) { const findings = []; const configPattern = /(Toolkit\.)?RequireAdmin\s*=/gi; for (const match of text.matchAll(configPattern)) { const nearby = text.slice(Math.max(0, match.index - 80), match.index + 160); if (!/\$adtSession\s*=|@\{|RequireAdmin\s*=\s*\$(true|false)/i.test(nearby) || /Toolkit\.RequireAdmin/i.test(match[0])) { findings.push(add('UPG-REQUIREADMIN-CONFIG', text, match)); } } return findings; } function checkRemovedDetectionConfig(text) { return [...text.matchAll(/\b(Toolkit\.)?(OOBEDetection|SessionDetection)\b/gi)] .map((match) => add('UPG-REMOVED-CONFIG-DETECTION', text, match, { detected: match[0] })); } function checkAppProcessesToClose(text) { if (/AppProcessesToClose/i.test(text)) return []; return [...text.matchAll(/Show-ADTInstallationWelcome[^\n\r]*-CloseProcesses\s+(?!\$adtSession\.AppProcessesToClose)/gi)] .map((match) => add('UPG-APP-PROCESSES-SESSION', text, match)); } function checkSessionCleanup(text) { if (!/Open-ADTSession/i.test(text)) return []; if (/Remove-ADTHashtableNullOrEmptyValues/i.test(text)) return []; const match = /Open-ADTSession/i.exec(text); return match ? [add('UPG-MISSING-SESSION-CLEANUP', text, match)] : []; } function checkLegacyFunctions(text) { const pattern = new RegExp(`\\b(${legacyFunctions.map(escapeRegExp).join('|')})\\b`, 'gi'); return [...text.matchAll(pattern)] .filter((match) => !isCommentMatch(text, match)) .map((match) => add('LEGACY-V3-FUNCTION', text, match, { detected: match[0] })); } function checkLegacyDeploymentNaming(text) { return [...text.matchAll(/\bDeploy-Application\.(ps1|exe)\b/gi)] .filter((match) => !isCommentMatch(text, match)) .map((match) => add('LEGACY-DEPLOY-APPLICATION', text, match)); } function checkLegacyDeploymentVariables(text) { return [...text.matchAll(/\$(appName|appVersion|appVendor|appArch|appLang|appRevision)\b/g)] .map((match) => add('LEGACY-DEPLOYMENT-VARIABLE', text, match, { detected: match[0] })); } function checkEnvironmentVariables(text) { if (/Open-ADTSession|Export-ADTEnvironmentTableToSessionState/i.test(text)) return []; return [...text.matchAll(/\$(envProgramFiles|envProgramFilesX86|envSystemRoot|envWinDir|envTemp)\b/g)] .map((match) => add('FAQ-ENV-VAR-BEFORE-SESSION', text, match, { detected: match[0] })); } function checkServiceUi(text) { return [...text.matchAll(/\b(ServiceUI\.exe|Invoke-ServiceUI\.ps1)\b/gi)] .filter((match) => !isCommentMatch(text, match)) .map((match) => add('INTUNE-SERVICEUI', text, match)); } function checkDeferRunInterval(text, context) { const findings = []; const intuneHint = context.platform === 'intune' || /Intune|IME|Microsoft Intune/i.test(text); for (const match of text.matchAll(/Show-ADTInstallationWelcome[^\n\r]*-AllowDefer\b(?![^\n\r]*-DeferRunInterval)/gi)) { if (intuneHint || /Invoke-AppDeployToolkit\.exe|DeployMode/i.test(text)) findings.push(add('INTUNE-DEFER-RUN-INTERVAL', text, match)); } return findings; } function checkPromptIcon(text) { return [...text.matchAll(/Show-ADTInstallationPrompt[^\n\r]*\s-Icon\b/gi)] .filter((match) => !isCommentMatch(text, match)) .map((match) => add('UI-PROMPT-ICON-FLUENT', text, match)); } function checkRawMsiExec(text) { return [...text.matchAll(/\bmsiexec(?:\.exe)?\b[^\n\r]*(\/i|\/x|\/f|\/p)/gi)] .filter((match) => !isCommentMatch(text, match)) .map((match) => add('INSTALL-RAW-MSIEXEC', text, match)); } function checkStartProcessWait(text) { return [...text.matchAll(/Start-ADTProcess[^\n\r]*-FilePath\s+['"][^'"]+\.(exe|cmd|bat)['"][^\n\r]*/gi)] .filter((match) => !/-WaitForChildProcesses\b/i.test(match[0]) && !/-NoWait\b/i.test(match[0])) .map((match) => add('INSTALL-PROCESS-NO-WAIT-CHILD', text, match)); } function checkExitCodes(text) { const findings = []; for (const match of text.matchAll(/\b(6[0-9]{4}|7[0-9]{4})\b/g)) { if (isCommentMatch(text, match)) continue; const code = Number(match[1]); if (code >= 60000 && code <= 68999) findings.push(add('EXIT-RESERVED-BUILTIN', text, match, { detected: String(code) })); if (code >= 69000 && code <= 69999) findings.push(add('EXIT-CUSTOM-RANGE', text, match, { detected: String(code) })); } return findings; } function checkModuleVersionDrift(text) { const findings = []; for (const match of text.matchAll(/ModuleVersion\s*=\s*['"]([0-9.]+)['"]/gi)) { if (isCommentMatch(text, match)) continue; if (compareVersion(match[1], '4.2.0') < 0) findings.push(add('REPO-MODULE-VERSION-DRIFT', text, match, { detected: match[1] })); } return findings; } function checkDeployModeAuto(text) { return [...text.matchAll(/-DeployMode\s+(Interactive|Silent|NonInteractive)\b/gi)] .filter((match) => !isCommentMatch(text, match)) .map((match) => add('REPO-DEPLOYMODE-NO-AUTO', text, match, { detected: match[1] })); } function checkDeprecatedObjectProperty(text) { return [...text.matchAll(/\bGet-ADTObjectProperty\b/gi)] .filter((match) => !isCommentMatch(text, match)) .map((match) => add('DEPRECATED-GET-ADTOBJECTPROPERTY', text, match)); } function checkGlobalModuleImport(text) { return [...text.matchAll(/Import-Module\s+['"]?PSAppDeployToolkit['"]?(?![\\/])/gi)].map((match) => add('FAQ-GLOBAL-MODULE-IMPORT', text, match)); } function checkUserContextRequireAdmin(text, context) { if (!/Start-ADTProcessAsUser|Start-ADTMsiProcessAsUser/i.test(text)) return []; if (/RequireAdmin\s*=\s*\$false/i.test(text) || context.requireAdmin === false) return []; const match = /Start-ADTMsiProcessAsUser|Start-ADTProcessAsUser/i.exec(text); return match ? [add('USERCONTEXT-REQUIREADMIN', text, match)] : []; } function scoreFindings(findings) { const penalties = { critical: 35, error: 22, warning: 10, info: 3 }; const penalty = findings.reduce((total, finding) => total + (penalties[finding.severity] || 0), 0); return Math.max(0, 100 - penalty); } function countFindings(findings) { return findings.reduce((counts, finding) => { counts[finding.severity] = (counts[finding.severity] || 0) + 1; counts.total += 1; return counts; }, { total: 0, critical: 0, error: 0, warning: 0, info: 0 }); } function lineNumber(text, index) { return text.slice(0, index).split(/\r?\n/).length; } function lineText(text, index) { const lines = text.split(/\r?\n/); return lines[lineNumber(text, index) - 1]?.trim().slice(0, 240) || ''; } function escapeRegExp(value) { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function isCommentMatch(text, match) { return lineText(text, match.index ?? 0).trimStart().startsWith('#'); } function compareVersion(left, right) { const a = String(left).split('.').map((part) => Number(part) || 0); const b = String(right).split('.').map((part) => Number(part) || 0); for (let index = 0; index < Math.max(a.length, b.length); index += 1) { if ((a[index] || 0) < (b[index] || 0)) return -1; if ((a[index] || 0) > (b[index] || 0)) return 1; } return 0; }