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

View File

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

View File

@@ -17,7 +17,6 @@ import {
updatePsadtProfile
} from '../models/psadtModel.js';
import fs from 'node:fs';
import path from 'node:path';
import { config } from '../config.js';
import { createChangeRequest, findApprovedRequest, findPendingRequest, markRequestExecuted } from '../models/changeRequestModel.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 { validatePsadtScript, validationRuleCatalog } from '../services/psadtValidator.js';
import { planPsadtMigration } from '../services/psadtMigrationService.js';
import { basenameAny, path } from '../utils/pathUtils.js';
export function catalog(req, res) {
res.json(getPsadtCatalog());
@@ -353,9 +353,10 @@ export async function intuneBuild(req, res) {
try {
const { outputFile } = await buildIntuneWin({ sourceFolder, setupFile, outputDir });
const stats = fs.statSync(outputFile);
const outputName = basenameAny(outputFile, 'package.intunewin');
const asset = createAssetFromUpload(
{ path: outputFile, originalname: path.basename(outputFile), mimetype: 'application/octet-stream', size: stats.size },
{ name: `${deployment.name} package`, visibility: deployment.visibility, groupId: deployment.groupId, packagePath: path.basename(outputFile) },
{ path: outputFile, originalname: outputName, mimetype: 'application/octet-stream', size: stats.size },
{ name: `${deployment.name} package`, visibility: deployment.visibility, groupId: deployment.groupId, packagePath: outputName },
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 path from 'node:path';
import { path } from './utils/pathUtils.js';
import { DatabaseSync } from 'node:sqlite';
import bcrypt from 'bcryptjs';
import { nanoid } from 'nanoid';

View File

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

View File

@@ -1,30 +1,13 @@
import crypto from 'node:crypto';
import fs from 'node:fs';
import path from 'node:path';
import { config } from '../config.js';
import { db, id, now, visibleClause } from '../db.js';
import { camelAsset, camelAssetFolder, camelAssetLink } from './mappers.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);
}
import { extnameAny, isPathInside, normalizePackagePath, path, sanitizeFileName } from '../utils/pathUtils.js';
function inferKind(mimeType = '', fileName = '') {
const mime = mimeType.toLowerCase();
const ext = path.extname(fileName).toLowerCase();
const ext = extnameAny(fileName);
if (mime.startsWith('image/')) return 'image';
if (mime.includes('zip') || ['.zip', '.intunewin', '.7z', '.rar'].includes(ext)) return 'archive';
if (['.msi', '.msp', '.exe'].includes(ext)) return 'installer';
@@ -162,8 +145,7 @@ export function getAssetFile(assetId, userId) {
const row = selectAssetById(assetId, userId);
if (!row) return null;
const resolved = path.resolve(row.storage_path);
const fileLockerRoot = path.resolve(config.fileLockerDir);
if (!resolved.startsWith(fileLockerRoot)) throw new Error('Asset file path is outside the configured FileLocker directory');
if (!isPathInside(config.fileLockerDir, resolved)) throw new Error('Asset file path is outside the configured FileLocker directory');
return { asset: camelAsset(row), filePath: resolved };
}

View File

@@ -1,7 +1,7 @@
import { spawn } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { config } from '../config.js';
import { nameWithoutExtensionAny, path } from '../utils/pathUtils.js';
// Wraps a PSADT/package source folder into a `.intunewin` using the Microsoft
// 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.
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 }) {

View File

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

View File

@@ -1,6 +1,5 @@
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { db, id, now } from '../db.js';
import { decryptSecret } from './cryptoStore.js';
@@ -9,6 +8,7 @@ import { logger } from './logger.js';
import { listExecutableAssets } from '../models/assetModel.js';
import { listExecutableCustomVariables } from '../models/variableModel.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
// 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';
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', () => {
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', () => {
assert.ok(expectedOutputFile('/out', 'ContosoVpnSetup.exe').endsWith('ContosoVpnSetup.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', () => {

View File

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