This commit is contained in:
2026-06-25 12:41:33 -05:00
parent 8c60e154a7
commit 56e34475fc
16 changed files with 113 additions and 46 deletions

35
package-lock.json generated
View File

@@ -20,6 +20,7 @@
"monaco-editor": "^0.53.0", "monaco-editor": "^0.53.0",
"multer": "^2.2.0", "multer": "^2.2.0",
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"path": "^0.12.7",
"tailwindcss": "^4.3.1", "tailwindcss": "^4.3.1",
"vite": "^8.1.0", "vite": "^8.1.0",
"vue": "^3.5.26", "vue": "^3.5.26",
@@ -2584,6 +2585,16 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/path": {
"version": "0.12.7",
"resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz",
"integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==",
"license": "MIT",
"dependencies": {
"process": "^0.11.1",
"util": "^0.10.3"
}
},
"node_modules/path-to-regexp": { "node_modules/path-to-regexp": {
"version": "8.4.2", "version": "8.4.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
@@ -2706,6 +2717,15 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
} }
}, },
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -3319,12 +3339,27 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/util": {
"version": "0.10.4",
"resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
"integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
"license": "MIT",
"dependencies": {
"inherits": "2.0.3"
}
},
"node_modules/util-deprecate": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/util/node_modules/inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
"license": "ISC"
},
"node_modules/vary": { "node_modules/vary": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

View File

@@ -29,6 +29,7 @@
"monaco-editor": "^0.53.0", "monaco-editor": "^0.53.0",
"multer": "^2.2.0", "multer": "^2.2.0",
"nanoid": "^5.1.6", "nanoid": "^5.1.6",
"path": "^0.12.7",
"tailwindcss": "^4.3.1", "tailwindcss": "^4.3.1",
"vite": "^8.1.0", "vite": "^8.1.0",
"vue": "^3.5.26", "vue": "^3.5.26",

View File

@@ -1,10 +1,11 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'path';
import { spawnSync } from 'node:child_process'; import { spawnSync } from 'node:child_process';
if (fs.existsSync('dist/index.html')) { if (fs.existsSync(path.join('dist', 'index.html'))) {
process.exit(0); process.exit(0);
} }
const result = spawnSync(process.execPath, ['node_modules/vite/bin/vite.js', 'build'], { stdio: 'inherit' }); const result = spawnSync(process.execPath, [path.join('node_modules', 'vite', 'bin', 'vite.js'), 'build'], { stdio: 'inherit' });
process.exit(result.status ?? 1); process.exit(result.status ?? 1);

View File

@@ -1,10 +1,11 @@
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import path from 'path';
const mode = process.argv[2] || 'development'; const mode = process.argv[2] || 'development';
const watch = process.argv.includes('--watch'); const watch = process.argv.includes('--watch');
const args = watch const args = watch
? ['node_modules/nodemon/bin/nodemon.js', 'server/index.js'] ? [path.join('node_modules', 'nodemon', 'bin', 'nodemon.js'), path.join('server', 'index.js')]
: ['server/index.js']; : [path.join('server', 'index.js')];
const child = spawn(process.execPath, args, { const child = spawn(process.execPath, args, {
stdio: 'inherit', stdio: 'inherit',

View File

@@ -1,6 +1,7 @@
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import path from 'path';
const child = spawn(process.execPath, ['node_modules/vite/bin/vite.js', 'preview', '--host', '0.0.0.0', '--port', '3000', '--strictPort'], { const child = spawn(process.execPath, [path.join('node_modules', 'vite', 'bin', 'vite.js'), 'preview', '--host', '0.0.0.0', '--port', '3000', '--strictPort'], {
stdio: 'inherit', stdio: 'inherit',
env: { env: {
...process.env, ...process.env,

View File

@@ -1,5 +1,5 @@
import dotenv from 'dotenv'; import dotenv from 'dotenv';
import path from 'node:path'; import { path } from './utils/pathUtils.js';
dotenv.config({ quiet: true }); dotenv.config({ quiet: true });

View File

@@ -17,7 +17,6 @@ import {
updatePsadtProfile updatePsadtProfile
} from '../models/psadtModel.js'; } from '../models/psadtModel.js';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path';
import { config } from '../config.js'; import { config } from '../config.js';
import { createChangeRequest, findApprovedRequest, findPendingRequest, markRequestExecuted } from '../models/changeRequestModel.js'; import { createChangeRequest, findApprovedRequest, findPendingRequest, markRequestExecuted } from '../models/changeRequestModel.js';
import { buildIntuneWin, builderAvailability } from '../services/intuneWinBuilder.js'; import { buildIntuneWin, builderAvailability } from '../services/intuneWinBuilder.js';
@@ -30,6 +29,7 @@ import { parseIntunewin } from '../services/intunewinParser.js';
import { createAssetFromUpload, getAssetFile } from '../models/assetModel.js'; import { createAssetFromUpload, getAssetFile } from '../models/assetModel.js';
import { validatePsadtScript, validationRuleCatalog } from '../services/psadtValidator.js'; import { validatePsadtScript, validationRuleCatalog } from '../services/psadtValidator.js';
import { planPsadtMigration } from '../services/psadtMigrationService.js'; import { planPsadtMigration } from '../services/psadtMigrationService.js';
import { basenameAny, path } from '../utils/pathUtils.js';
export function catalog(req, res) { export function catalog(req, res) {
res.json(getPsadtCatalog()); res.json(getPsadtCatalog());
@@ -353,9 +353,10 @@ export async function intuneBuild(req, res) {
try { try {
const { outputFile } = await buildIntuneWin({ sourceFolder, setupFile, outputDir }); const { outputFile } = await buildIntuneWin({ sourceFolder, setupFile, outputDir });
const stats = fs.statSync(outputFile); const stats = fs.statSync(outputFile);
const outputName = basenameAny(outputFile, 'package.intunewin');
const asset = createAssetFromUpload( const asset = createAssetFromUpload(
{ path: outputFile, originalname: path.basename(outputFile), mimetype: 'application/octet-stream', size: stats.size }, { path: outputFile, originalname: outputName, mimetype: 'application/octet-stream', size: stats.size },
{ name: `${deployment.name} package`, visibility: deployment.visibility, groupId: deployment.groupId, packagePath: path.basename(outputFile) }, { name: `${deployment.name} package`, visibility: deployment.visibility, groupId: deployment.groupId, packagePath: outputName },
req.user.id req.user.id
); );
const updated = updateIntuneDeployment(req.params.id, { ...deployment, intunewinFile: asset.originalName }, req.user.id); const updated = updateIntuneDeployment(req.params.id, { ...deployment, intunewinFile: asset.originalName }, req.user.id);

View File

@@ -1,5 +1,5 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import { path } from './utils/pathUtils.js';
import { DatabaseSync } from 'node:sqlite'; import { DatabaseSync } from 'node:sqlite';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';

View File

@@ -1,20 +1,12 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path';
import multer from 'multer'; import multer from 'multer';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { config } from '../config.js'; import { config } from '../config.js';
import { path, sanitizeFileName } from '../utils/pathUtils.js';
const incomingDir = path.join(config.fileLockerDir, '_incoming'); const incomingDir = path.resolve(config.fileLockerDir, '_incoming');
fs.mkdirSync(incomingDir, { recursive: true }); fs.mkdirSync(incomingDir, { recursive: true });
function sanitizeFileName(name) {
return String(name || 'asset.bin')
.replace(/[/\\?%*:|"<>]/g, '-')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 180) || 'asset.bin';
}
const storage = multer.diskStorage({ const storage = multer.diskStorage({
destination(req, file, callback) { destination(req, file, callback) {
fs.mkdirSync(incomingDir, { recursive: true }); fs.mkdirSync(incomingDir, { recursive: true });

View File

@@ -1,30 +1,13 @@
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path';
import { config } from '../config.js'; import { config } from '../config.js';
import { db, id, now, visibleClause } from '../db.js'; import { db, id, now, visibleClause } from '../db.js';
import { camelAsset, camelAssetFolder, camelAssetLink } from './mappers.js'; import { camelAsset, camelAssetFolder, camelAssetLink } from './mappers.js';
import { extnameAny, isPathInside, normalizePackagePath, path, sanitizeFileName } from '../utils/pathUtils.js';
function sanitizeFileName(name) {
return String(name || 'asset.bin')
.replace(/[/\\?%*:|"<>]/g, '-')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 180) || 'asset.bin';
}
function normalizePackagePath(value, fallbackName = '') {
const clean = String(value || fallbackName || '')
.replace(/\\/g, '/')
.split('/')
.filter((part) => part && part !== '.' && part !== '..')
.join('/');
return clean.slice(0, 500);
}
function inferKind(mimeType = '', fileName = '') { function inferKind(mimeType = '', fileName = '') {
const mime = mimeType.toLowerCase(); const mime = mimeType.toLowerCase();
const ext = path.extname(fileName).toLowerCase(); const ext = extnameAny(fileName);
if (mime.startsWith('image/')) return 'image'; if (mime.startsWith('image/')) return 'image';
if (mime.includes('zip') || ['.zip', '.intunewin', '.7z', '.rar'].includes(ext)) return 'archive'; if (mime.includes('zip') || ['.zip', '.intunewin', '.7z', '.rar'].includes(ext)) return 'archive';
if (['.msi', '.msp', '.exe'].includes(ext)) return 'installer'; if (['.msi', '.msp', '.exe'].includes(ext)) return 'installer';
@@ -162,8 +145,7 @@ export function getAssetFile(assetId, userId) {
const row = selectAssetById(assetId, userId); const row = selectAssetById(assetId, userId);
if (!row) return null; if (!row) return null;
const resolved = path.resolve(row.storage_path); const resolved = path.resolve(row.storage_path);
const fileLockerRoot = path.resolve(config.fileLockerDir); if (!isPathInside(config.fileLockerDir, resolved)) throw new Error('Asset file path is outside the configured FileLocker directory');
if (!resolved.startsWith(fileLockerRoot)) throw new Error('Asset file path is outside the configured FileLocker directory');
return { asset: camelAsset(row), filePath: resolved }; return { asset: camelAsset(row), filePath: resolved };
} }

View File

@@ -1,7 +1,7 @@
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path';
import { config } from '../config.js'; import { config } from '../config.js';
import { nameWithoutExtensionAny, path } from '../utils/pathUtils.js';
// Wraps a PSADT/package source folder into a `.intunewin` using the Microsoft // Wraps a PSADT/package source folder into a `.intunewin` using the Microsoft
// Win32 Content Prep Tool (IntuneWinAppUtil.exe) on Windows, or an optional // Win32 Content Prep Tool (IntuneWinAppUtil.exe) on Windows, or an optional
@@ -45,7 +45,7 @@ export function buildPackagerInvocation({ toolPath, sourceFolder, setupFile, out
// IntuneWinAppUtil names the output after the setup file's base name. // IntuneWinAppUtil names the output after the setup file's base name.
export function expectedOutputFile(outputDir, setupFile) { export function expectedOutputFile(outputDir, setupFile) {
return path.join(outputDir, `${path.parse(setupFile).name}.intunewin`); return path.join(outputDir, `${nameWithoutExtensionAny(setupFile, 'package')}.intunewin`);
} }
export function buildIntuneWin({ sourceFolder, setupFile, outputDir }) { export function buildIntuneWin({ sourceFolder, setupFile, outputDir }) {

View File

@@ -1,5 +1,5 @@
import fs from 'node:fs'; import fs from 'node:fs';
import path from 'node:path'; import { path } from '../utils/pathUtils.js';
import winston from 'winston'; import winston from 'winston';
import { config } from '../config.js'; import { config } from '../config.js';

View File

@@ -1,6 +1,5 @@
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import { db, id, now } from '../db.js'; import { db, id, now } from '../db.js';
import { decryptSecret } from './cryptoStore.js'; import { decryptSecret } from './cryptoStore.js';
@@ -9,6 +8,7 @@ import { logger } from './logger.js';
import { listExecutableAssets } from '../models/assetModel.js'; import { listExecutableAssets } from '../models/assetModel.js';
import { listExecutableCustomVariables } from '../models/variableModel.js'; import { listExecutableCustomVariables } from '../models/variableModel.js';
import { getBooleanSetting } from '../models/settingsModel.js'; import { getBooleanSetting } from '../models/settingsModel.js';
import { path } from '../utils/pathUtils.js';
// The execution kill-switch can be set at boot via ALLOW_SCRIPT_EXECUTION and // The execution kill-switch can be set at boot via ALLOW_SCRIPT_EXECUTION and
// flipped at runtime via the Config screen (allow_script_execution). The DB // flipped at runtime via the Config screen (allow_script_execution). The DB

View File

@@ -4,6 +4,7 @@ import assert from 'node:assert/strict';
import { load } from './setup.mjs'; import { load } from './setup.mjs';
const { buildPackagerInvocation, expectedOutputFile, resolveToolPath, builderAvailability } = await load('../services/intuneWinBuilder.js'); const { buildPackagerInvocation, expectedOutputFile, resolveToolPath, builderAvailability } = await load('../services/intuneWinBuilder.js');
const { isPathInside, normalizePackagePath, sanitizeFileName } = await load('../utils/pathUtils.js');
test('bundled invocation uses IntuneWinAppUtil flags', () => { test('bundled invocation uses IntuneWinAppUtil flags', () => {
const inv = buildPackagerInvocation({ toolPath: 'C:/tools/IntuneWinAppUtil.exe', sourceFolder: 'C:/src', setupFile: 'setup.exe', outputDir: 'C:/out' }); const inv = buildPackagerInvocation({ toolPath: 'C:/tools/IntuneWinAppUtil.exe', sourceFolder: 'C:/src', setupFile: 'setup.exe', outputDir: 'C:/out' });
@@ -24,6 +25,18 @@ test('external command template substitutes placeholders', () => {
test('expectedOutputFile names after the setup base name', () => { test('expectedOutputFile names after the setup base name', () => {
assert.ok(expectedOutputFile('/out', 'ContosoVpnSetup.exe').endsWith('ContosoVpnSetup.intunewin')); assert.ok(expectedOutputFile('/out', 'ContosoVpnSetup.exe').endsWith('ContosoVpnSetup.intunewin'));
assert.ok(expectedOutputFile('/out', 'app.msi').endsWith('app.intunewin')); assert.ok(expectedOutputFile('/out', 'app.msi').endsWith('app.intunewin'));
assert.ok(expectedOutputFile('/out', 'Files\\Vendor Setup.exe').endsWith('Vendor Setup.intunewin'));
});
test('path utilities normalize Windows-style upload and package paths', () => {
assert.equal(sanitizeFileName('C:\\temp\\Contoso VPN?.msi'), 'Contoso VPN-.msi');
assert.equal(normalizePackagePath('Files\\Installers\\..\\setup.exe'), 'Files/Installers/setup.exe');
assert.equal(normalizePackagePath('../SupportFiles/config.json'), 'SupportFiles/config.json');
});
test('path containment rejects sibling directories with the same prefix', () => {
assert.equal(isPathInside('/tmp/FileLocker', '/tmp/FileLocker/assets/a.bin'), true);
assert.equal(isPathInside('/tmp/FileLocker', '/tmp/FileLocker2/assets/a.bin'), false);
}); });
test('resolveToolPath defaults into the tools directory', () => { test('resolveToolPath defaults into the tools directory', () => {

View File

@@ -3,7 +3,7 @@
// loaded, then migrates + seeds a fresh schema. Import this first, then use // loaded, then migrates + seeds a fresh schema. Import this first, then use
// `load()` to dynamically import the modules under test. // `load()` to dynamically import the modules under test.
import os from 'node:os'; import os from 'node:os';
import path from 'node:path'; import path from 'path';
import fs from 'node:fs'; import fs from 'node:fs';
import crypto from 'node:crypto'; import crypto from 'node:crypto';

40
server/utils/pathUtils.js Normal file
View File

@@ -0,0 +1,40 @@
import path from 'path';
export { path };
export function basenameAny(value, fallback = '') {
const normalized = String(value || fallback || '').replace(/\\/g, '/');
return path.posix.basename(normalized);
}
export function extnameAny(value) {
return path.extname(basenameAny(value)).toLowerCase();
}
export function nameWithoutExtensionAny(value, fallback = '') {
return path.parse(basenameAny(value, fallback)).name;
}
export function sanitizeFileName(name, fallback = 'asset.bin') {
return basenameAny(name, fallback)
.replace(/[/\\?%*:|"<>]/g, '-')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 180) || fallback;
}
export function normalizePackagePath(value, fallbackName = '') {
const clean = String(value || fallbackName || '')
.replace(/\\/g, '/')
.split('/')
.filter((part) => part && part !== '.' && part !== '..')
.join('/');
return clean.slice(0, 500);
}
export function isPathInside(parentDir, candidatePath) {
const parent = path.resolve(parentDir);
const candidate = path.resolve(candidatePath);
const relative = path.relative(parent, candidate);
return relative === '' || (relative && !relative.startsWith('..') && !path.isAbsolute(relative));
}