Files
poshmanager/server/services/psadtValidator.js
2026-06-25 12:05:53 -05:00

248 lines
9.2 KiB
JavaScript

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;
}