fetch-cwe-list
Version:
A simple Node.js module that fetches and parses the latest Common Weakness Enumeration (CWE) list
164 lines (153 loc) • 5.46 kB
JavaScript
const https = require('https')
const unzipper = require('unzipper')
const fs = require('fs')
const { XMLParser } = require('fast-xml-parser')
const path = require('path')
let externalReferenceAry = []
const options = {
ignoreAttributes: false,
attributeNamePrefix: '',
trimValues: true,
parseAttributeValue: true
}
// Download the CWE zip file and save it locally
async function downloadCweZip (url, destPath) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(destPath)
const request = https.get(url, (response) => {
if (response.statusCode === 404) {
file.close()
fs.unlink(destPath, () => {})
return reject(new Error(`CWE version not found at ${url} (status: 404)`))
}
if (response.statusCode < 200 || response.statusCode >= 300) {
file.close()
fs.unlink(destPath, () => {})
return reject(new Error(`Failed to download file: ${url} (status: ${response.statusCode})`))
}
response.pipe(file)
file.on('finish', () => {
file.close(resolve)
})
})
request.on('error', (err) => {
handleFileError(file, destPath, reject, err)
})
})
}
function handleFileError(file, destPath, reject, error) {
file.close()
fs.unlink(destPath, () => {})
reject(error)
}
// Extract all XML files from the zip and return their buffers
async function extractXmlBuffersFromZip (zipPath) {
const xmlBuffers = []
await new Promise((resolve, reject) => {
fs.createReadStream(zipPath)
.pipe(unzipper.Parse())
.on('entry', (entry) => {
const fileName = entry.path
if (fileName.endsWith('.xml')) {
const chunks = []
entry.on('data', (chunk) => chunks.push(chunk))
entry.on('end', () => xmlBuffers.push(Buffer.concat(chunks)))
entry.on('error', reject)
} else {
entry.autodrain()
}
})
.on('close', resolve)
.on('error', reject)
})
return xmlBuffers
}
// Parse XML buffer to JSON using fast-xml-parser
function parseXmlBufferToJson (xmlBuffer, options) {
const xmlPreview = xmlBuffer.toString('utf8', 0, 200)
if (!xmlPreview.trim().startsWith('<?xml')) {
throw new Error('Extracted file does not appear to be valid XML.')
}
const parser = new XMLParser(options)
return parser.parse(xmlBuffer.toString('utf8'))
}
// Clean up temporary files
function cleanupFile (filePath) {
try {
if (fs.existsSync(filePath)) fs.unlinkSync(filePath)
} catch (cleanupErr) {
console.warn('Warning: Could not delete temporary files:', cleanupErr)
}
}
// Helper to construct the correct URL and zip path for a given version
function getCweZipUrlAndPath (version) {
let url, zipPath
if (!version || version === 'latest') {
url = 'https://cwe.mitre.org/data/xml/cwec_latest.xml.zip'
zipPath = path.join(__dirname, 'output', 'cwec_latest.xml.zip')
} else {
// Validate version string (e.g., "4.16")
// Only allow digits and dots, and must match the MITRE version format
if (!/^\d+(\.\d+)*$/.test(version)) {
throw new Error('Invalid version format. Use, for example, "4.16" or "latest".')
}
// Prevent path traversal by rejecting any version with '..' or '/'
if (version.includes('..') || version.includes('/') || version.includes('\\')) {
throw new Error('Invalid version: path traversal is not allowed.')
}
url = `https://cwe.mitre.org/data/xml/cwec_v${version}.xml.zip`
zipPath = path.join(__dirname, 'output', `cwec_v${version}.xml.zip`)
}
return { url, zipPath }
}
// Fetch and parse CWE catalog for a given version (or latest)
async function fetchCwec (version) {
const { url, zipPath } = getCweZipUrlAndPath(version)
try {
await downloadCweZip(url, zipPath)
const xmlBuffers = await extractXmlBuffersFromZip(zipPath)
if (xmlBuffers.length === 0) throw new Error('No XML files found in the zip.')
const data = Buffer.concat(xmlBuffers)
const cweParsed = parseXmlBufferToJson(data, options)
const cweWeaknessAry = cweParsed.Weakness_Catalog.Weaknesses.Weakness.map((x) => x)
externalReferenceAry = cweParsed.Weakness_Catalog.External_References.External_Reference
cleanupFile(zipPath)
return cweWeaknessAry
} catch (error) {
cleanupFile(zipPath)
throw error
}
}
const getExternalReferencesByCwe = (cwe) => {
if (Array.isArray(cwe.References?.Reference)) {
cwe.References.Full_Details = []
for (const externalReferenceId of cwe.References.Reference) {
const fullReferenceDetails = externalReferenceAry.find(
(reference) => externalReferenceId.External_Reference_ID === reference.Reference_ID
)
cwe.References.Full_Details.push(fullReferenceDetails)
}
}
}
// Main API: fetchCweList([version])
const fetchCweList = async (version) => {
const cweWeaknessAry = await fetchCwec(version)
for (const cwe of cweWeaknessAry) {
if (cwe.References) getExternalReferencesByCwe(cwe)
}
return cweWeaknessAry
}
module.exports = fetchCweList
// CLI usage
if (require.main === module) {
// Accept version as optional CLI argument
const version = process.argv[2] || 'latest'
fetchCweList(version).then((list) => {
console.log(`Fetched ${list.length} CWE entries.`)
// Optionally print a sample entry
console.log(JSON.stringify(list[0], null, 2))
}).catch((err) => {
console.error(err)
process.exit(1)
})
}