#!/usr/bin/env node /*! * @license * Copyright © 2005-2026 Hyland Software, Inc. and its affiliates. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { spawnSync } from 'node:child_process'; import * as path from 'path'; import * as fs from 'fs'; import { argv, exit } from 'node:process'; import { parseArgs } from 'node:util'; import { escapeHtml } from './utils'; interface AuditCommandArgs { package?: string; outDir?: string; } /** * Render the audit report as a Markdown page. * * @param jsonAudit parsed npm audit JSON output * @param projName project name * @param projVersion project version * @returns rendered Markdown string */ function renderAuditPage(jsonAudit: any, projName: string, projVersion: string): string { const rows: string[] = []; if (jsonAudit.auditReportVersion >= 2) { for (const key in jsonAudit.vulnerabilities) { const v = jsonAudit.vulnerabilities[key]; rows.push(`|${escapeHtml(v.severity)} | ${escapeHtml(v.name)} | ${JSON.stringify(v.range)} |`); } } else { for (const key in jsonAudit.advisories) { const a = jsonAudit.advisories[key]; rows.push(`|${escapeHtml(a.severity)} | ${escapeHtml(a.module_name)} | ${JSON.stringify(a.vulnerable_versions)} |`); } } return `--- Title: Audit info, ${escapeHtml(projName)} ${escapeHtml(projVersion)} --- # Audit information for ${escapeHtml(projName)} ${escapeHtml(projVersion)} This page lists the security audit of the dependencies this project depends on. ## Risks - Critical risk: ${jsonAudit.metadata.vulnerabilities.critical} - High risk: ${jsonAudit.metadata.vulnerabilities.high} - Moderate risk: ${jsonAudit.metadata.vulnerabilities.moderate} - Low risk: ${jsonAudit.metadata.vulnerabilities.low} Dependencies analyzed: ${jsonAudit.metadata.totalDependencies} ## Libraries | Severity | Module | Vulnerable versions | | --- | --- | --- | ${rows.join('\n')} `; } /** * Audit report command * * @param _args (unused) * @param workingDir working directory * @returns void */ export default function main(_args: string[], workingDir: string) { if (argv.includes('-h') || argv.includes('--help')) { console.log(` Usage: audit [options] Generate an audit report Options: -p, --package Path to package file (default: package.json in working directory) -d, --outDir Output directory (default: working directory) -h, --help Display help for command `); exit(0); } const { values } = parseArgs({ args: argv.slice(2), options: { package: { type: 'string', short: 'p' }, outDir: { type: 'string', short: 'd' } }, allowPositionals: true }); const options: AuditCommandArgs = { package: values.package as string | undefined, outDir: values.outDir as string | undefined }; let packagePath = path.resolve(workingDir, 'package.json'); if (options.package) { packagePath = path.resolve(options.package); } if (!fs.existsSync(packagePath)) { console.error('The package.json file was not found'); exit(1); } return new Promise((resolve, reject) => { console.log(`Running audit on ${packagePath}`); const packageJson = JSON.parse(fs.readFileSync(packagePath).toString()); // Run in the directory containing the package.json const packageDir = path.dirname(packagePath); // Use spawnSync with array arguments for safer command execution (prevents shell injection) // Cross-platform: pnpm is available on PATH on all platforms (Windows, macOS, Linux) const result = spawnSync('pnpm', ['audit', '--json', '--prod'], { cwd: packageDir, encoding: 'utf-8', // shell: false is the default and more secure (no shell interpretation) shell: false, // Set maxBuffer to handle large audit outputs maxBuffer: 10 * 1024 * 1024 // 10MB }); let jsonAudit; // npm audit returns non-zero exit code when vulnerabilities are found // We still want to parse the JSON output in this case if (result.error) { console.error('Failed to run npm audit:', result.error.message); reject(result.error); return; } const auditOutput = result.stdout; if (!auditOutput) { console.error('npm audit produced no output'); reject(new Error('npm audit produced no output')); return; } try { jsonAudit = JSON.parse(auditOutput); } catch (parseError) { console.error('Failed to parse npm audit output'); reject(parseError); return; } const mdText = renderAuditPage(jsonAudit, packageJson.name, packageJson.version); const outputPath = path.resolve(options.outDir || workingDir); const outputFile = path.join(outputPath, `audit-info-${packageJson.version}.md`); fs.writeFileSync(outputFile, mdText); console.log(`Report saved as ${outputFile}`); resolve(0); }); }