Initial
This commit is contained in:
35
package-lock.json
generated
35
package-lock.json
generated
@@ -20,6 +20,7 @@
|
||||
"monaco-editor": "^0.53.0",
|
||||
"multer": "^2.2.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"path": "^0.12.7",
|
||||
"tailwindcss": "^4.3.1",
|
||||
"vite": "^8.1.0",
|
||||
"vue": "^3.5.26",
|
||||
@@ -2584,6 +2585,16 @@
|
||||
"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": {
|
||||
"version": "8.4.2",
|
||||
"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_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": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -3319,12 +3339,27 @@
|
||||
"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": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"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": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"monaco-editor": "^0.53.0",
|
||||
"multer": "^2.2.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"path": "^0.12.7",
|
||||
"tailwindcss": "^4.3.1",
|
||||
"vite": "^8.1.0",
|
||||
"vue": "^3.5.26",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'path';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
|
||||
if (fs.existsSync('dist/index.html')) {
|
||||
if (fs.existsSync(path.join('dist', 'index.html'))) {
|
||||
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);
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import path from 'path';
|
||||
|
||||
const mode = process.argv[2] || 'development';
|
||||
const watch = process.argv.includes('--watch');
|
||||
const args = watch
|
||||
? ['node_modules/nodemon/bin/nodemon.js', 'server/index.js']
|
||||
: ['server/index.js'];
|
||||
? [path.join('node_modules', 'nodemon', 'bin', 'nodemon.js'), path.join('server', 'index.js')]
|
||||
: [path.join('server', 'index.js')];
|
||||
|
||||
const child = spawn(process.execPath, args, {
|
||||
stdio: 'inherit',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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',
|
||||
env: {
|
||||
...process.env,
|
||||
|
||||
@@ -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