Initial
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'node:path';
|
||||
import { path } from './utils/pathUtils.js';
|
||||
|
||||
dotenv.config({ quiet: true });
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
40
server/utils/pathUtils.js
Normal 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));
|
||||
}
|
||||
Reference in New Issue
Block a user