Migrate from xlsx to exceljs to fix security vulnerabilities
- Replace xlsx package (v0.18.5) with exceljs (v4.4.0) - Remove @types/xlsx dependency (exceljs has built-in TypeScript types) - Update biaMatchingService.ts to use ExcelJS API: - Replace XLSX.read() with workbook.xlsx.load() - Replace XLSX.utils.sheet_to_json() with eachRow() iteration - Handle 1-based column indexing correctly - Make loadBIAData() and findBIAMatch() async functions - Update all callers in applications.ts and claude.ts to use await - Fix npm audit: 0 vulnerabilities (was 1 high severity) This migration eliminates the Prototype Pollution and ReDoS vulnerabilities in the xlsx package while maintaining full functionality.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "zira-backend",
|
||||
"name": "cmdb-insight-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "ZiRA Classificatie Tool Backend",
|
||||
"description": "CMDB Insight Backend",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
@@ -29,7 +29,7 @@
|
||||
"openai": "^6.15.0",
|
||||
"pg": "^8.13.1",
|
||||
"winston": "^3.17.0",
|
||||
"xlsx": "^0.18.5"
|
||||
"exceljs": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.12",
|
||||
@@ -38,7 +38,6 @@
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^22.9.0",
|
||||
"@types/pg": "^8.11.10",
|
||||
"@types/xlsx": "^0.0.35",
|
||||
"tsx": "^4.19.2",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ router.get('/bia-test', async (req: Request, res: Response) => {
|
||||
if (getQueryString(req, 'clear') === 'true') {
|
||||
clearBIACache();
|
||||
}
|
||||
const biaData = loadBIAData();
|
||||
const biaData = await loadBIAData();
|
||||
res.json({
|
||||
recordCount: biaData.length,
|
||||
records: biaData.slice(0, 20), // First 20 records
|
||||
@@ -119,7 +119,7 @@ router.get('/bia-test', async (req: Request, res: Response) => {
|
||||
router.get('/bia-debug', async (req: Request, res: Response) => {
|
||||
try {
|
||||
clearBIACache();
|
||||
const biaData = loadBIAData();
|
||||
const biaData = await loadBIAData();
|
||||
|
||||
// Get a few sample applications
|
||||
const searchResult = await dataService.searchApplications({}, 1, 50);
|
||||
@@ -138,7 +138,7 @@ router.get('/bia-debug', async (req: Request, res: Response) => {
|
||||
|
||||
// Test each sample app
|
||||
for (const app of [...sampleApps, ...testApps]) {
|
||||
const matchResult = findBIAMatch(app.name, app.searchReference ?? null);
|
||||
const matchResult = await findBIAMatch(app.name, app.searchReference ?? null);
|
||||
|
||||
// Find all potential matches in Excel data for detailed analysis
|
||||
const normalizedAppName = app.name.toLowerCase().trim();
|
||||
@@ -207,7 +207,7 @@ router.get('/bia-comparison', async (req: Request, res: Response) => {
|
||||
clearBIACache();
|
||||
|
||||
// Load fresh data
|
||||
const testBIAData = loadBIAData();
|
||||
const testBIAData = await loadBIAData();
|
||||
logger.info(`BIA comparison: Loaded ${testBIAData.length} records from Excel file`);
|
||||
if (testBIAData.length === 0) {
|
||||
logger.error('BIA comparison: No Excel data loaded - check if BIA.xlsx exists and is readable');
|
||||
@@ -251,7 +251,7 @@ router.get('/bia-comparison', async (req: Request, res: Response) => {
|
||||
|
||||
for (const app of applications) {
|
||||
// Find BIA match in Excel
|
||||
const matchResult = findBIAMatch(app.name, app.searchReference ?? null);
|
||||
const matchResult = await findBIAMatch(app.name, app.searchReference ?? null);
|
||||
|
||||
// Log first few matches for debugging
|
||||
if (comparisonItems.length < 5) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { readFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import * as XLSX from 'xlsx';
|
||||
import ExcelJS from 'exceljs';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
// Get __dirname equivalent for ES modules
|
||||
@@ -52,13 +52,13 @@ export function clearBIACache(): void {
|
||||
/**
|
||||
* Load BIA data from Excel file
|
||||
*/
|
||||
export function loadBIAData(): BIARecord[] {
|
||||
export async function loadBIAData(): Promise<BIARecord[]> {
|
||||
const now = Date.now();
|
||||
// Return cached data if still valid AND has records
|
||||
// Don't use cache if it's empty (indicates previous load failure)
|
||||
if (biaDataCache && biaDataCache.length > 0 && (now - biaDataCacheTimestamp) < BIA_CACHE_TTL) {
|
||||
logger.debug(`Using cached BIA data (${biaDataCache.length} records, cached ${Math.round((now - biaDataCacheTimestamp) / 1000)}s ago)`);
|
||||
return biaDataCache;
|
||||
return Promise.resolve(biaDataCache);
|
||||
}
|
||||
|
||||
// Clear cache if it's empty or expired
|
||||
@@ -96,19 +96,45 @@ export function loadBIAData(): BIARecord[] {
|
||||
logger.error(`__dirname: ${__dirname}`);
|
||||
biaDataCache = [];
|
||||
biaDataCacheTimestamp = now;
|
||||
return [];
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
logger.info(`Loading BIA data from: ${biaFilePath}`);
|
||||
|
||||
try {
|
||||
// Read file using readFileSync and then parse with XLSX.read
|
||||
// This works better in ES modules than XLSX.readFile
|
||||
// Read file using readFileSync and then parse with ExcelJS
|
||||
const fileBuffer = readFileSync(biaFilePath);
|
||||
const workbook = XLSX.read(fileBuffer, { type: 'buffer' });
|
||||
const sheetName = workbook.SheetNames[0];
|
||||
const worksheet = workbook.Sheets[sheetName];
|
||||
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 }) as any[][];
|
||||
const workbook = new ExcelJS.Workbook();
|
||||
// ExcelJS accepts Buffer, but TypeScript types may be strict - use type assertion
|
||||
await workbook.xlsx.load(fileBuffer as Buffer);
|
||||
const worksheet = workbook.worksheets[0]; // First sheet
|
||||
|
||||
// Converteer naar 2D array formaat (zoals xlsx.utils.sheet_to_json met header: 1)
|
||||
// We need at least column K (index 10), so ensure we read up to column 11 (1-based)
|
||||
const data: any[][] = [];
|
||||
const maxColumnNeeded = 11; // Column K is index 10 (0-based), so we need column 11 (1-based)
|
||||
|
||||
worksheet.eachRow((row, rowNumber) => {
|
||||
const rowData: any[] = [];
|
||||
// Ensure we have at least maxColumnNeeded columns, but also check actual cells
|
||||
const actualMaxCol = Math.max(maxColumnNeeded, row.actualCellCount || 0);
|
||||
for (let colNumber = 1; colNumber <= actualMaxCol; colNumber++) {
|
||||
const cell = row.getCell(colNumber);
|
||||
// ExcelJS uses 1-based indexing, convert to 0-based for array
|
||||
// Handle different cell value types: convert to string for consistency
|
||||
let cellValue: any = cell.value;
|
||||
if (cellValue === null || cellValue === undefined) {
|
||||
cellValue = '';
|
||||
} else if (cellValue instanceof Date) {
|
||||
cellValue = cellValue.toISOString();
|
||||
} else if (typeof cellValue === 'object' && 'richText' in cellValue) {
|
||||
// Handle RichText objects
|
||||
cellValue = cell.value?.toString() || '';
|
||||
}
|
||||
rowData[colNumber - 1] = cellValue;
|
||||
}
|
||||
data.push(rowData);
|
||||
});
|
||||
|
||||
logger.info(`Loaded Excel file: ${data.length} rows, first row has ${data[0]?.length || 0} columns`);
|
||||
if (data.length > 0 && data[0]) {
|
||||
@@ -236,12 +262,12 @@ export function loadBIAData(): BIARecord[] {
|
||||
}
|
||||
biaDataCache = records;
|
||||
biaDataCacheTimestamp = now;
|
||||
return records;
|
||||
return Promise.resolve(records);
|
||||
} catch (error) {
|
||||
logger.error('Failed to load BIA data from Excel', error);
|
||||
biaDataCache = [];
|
||||
biaDataCacheTimestamp = now;
|
||||
return [];
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,11 +356,11 @@ function wordBasedSimilarity(str1: string, str2: string): number {
|
||||
* - Confidence/similarity score
|
||||
* - Length similarity (prefer matches with similar length)
|
||||
*/
|
||||
export function findBIAMatch(
|
||||
export async function findBIAMatch(
|
||||
applicationName: string,
|
||||
searchReference: string | null
|
||||
): BIAMatchResult {
|
||||
const biaData = loadBIAData();
|
||||
): Promise<BIAMatchResult> {
|
||||
const biaData = await loadBIAData();
|
||||
if (biaData.length === 0) {
|
||||
logger.warn(`No BIA data available for lookup of "${applicationName}" (biaData.length = 0)`);
|
||||
return {
|
||||
|
||||
@@ -52,7 +52,7 @@ try {
|
||||
async function findBIAValue(applicationName: string, searchReference?: string | null): Promise<string | null> {
|
||||
// Use the unified matching service (imported at top of file)
|
||||
const { findBIAMatch } = await import('./biaMatchingService.js');
|
||||
const matchResult = findBIAMatch(applicationName, searchReference || null);
|
||||
const matchResult = await findBIAMatch(applicationName, searchReference || null);
|
||||
return matchResult.biaValue || null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user