├── .gitignore ├── README.md ├── build.js ├── esm_to_common.mjs ├── lib ├── README.md ├── cvss.cjs ├── helpers.cjs ├── index.cjs └── logger.cjs ├── package.json ├── src ├── README.md ├── cvss.js ├── helpers.js ├── index.js └── logger.js ├── test ├── test_common.cjs └── test_esm.mjs └── update.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CVEAggregate 2 | Build a CVE library with aggregated CISA, EPSS and CVSS data 3 | 4 | - **CISA Values** : The remediation due date (or null) 5 | - **EPSS Values** : The EPSS probability score (or 0) 6 | - **CVSS Values** : V2 and/or V3 vector strings (or null) 7 | 8 | ```js 9 | /** CommonJS 10 | * When requiring, reference the .cjs files in the /lib folder 11 | */ 12 | const { CVEAggregate } = require("/path/to/CVEAggregate/lib/index.cjs") 13 | 14 | /** ESM 15 | * When importing, reference the .js files in the /src folder 16 | */ 17 | import { CVEAggregate } from "/path/to/CVEAggregate/src/index.js" 18 | 19 | // or 20 | 21 | const { CVEAggregate } = await import("/path/to/CVEAggregate/src/index.js") 22 | ``` 23 | 24 | ```js 25 | /* If verbose, will log stuff to console */ 26 | const verbose = true 27 | const cves = new CVEAggregate('/path/to/cves.json', verbose) 28 | ``` 29 | 30 | ## Building the aggregate 31 | 32 | ```sh 33 | # Quick-build/update full list (with all CVEs) 34 | $ node /path/to/CVEAggregate/build.js /path/to/cves.json 35 | 36 | # Quick-build/update short list (ignoring inactive CVEs) 37 | $ node /path/to/CVEAggregate/update.js /path/to/cves.json 38 | ``` 39 | 40 | The path provided to the constructor will load file if exists and will save updates to same location. 41 | 42 | The build process will collect all existing CVE Ids regardless of their state or age. 43 | 44 | The update process will collect only the CVE Ids that have associated aggregate data (epps, cvss, cisa). 45 | 46 | Note: *Once the initial aggregate has been created, subsequent build or update calls will only collect new items since last save.* 47 | 48 | ```js 49 | const saveAtEachStage = true 50 | 51 | /* Build/update full list */ 52 | await cves.build(saveAtEachStage) 53 | 54 | /* Build/update short list */ 55 | await cves.update(saveAtEachStage) 56 | 57 | /* List new items since last load, plus aggregate totals and details */ 58 | cves.report() 59 | 60 | /* Return the full json aggregate */ 61 | const data = cves.dump() 62 | 63 | /* Save the current cache (to the filepath provided) */ 64 | cves.save() 65 | 66 | /* Load the cache from file (from the filepath provided) */ 67 | cves.load() 68 | 69 | /* Force rebuild full list */ 70 | await cves.forceBuild(saveAtEachStage) 71 | 72 | /* Force rebuild short list */ 73 | await cves.forceUpdate(saveAtEachStage) 74 | ``` 75 | 76 | ## Accessing the aggregate 77 | 78 | Helper functions are provided to help access and reference the aggregate 79 | 80 | ```js 81 | const listOfCves = ['CVE-2023-35390','CVE-2023-35391','CVE-2023-38180'] 82 | 83 | /* Get matching cve entries as an object */ 84 | const map = cves.map(...listOfCves) 85 | //> { 86 | //> 'CVE-2023-35390': { 87 | //> days: 0, 88 | //> cisa: null, 89 | //> epss: 0.00564, 90 | //> cvss2: null, 91 | //> cvss3: 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H' 92 | //> }, 93 | //> 'CVE-2023-35391': { 94 | //> days: 0, 95 | //> cisa: null, 96 | //> epss: 0.00114, 97 | //> cvss2: null, 98 | //> cvss3: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N' 99 | //> }, 100 | //> 'CVE-2023-38180': { 101 | //> days: 21, 102 | //> cisa: '2023-08-30', 103 | //> epss: 0.00484, 104 | //> cvss2: null, 105 | //> cvss3: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H' 106 | //> } 107 | //> } 108 | 109 | /* Get matching cve entries as an array */ 110 | const list = cves.list(...listOfCves) 111 | //> [ 112 | //> { 113 | //> id: 'CVE-2023-35390', 114 | //> days: 0, 115 | //> cisa: null, 116 | //> epss: 0.00564, 117 | //> cvss2: null, 118 | //> cvss3: 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H' 119 | //> }, 120 | //> { 121 | //> id: 'CVE-2023-35391', 122 | //> days: 0, 123 | //> cisa: null, 124 | //> epss: 0.00114, 125 | //> cvss2: null, 126 | //> cvss3: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N' 127 | //> }, 128 | //> { 129 | //> id: 'CVE-2023-38180', 130 | //> days: 21, 131 | //> cisa: '2023-08-30', 132 | //> epss: 0.00484, 133 | //> cvss2: null, 134 | //> cvss3: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H' 135 | //> } 136 | //> ] 137 | 138 | /* Get the whole list of CVE IDs in the cache */ 139 | const allCVEs = cves.cveList() 140 | ``` 141 | 142 | Get a value reduced/scaled across one or more CVE Ids 143 | 144 | ```js 145 | /* Check one or more CVE Ids if (any) in the CISA KEV */ 146 | const inKEV = cves.getCISA(...listOfCves) 147 | //> true 148 | 149 | /* Get the scaled EPSS score for one or more CVE Ids */ 150 | const epssScore = cves.getEPSS(...listOfCves) 151 | //> 0.011580786319263958 152 | 153 | /* Get the maximum CVSS score across one or more CVE Ids */ 154 | const cvssScore = cves.getCVSS(...listOfCves) 155 | //> 7.8 156 | ``` 157 | 158 | Get the full mapping of CVE Ids -to- values 159 | 160 | ```js 161 | const cisaMap = cves.mapCISA(...listOfCves) 162 | //> { 163 | //> 'CVE-2023-35390': null, 164 | //> 'CVE-2023-35391': null, 165 | //> 'CVE-2023-38180': '2023-08-30' 166 | //> } 167 | 168 | const epssMap = cves.mapEPSS(...listOfCves) 169 | //> { 170 | //> 'CVE-2023-35390': 0.00564, 171 | //> 'CVE-2023-35391': 0.00114, 172 | //> 'CVE-2023-38180': 0.00484 173 | //> } 174 | 175 | const cvssMap = cves.mapCVSS(...listOfCves) 176 | //> { 177 | //> 'CVE-2023-35390': 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H', 178 | //> 'CVE-2023-35391': 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N', 179 | //> 'CVE-2023-38180': 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H' 180 | //> } 181 | ``` 182 | 183 | Search the aggregate with criteria (gt, gte, lt, lte, eq, ne) 184 | 185 | ```js 186 | const critical = cves.search({ 187 | epss:{ gt:0.7 }, 188 | cvss:{ gt:9.0 }, 189 | cisa:{ gte:'2023-09-01' } 190 | }) 191 | //> { 192 | //> 'CVE-2023-24489': { daysUntilDue: -14, cisa: '2023-09-06', epss: 0.97441, cvss: 9.8 }, 193 | //> 'CVE-2023-38035': { daysUntilDue: -8, cisa: '2023-09-12', epss: 0.96013, cvss: 9.8 }, 194 | //> 'CVE-2023-33246': { daysUntilDue: 7, cisa: '2023-09-27', epss: 0.97146, cvss: 9.8 }, 195 | //> 'CVE-2021-3129': { daysUntilDue: 19, cisa: '2023-10-09', epss: 0.97515, cvss: 9.8 } 196 | //> } 197 | ``` 198 | 199 | 200 | ## Calculations 201 | 202 | The aggregate uses CVSS vectors and calculates the CVSS scores as needed 203 | 204 | This allows the ability to manipulate vectors with optional temporal and environmental metrics 205 | 206 | ```js 207 | //Calculate a CVSSv2 vector details 208 | const cvss2 = cves.calculateCVSS("AV:N/AC:L/Au:N/C:C/I:C/A:C") 209 | //> { 210 | //> baseMetricScore: 7.2, 211 | //> baseSeverity: 'High', 212 | //> baseImpact: 10.00084536, 213 | //> baseExploitability: 4.1086848, 214 | //> temporalMetricScore: 7.2, 215 | //> temporalSeverity: 'High', 216 | //> environmentalMetricScore: 7.2, 217 | //> environmentalSeverity: 'High', 218 | //> environmentalModifiedImpact: 10, 219 | //> metricValues: { AV:'N', AC:'L', Au:'N', C:'C', I:'C', A:'C' }, 220 | //> vectorString: 'AV:N/AC:L/Au:N/C:C/I:C/A:C/E:ND/RL:ND/RC:ND/CDP:ND/TD:ND/CR:ND/IR:ND/AR:ND', 221 | //> version: 'CVSS:2' 222 | //> } 223 | 224 | //Calculate a CVSSv3 vector details 225 | const cvss3 = cves.calculateCVSS("CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H") 226 | //> { 227 | //> baseMetricScore: 7.8, 228 | //> baseSeverity: 'High', 229 | //> baseImpact: 5.873118720000001, 230 | //> baseExploitability: 1.8345765900000002, 231 | //> temporalMetricScore: 7.8, 232 | //> temporalSeverity: 'High', 233 | //> environmentalMetricScore: 7.8, 234 | //> environmentalSeverity: 'High', 235 | //> environmentalModifiedImpact: 5.873118720000001, 236 | //> metricValues: { AV:'L', AC:'L', PR:'N', UI:'R', S:'U', C:'H', I:'H', A:'H' }, 237 | //> vectorString: 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H/E:X/RL:X/RC:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:X/MI:X/MA:X', 238 | //> version: 'CVSS:3.1' 239 | //> } 240 | ``` 241 | 242 | ## Aggregate structure 243 | 244 | Example of the aggregated cves.json 245 | 246 | ```json 247 | { 248 | "lastUpdated": "2023-08-31T14:41:33.076Z", 249 | "cvesUpdated": null, 250 | "cisaUpdated": "2023-08-31T14:35:31.532Z", 251 | "epssUpdated": "2023-08-31T14:41:33.076Z", 252 | "cvssUpdated": "2023-08-31T14:49:33.076Z", 253 | "lastCount": 216857, 254 | "cves": { 255 | "CVE-2018-4939": { 256 | "days": 181, 257 | "cisa": "2022-05-03", 258 | "epss": 0.97236, 259 | "cvss2": "AV:N/AC:L/Au:N/C:C/I:C/A:C", 260 | "cvss3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 261 | }, 262 | "CVE-2018-4878": { 263 | "days": 181, 264 | "cisa": "2022-05-03", 265 | "epss": 0.9742, 266 | "cvss2": "AV:N/AC:L/Au:N/C:P/I:P/A:P", 267 | "cvss3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 268 | }, 269 | ... 270 | } 271 | } 272 | ``` 273 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | import { join, dirname } from 'path' 2 | import { fileURLToPath } from 'url' 3 | 4 | const __dirname = dirname(fileURLToPath(import.meta.url)) 5 | const fileJson = process.argv[2] 6 | if( !fileJson?.length ) { 7 | console.error('Error: No export file specified\n') 8 | console.log(`Build a full CVE aggregate with:\n$ node ${join(__dirname, 'build.js')} /path/to/cves.json`) 9 | process.exit(-1) 10 | } 11 | 12 | const cveLibPath = join(__dirname, 'src', 'index.js') 13 | const { CVEAggregate } = await import(cveLibPath) 14 | const aggregate = new CVEAggregate(fileJson, true) 15 | 16 | await aggregate.build(true) 17 | await aggregate.logger.log("-".repeat(30)) 18 | await aggregate.report() 19 | -------------------------------------------------------------------------------- /esm_to_common.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert ESM files from /src into CommonJS files to /lib 3 | * Simple stream-pipe, by-line 4 | * - Changes /src references to /lib 5 | * - Changes .js references to .cjs 6 | * - Changes import to require 7 | * - Changes export to module.exports 8 | */ 9 | 10 | import { createReadStream, createWriteStream, readdirSync, existsSync, mkdirSync } from 'fs' 11 | import { fileURLToPath } from 'url' 12 | import { join, dirname } from 'path' 13 | import readline from 'node:readline' 14 | import events from 'node:events' 15 | 16 | const libraryFolder = 'lib' 17 | const sourceFolder = 'src' 18 | 19 | const __dirname = dirname(fileURLToPath(import.meta.url)) 20 | const src = join(__dirname, sourceFolder) 21 | const files = readdirSync(src) 22 | 23 | const lib = join(__dirname, libraryFolder) 24 | if( !existsSync(lib) ) mkdirSync(lib) 25 | 26 | for(const file of files) { 27 | if( !file.match(/[\.js|\.md]$/) ) continue 28 | const type = file.slice(-2) 29 | try { 30 | const srcpath = join(src, file) 31 | const rl = readline.createInterface({ input: createReadStream(srcpath), crlfDelay: Infinity }) 32 | 33 | const outfile = type === 'js' ? file.replace(/(\.js)$/, '.cjs') : file 34 | const libpath = join(lib, outfile) 35 | const wl = createWriteStream(libpath) 36 | 37 | const exports = [] 38 | rl.on('line', (line) => { 39 | if( line.startsWith('const __dirname') ) { 40 | return //ignore this line 41 | } 42 | 43 | line = line.replace('await import','require') 44 | line = line.replace(/(\.js)\b/,'.cjs') 45 | line = line.replace('/src','/lib') 46 | 47 | if( line.startsWith('export') ) { 48 | line = line.slice(7) 49 | exports.push(line.match(/\w+\s{1,}([\w|\d]*)/)?.[1]) 50 | } else if( line.startsWith('import') ) { 51 | line = line.replace('import', 'const') 52 | line = line.replace('from', '=') 53 | line = line.replace(/\'/g,'"') 54 | const eqInx = line.lastIndexOf('=') 55 | const path = JSON.parse(line.slice(eqInx+1).trim()) 56 | line = line.slice(0, eqInx+2) + `require('${path}')` 57 | } 58 | 59 | wl.write(`${line}\n`) 60 | }) 61 | 62 | rl.on('close', () => { 63 | if( type === 'js' && exports.length ) { 64 | wl.write(`module.exports = {\n\t${exports.join(',\n\t')}\n}\n\n`) 65 | } 66 | wl.end() 67 | }) 68 | 69 | await events.once(rl, 'close') 70 | } catch(e) { 71 | console.error(e) 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | # CVEAggregate 2 | Build a CVE library with aggregated CISA, EPSS and CVSS data 3 | 4 | - **CISA Values** : The remediation due date (or null) 5 | - **EPSS Values** : The EPSS probability score (or 0) 6 | - **CVSS Values** : V2 and/or V3 vector strings (or null) 7 | 8 | ```js 9 | const { CVEAggregate } = require('/path/to/CVEAggregate/lib/index.cjs') 10 | 11 | /* If verbose, will log stuff to console */ 12 | const verbose = true 13 | const cves = new CVEAggregate('/path/to/cves.json', verbose) 14 | ``` 15 | 16 | ## Building the aggregate 17 | 18 | The path provided to the constructor will load file if exists and will save updates to same location. 19 | 20 | The build process will collect all existing CVE Ids regardless of their state or age. 21 | 22 | The update process will collect only the CVE Ids that have associated aggregate data (epps, cvss, cisa). 23 | 24 | Note: *Once the initial aggregate has been created, subsequent build or update calls will only collect new items since last save.* 25 | 26 | ```js 27 | /* Build full list */ 28 | await cves.build() 29 | 30 | /* Build short list */ 31 | await cves.update() 32 | 33 | /* List new items since last load, plus aggregate totals and details */ 34 | cves.report() 35 | 36 | /* Return the full json aggregate */ 37 | const data = cves.dump() 38 | 39 | /* Force save (to the filepath provided) */ 40 | cves.save() 41 | 42 | /* Force load (from the filepath provided) */ 43 | cves.load() 44 | ``` 45 | 46 | ## Accessing the aggregate 47 | 48 | Helper functions are provided to help access and reference the aggregate 49 | 50 | ```js 51 | const listOfCves = ['CVE-2023-35390','CVE-2023-35391','CVE-2023-38180'] 52 | 53 | /* Get matching cve entries as an object */ 54 | const map = cves.map(...listOfCves) 55 | //> { 56 | //> 'CVE-2023-35390': { 57 | //> days: 0, 58 | //> cisa: null, 59 | //> epss: 0.00564, 60 | //> cvss2: null, 61 | //> cvss3: 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H' 62 | //> }, 63 | //> 'CVE-2023-35391': { 64 | //> days: 0, 65 | //> cisa: null, 66 | //> epss: 0.00114, 67 | //> cvss2: null, 68 | //> cvss3: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N' 69 | //> }, 70 | //> 'CVE-2023-38180': { 71 | //> days: 21, 72 | //> cisa: '2023-08-30', 73 | //> epss: 0.00484, 74 | //> cvss2: null, 75 | //> cvss3: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H' 76 | //> } 77 | //> } 78 | 79 | /* Get matching cve entries as an array */ 80 | const list = cves.list(...listOfCves) 81 | //> [ 82 | //> { 83 | //> id: 'CVE-2023-35390', 84 | //> days: 0, 85 | //> cisa: null, 86 | //> epss: 0.00564, 87 | //> cvss2: null, 88 | //> cvss3: 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H' 89 | //> }, 90 | //> { 91 | //> id: 'CVE-2023-35391', 92 | //> days: 0, 93 | //> cisa: null, 94 | //> epss: 0.00114, 95 | //> cvss2: null, 96 | //> cvss3: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N' 97 | //> }, 98 | //> { 99 | //> id: 'CVE-2023-38180', 100 | //> days: 21, 101 | //> cisa: '2023-08-30', 102 | //> epss: 0.00484, 103 | //> cvss2: null, 104 | //> cvss3: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H' 105 | //> } 106 | //> ] 107 | 108 | /* Get the whole list of CVE IDs in the cache */ 109 | const allCVEs = cves.cveList() 110 | ``` 111 | 112 | Get a value reduced/scaled across one or more CVE Ids 113 | 114 | ```js 115 | /* Check one or more CVE Ids if (any) in the CISA KEV */ 116 | const inKEV = cves.getCISA(...listOfCves) 117 | //> true 118 | 119 | /* Get the scaled EPSS score for one or more CVE Ids */ 120 | const epssScore = cves.getEPSS(...listOfCves) 121 | //> 0.011580786319263958 122 | 123 | /* Get the maximum CVSS score across one or more CVE Ids */ 124 | const cvssScore = cves.getCVSS(...listOfCves) 125 | //> 7.8 126 | ``` 127 | 128 | Get the full mapping of CVE Ids -to- values 129 | 130 | ```js 131 | const cisaMap = cves.mapCISA(...listOfCves) 132 | //> { 133 | //> 'CVE-2023-35390': null, 134 | //> 'CVE-2023-35391': null, 135 | //> 'CVE-2023-38180': '2023-08-30' 136 | //> } 137 | 138 | const epssMap = cves.mapEPSS(...listOfCves) 139 | //> { 140 | //> 'CVE-2023-35390': 0.00564, 141 | //> 'CVE-2023-35391': 0.00114, 142 | //> 'CVE-2023-38180': 0.00484 143 | //> } 144 | 145 | const cvssMap = cves.mapCVSS(...listOfCves) 146 | //> { 147 | //> 'CVE-2023-35390': 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H', 148 | //> 'CVE-2023-35391': 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N', 149 | //> 'CVE-2023-38180': 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H' 150 | //> } 151 | ``` 152 | 153 | Search the aggregate with criteria (gt, gte, lt, lte, eq, ne) 154 | 155 | ```js 156 | const critical = cves.search({ 157 | epss:{ gt:0.7 }, 158 | cvss:{ gt:9.0 }, 159 | cisa:{ gte:'2023-09-01' } 160 | }) 161 | //> { 162 | //> 'CVE-2023-24489': { daysUntilDue: -14, cisa: '2023-09-06', epss: 0.97441, cvss: 9.8 }, 163 | //> 'CVE-2023-38035': { daysUntilDue: -8, cisa: '2023-09-12', epss: 0.96013, cvss: 9.8 }, 164 | //> 'CVE-2023-33246': { daysUntilDue: 7, cisa: '2023-09-27', epss: 0.97146, cvss: 9.8 }, 165 | //> 'CVE-2021-3129': { daysUntilDue: 19, cisa: '2023-10-09', epss: 0.97515, cvss: 9.8 } 166 | //> } 167 | ``` 168 | 169 | 170 | ## Calculations 171 | 172 | The aggregate uses CVSS vectors and calculates the CVSS scores as needed 173 | 174 | This allows the ability to manipulate vectors with optional temporal and environmental metrics 175 | 176 | ```js 177 | //Calculate a CVSSv2 vector details 178 | const cvss2 = cves.calculateCVSS("AV:N/AC:L/Au:N/C:C/I:C/A:C") 179 | //> { 180 | //> baseMetricScore: 7.2, 181 | //> baseSeverity: 'High', 182 | //> baseImpact: 10.00084536, 183 | //> baseExploitability: 4.1086848, 184 | //> temporalMetricScore: 7.2, 185 | //> temporalSeverity: 'High', 186 | //> environmentalMetricScore: 7.2, 187 | //> environmentalSeverity: 'High', 188 | //> environmentalModifiedImpact: 10, 189 | //> metricValues: { AV:'N', AC:'L', Au:'N', C:'C', I:'C', A:'C' }, 190 | //> vectorString: 'AV:N/AC:L/Au:N/C:C/I:C/A:C/E:ND/RL:ND/RC:ND/CDP:ND/TD:ND/CR:ND/IR:ND/AR:ND', 191 | //> version: 'CVSS:2' 192 | //> } 193 | 194 | //Calculate a CVSSv3 vector details 195 | const cvss3 = cves.calculateCVSS("CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H") 196 | //> { 197 | //> baseMetricScore: 7.8, 198 | //> baseSeverity: 'High', 199 | //> baseImpact: 5.873118720000001, 200 | //> baseExploitability: 1.8345765900000002, 201 | //> temporalMetricScore: 7.8, 202 | //> temporalSeverity: 'High', 203 | //> environmentalMetricScore: 7.8, 204 | //> environmentalSeverity: 'High', 205 | //> environmentalModifiedImpact: 5.873118720000001, 206 | //> metricValues: { AV:'L', AC:'L', PR:'N', UI:'R', S:'U', C:'H', I:'H', A:'H' }, 207 | //> vectorString: 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H/E:X/RL:X/RC:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:X/MI:X/MA:X', 208 | //> version: 'CVSS:3.1' 209 | //> } 210 | ``` 211 | 212 | ## Aggregate structure 213 | 214 | Example of the aggregated cves.json 215 | 216 | ```json 217 | { 218 | "lastUpdated": "2023-08-31T14:41:33.076Z", 219 | "cvesUpdated": null, 220 | "cisaUpdated": "2023-08-31T14:35:31.532Z", 221 | "epssUpdated": "2023-08-31T14:41:33.076Z", 222 | "cvssUpdated": "2023-08-31T14:49:33.076Z", 223 | "lastCount": 216857, 224 | "cves": { 225 | "CVE-2018-4939": { 226 | "days": 181, 227 | "cisa": "2022-05-03", 228 | "epss": 0.97236, 229 | "cvss2": "AV:N/AC:L/Au:N/C:C/I:C/A:C", 230 | "cvss3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 231 | }, 232 | "CVE-2018-4878": { 233 | "days": 181, 234 | "cisa": "2022-05-03", 235 | "epss": 0.9742, 236 | "cvss2": "AV:N/AC:L/Au:N/C:P/I:P/A:P", 237 | "cvss3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 238 | }, 239 | ... 240 | } 241 | } 242 | ``` 243 | -------------------------------------------------------------------------------- /lib/cvss.cjs: -------------------------------------------------------------------------------- 1 | class CVSS2 { 2 | #vectorRegex = /((AV:[NAL]|AC:[LHM]|Au:[MSN]|[CIA]:[NPC]|E:[ND|U|POC|F|H]{1,3}|RL:[ND|OF|TF|W|U]{1,2}|RC:[ND|UC|UR|C]{1,2}|CDP:[ND|N|L|LM|MH|H]{1,2}|TD:[ND|N|L|M|H]{1,2}|CR:[ND|L|M|H]{1,2}|IR:[ND|L|M|H]{1,2}|AR:[ND|L|M|H]{1,2})\/)*(AV:[NAL]|AC:[LHM]|Au:[MSN]|[CIA]:[NPC]|E:[ND|U|POC|F|H]{1,3}|RL:[ND|OF|TF|W|U]{1,2}|RC:[ND|UC|UR|C]{1,2}|CDP:[ND|N|L|LM|MH|H]{1,2}|TD:[ND|N|L|M|H]{1,2}|CR:[ND|L|M|H]{1,2}|IR:[ND|L|M|H]{1,2}|AR:[ND|L|M|H]{1,2})/ 3 | #vectorPattern = /[A-Za-z]{1,3}:[A-Za-z]{1,3}/ig 4 | 5 | constructor(options={}) { 6 | this.version = options.version || "CVSS:2" 7 | this.exploitabilityCoefficient = options.exploitabilityCoefficient || 8.22 8 | this.baseKeys = options.baseKeys || ["AV","AC","Au","C","I","A"] 9 | this.temporalKeys = options.temporalKeys || ["E","RL","RC"] 10 | this.environmentKeys = options.environmentKeys || ["CDP","TD","CR","IR","AR"] 11 | this.weight = { 12 | AV: { L:0.395, A:0.646, N:1.0 }, 13 | AC: { H:0.35, M:0.61, L:0.71 }, 14 | Au: { M:0.45, S:0.56, N:0.704 }, 15 | C: { N:0, P:0.275, C:0.660 }, 16 | I: { N:0, P:0.275, C:0.660 }, 17 | A: { N:0, P:0.275, C:0.660 }, 18 | E: { ND:1, U:0.85, POC:0.9, F:0.95, H:1 }, 19 | RL: { ND:1, OF:0.97, TF:0.9, W:0.95, U:1 }, 20 | RC: { ND:1, UC:0.9, UR:0.95, C:1 }, 21 | CDP:{ ND:0, N:0, L:0.1, LM:0.3, MH:0.4, H:0.5 }, 22 | TD: { ND:1, N:0, L:0.25, M:0.75, H:1 }, 23 | CR: { ND:1, L:0.5, M:1, H:1.51 }, 24 | IR: { ND:1, L:0.5, M:1, H:1.51 }, 25 | AR: { ND:1, L:0.5, M:1, H:1.51 }, 26 | } 27 | this.severityRatings = options.severityRatings || [ 28 | { name: "None", bottom: 0.0, top: 0.0 }, 29 | { name: "Low", bottom: 0.1, top: 3.9 }, 30 | { name: "Medium", bottom: 4.0, top: 6.9 }, 31 | { name: "High", bottom: 7.0, top: 8.9 }, 32 | { name: "Critical", bottom: 9.0, top: 10.0 } 33 | ] 34 | 35 | this.metricKeys = this.baseKeys.concat( this.temporalKeys ).concat( this.environmentKeys ) 36 | this.metricNames = { 37 | AV: 'Attack Vector', 38 | AC: 'Attack Complexity', 39 | Au: 'Authentication', 40 | C: 'Confidentiality', 41 | I: 'Integrity', 42 | A: 'Availability', 43 | E: 'Exploitability', 44 | RL: 'Remediation Level', 45 | RC: 'Report Confidence', 46 | CDP: 'Collateral Damage Potential', 47 | TD: 'Target Distribution', 48 | CR: 'Confidentiality Requirement', 49 | IR: 'Integrity Requirement', 50 | AR: 'Availability Requirement', 51 | } 52 | this.metricValues = { 53 | AV: { N: "NETWORK", A: "ADJACENT_NETWORK", L: "LOCAL" }, 54 | AC: { H: "HIGH", M: "MEDIUM", L: "LOW" }, 55 | Au: { N: "NONE", S: "SINGLE", M: "MULTIPLE" }, 56 | C: { N: "NONE", P: "PARTIAL", C: "COMPLETE" }, 57 | I: { N: "NONE", P: "PARTIAL", C: "COMPLETE" }, 58 | A: { N: "NONE", P: "PARTIAL", C: "COMPLETE" }, 59 | E: { ND: "NOT_DEFINED", U: "UNPROVEN", POC: "PROOF_OF_CONCEPT", F: "FUNCTIONAL", H: "HIGH" }, 60 | RL: { ND: "NOT_DEFINED", OF: "OFFICIAL_FIX", TF: "TEMPORARY_FIX", W: "WORKAROUND", U: "UNAVAILABLE" }, 61 | RC: { ND: "NOT_DEFINED", UC: "UNCONFIRMED", UR: "UNCORROBORATED", C: "CONFIRMED" }, 62 | CDP: { ND: "NOT_DEFINED", N: "NONE", L: "LOW", LM: "LOW_MEDIUM", M: "MEDIUM", H: "HIGH" }, 63 | TD: { ND: "NOT_DEFINED", N: "NONE", L: "LOW", M: "MEDIUM", H: "HIGH" }, 64 | CR: { ND: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 65 | IR: { ND: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 66 | AR: { ND: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 67 | } 68 | } 69 | 70 | error(reason) { 71 | return { 72 | baseMetricScore: 0, 73 | baseSeverity: null, 74 | baseImpact: 0, 75 | baseExploitability: 0, 76 | temporalMetricScore: 0, 77 | temporalSeverity: null, 78 | environmentalMetricScore: 0, 79 | environmentalSeverity: null, 80 | environmentalModifiedImpact: 0, 81 | vectorValues:{}, 82 | vectorString:reason, 83 | version:this.version 84 | } 85 | } 86 | 87 | severityRating(score) { 88 | const severityRatingLength = this.severityRatings.length 89 | const validatedScore = Number(score) 90 | if( isNaN(validatedScore) ) 91 | return validatedScore 92 | 93 | for( let i = 0; i < severityRatingLength; i++ ) { 94 | if( score >= this.severityRatings[i].bottom && score <= this.severityRatings[i].top ) 95 | return this.severityRatings[i].name 96 | } 97 | 98 | return undefined 99 | } 100 | 101 | calculateFromMetrics(metricValues) { 102 | const value = (key) => this.weight[key][metricValues[key]||"ND"] 103 | 104 | const impact = 10.41 * (1 - (1 - value('C')) * (1 - value("I")) * (1 - value('A'))) 105 | const exploitability = this.exploitabilityCoefficient * value("AC") * value("Au") * value("AV") 106 | const baseScore = ((0.6 * impact) + (0.4 * exploitability) - 1.5) * (impact === 0 ? 0 : 1.176) 107 | const temporalScore = baseScore * value("E") * value("RL") * value("RC") 108 | const modifiedImpact = Math.min(10, 10.41 * (1 - (1 - value("C") * value("CR")) * (1 - value("I") * value("IR")) * (1 - value("A") * value("AR")))) 109 | const modifiedBase = ((0.6 * modifiedImpact) + (0.4 * exploitability) - 1.5) * (modifiedImpact === 0 ? 0 : 1.176) 110 | const modifiedTemporal = modifiedBase * value("E") * value("RL") * value("RC") 111 | const envScore = (modifiedTemporal + (10 - modifiedTemporal) * value("CDP")) * value("TD") 112 | 113 | const vectorString = this.baseKeys 114 | .concat( this.temporalKeys ) 115 | .concat( this.environmentKeys ) 116 | .map(key => `${key}:${metricValues[key]||"ND"}`) 117 | .join("/") 118 | 119 | return { 120 | baseMetricScore: Number(baseScore.toFixed(1)), 121 | baseSeverity: this.severityRating(baseScore.toFixed(1)), 122 | baseImpact: impact, 123 | baseExploitability: exploitability, 124 | temporalMetricScore: Number(temporalScore.toFixed(1)), 125 | temporalSeverity: this.severityRating(temporalScore.toFixed(1)), 126 | environmentalMetricScore: Number(envScore.toFixed(1)), 127 | environmentalSeverity: this.severityRating(envScore.toFixed(1)), 128 | environmentalModifiedImpact: modifiedImpact, 129 | metricValues, 130 | vectorString, 131 | version:this.version, 132 | adjust:(metrics={}) => this.calculateFromMetrics( Object.assign(metricValues, metrics) ) 133 | } 134 | } 135 | 136 | calculateFromVector(vectorString) { 137 | if( !this.#vectorRegex.test(vectorString) ) 138 | return this.error("Malformed V2 Vector String") 139 | 140 | const vectorMatches = vectorString.match(this.#vectorPattern) 141 | const metricValues = vectorMatches.reduce((acc,m) => { 142 | const [key, val] = m.split(':') 143 | if( key && val ) acc[key] = val 144 | return acc 145 | },{}) 146 | 147 | return this.calculateFromMetrics(metricValues) 148 | } 149 | 150 | describe(vectorOrMetrics) { 151 | return typeof vectorOrMetrics === 'string' 152 | ? this.describeVector(vectorOrMetrics) 153 | : this.describeMetrics(vectorOrMetrics) 154 | } 155 | 156 | describeVector(vectorString) { 157 | if( typeof vectorString !== 'string' || !vectorString?.length ) 158 | throw new Error('CVSS vector string required') 159 | 160 | if( !this.#vectorRegex.test(vectorString) ) 161 | return this.error("Malformed V2 Vector String") 162 | 163 | const vectorMatches = vectorString.match(this.#vectorPattern) 164 | const metricValues = vectorMatches.reduce((acc,m) => { 165 | const [key, val] = m.split(':') 166 | if( key && val ) acc[key] = val 167 | return acc 168 | },{}) 169 | 170 | return this.describeMetrics(metricValues) 171 | } 172 | 173 | describeMetrics(metricValues) { 174 | if( typeof metricValues !== 'object' ) 175 | throw new Error('Metrics object required') 176 | 177 | return this.metricKeys.reduce((acc,key) => { 178 | acc[this.metricNames[key]] = this.metricValues[key][metricValues[key]] 179 | return acc 180 | },{}) 181 | 182 | } 183 | } 184 | 185 | class CVSS3 { 186 | #vectorRegex = /CVSS:3(\.\d){0,1}\/((AV:[NALP]|AC:[LH]|PR:[UNLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XUNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])\/)*(AV:[NALP]|AC:[LH]|PR:[UNLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XUNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])/ 187 | 188 | constructor(options={}) { 189 | this.version = options.version || "CVSS:3.1" 190 | this.exploitabilityCoefficient = options.exploitabilityCoefficient || 8.22 191 | this.scopeCoefficient = options.scopeCoefficient || 1.08 192 | this.weight = options.weight || { 193 | AV: { N: 0.85, A: 0.62, L: 0.55, P: 0.2 }, 194 | AC: { H: 0.44, L: 0.77 }, 195 | PR: { 196 | U: { N: 0.85, L: 0.62, H: 0.27 }, 197 | C: { N: 0.85, L: 0.68, H: 0.5 } 198 | }, 199 | UI: { N: 0.85, R: 0.62 }, 200 | S: { U: 6.42, C: 7.52 }, 201 | CIA: { N: 0, L: 0.22, H: 0.56 }, 202 | E: { X: 1, U: 0.91, P: 0.94, F: 0.97, H: 1 }, 203 | RL: { X: 1, O: 0.95, T: 0.96, W: 0.97, U: 1 }, 204 | RC: { X: 1, U: 0.92, R: 0.96, C: 1 }, 205 | CIAR: { X: 1, L: 0.5, M: 1, H: 1.5 } 206 | } 207 | this.severityRatings = options.severityRatings || [ 208 | { name: "None", bottom: 0.0, top: 0.0 }, 209 | { name: "Low", bottom: 0.1, top: 3.9 }, 210 | { name: "Medium", bottom: 4.0, top: 6.9 }, 211 | { name: "High", bottom: 7.0, top: 8.9 }, 212 | { name: "Critical", bottom: 9.0, top: 10.0 } 213 | ] 214 | this.metricKeys = ['AV','AC','PR','UI','S','C','I','A','E','RL','RC','CR','IR','AR','MAV','MAC','MPR','MUI','MS','MC','MI','MA'] 215 | this.metricNames = { 216 | AV: 'Attack Vector', 217 | AC: 'Attack Complexity', 218 | PR: 'Privileges Required', 219 | UI: 'User Interaction', 220 | S: 'Scope', 221 | C: 'Confidentiality', 222 | I: 'Integrity', 223 | A: 'Availability', 224 | E: 'Exploit Code Maturity', 225 | RL: 'Remediation Level', 226 | RC: 'Report Confidence', 227 | CR: 'Confidentiality Requirement', 228 | IR: 'Integrity Requirement', 229 | AR: 'Availability Requirement', 230 | MAV: 'Modified Attack Vector', 231 | MAC: 'Modified Attack Complexity', 232 | MPR: 'Modified Privileges Required', 233 | MUI: 'Modified User Interaction', 234 | MS: 'Modified Scope', 235 | MC: 'Modified Confidentiality', 236 | MI: 'Modified Integrity', 237 | MA: 'Modified Availability' 238 | } 239 | this.metricValues = { 240 | E: { X: "NOT_DEFINED", U: "UNPROVEN", P: "PROOF_OF_CONCEPT", F: "FUNCTIONAL", H: "HIGH" }, 241 | RL: { X: "NOT_DEFINED", O: "OFFICIAL_FIX", T: "TEMPORARY_FIX", W: "WORKAROUND", U: "UNAVAILABLE" }, 242 | RC: { X: "NOT_DEFINED", U: "UNKNOWN", R: "REASONABLE", C: "CONFIRMED" }, 243 | CIAR: { X: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 244 | MAV: { X: "NOT_DEFINED", N: "NETWORK", A: "ADJACENT_NETWORK", L: "LOCAL", P: "PHYSICAL" }, 245 | MAC: { X: "NOT_DEFINED", H: "HIGH", L: "LOW" }, 246 | MPR: { X: "NOT_DEFINED", N: "NONE", L: "LOW", H: "HIGH" }, 247 | MUI: { X: "NOT_DEFINED", N: "NONE", R: "REQUIRED" }, 248 | MS: { X: "NOT_DEFINED", U: "UNCHANGED", C: "CHANGED" }, 249 | MCIA: { X: "NOT_DEFINED", N: "NONE", L: "LOW", H: "HIGH" }, 250 | /*duplicates*/ 251 | C: { X: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 252 | R: { X: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 253 | CR: { X: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 254 | IR: { X: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 255 | AR: { X: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 256 | AV: { N: "NETWORK", A: "ADJACENT_NETWORK", L: "LOCAL", P: "PHYSICAL" }, 257 | AC: { H: "HIGH", L: "LOW" }, 258 | PR: { N: "NONE", L: "LOW", H: "HIGH" }, 259 | UI: { N: "NONE", R: "REQUIRED" }, 260 | S: { U: "UNCHANGED", C: "CHANGED" }, 261 | I: { L: "LOW", M: "MEDIUM", H: "HIGH" }, 262 | A: { L: "LOW", M: "MEDIUM", H: "HIGH" }, 263 | MC: { X: "NOT_DEFINED", N: "NONE", L: "LOW", H: "HIGH" }, 264 | MI: { X: "NOT_DEFINED", N: "NONE", L: "LOW", H: "HIGH" }, 265 | MA: { X: "NOT_DEFINED", N: "NONE", L: "LOW", H: "HIGH" }, 266 | } 267 | 268 | } 269 | 270 | error(reason, version = this.version) { 271 | return { 272 | baseMetricScore: 0, 273 | baseSeverity: null, 274 | baseImpact: 0, 275 | baseExploitability: 0, 276 | temporalMetricScore: 0, 277 | temporalSeverity: null, 278 | environmentalMetricScore: 0, 279 | environmentalSeverity: null, 280 | environmentalModifiedImpact: 0, 281 | vectorValues:{}, 282 | vectorString:reason, 283 | version 284 | } 285 | } 286 | 287 | roundUp(input) { 288 | const int_input = Math.round(input * 100000) 289 | return int_input % 10000 === 0 290 | ? int_input / 100000 291 | : (Math.floor(int_input / 10000) + 1) / 10 292 | } 293 | 294 | severityRating(score) { 295 | const severityRatingLength = this.severityRatings.length 296 | const validatedScore = Number(score) 297 | if( isNaN(validatedScore) ) 298 | return validatedScore 299 | 300 | for( let i = 0; i < severityRatingLength; i++ ) { 301 | if( score >= this.severityRatings[i].bottom && score <= this.severityRatings[i].top ) 302 | return this.severityRatings[i].name 303 | } 304 | 305 | return undefined 306 | } 307 | 308 | calculateFromMetrics(metricValues, version = this.version) { 309 | const { 310 | AV = null, AC = null, PR = null, UI = null, S = null, C = null, I = null, A = null, 311 | E = 'X', RL = 'X', RC = 'X', CR = 'X',IR = 'X', AR = 'X', 312 | MAV = 'X', MAC = 'X', MPR = 'X', MUI = 'X',MS = 'X', MC = 'X', MI = 'X', MA = 'X' 313 | } = metricValues 314 | 315 | if( !AV || !AC || !PR || !UI || !S || !C || !I || !A ) 316 | return this.error("Malformed V3.x Metrics") 317 | 318 | const metricWeightAV = this.weight.AV[AV] 319 | const metricWeightAC = this.weight.AC[AC] 320 | const metricWeightPR = this.weight.PR[S][PR] 321 | const metricWeightUI = this.weight.UI[UI] 322 | const metricWeightS = this.weight.S[S] 323 | const metricWeightC = this.weight.CIA[C] 324 | const metricWeightI = this.weight.CIA[I] 325 | const metricWeightA = this.weight.CIA[A] 326 | const metricWeightE = this.weight.E[E] 327 | const metricWeightRL = this.weight.RL[RL] 328 | const metricWeightRC = this.weight.RC[RC] 329 | const metricWeightCR = this.weight.CIAR[CR] 330 | const metricWeightIR = this.weight.CIAR[IR] 331 | const metricWeightAR = this.weight.CIAR[AR] 332 | const metricWeightMAV = this.weight.AV[MAV !== "X" ? MAV : AV] 333 | const metricWeightMAC = this.weight.AC[MAC !== "X" ? MAC : AC] 334 | const metricWeightMPR = this.weight.PR[MS !== "X" ? MS : S][MPR !== "X" ? MPR : PR] 335 | const metricWeightMUI = this.weight.UI[MUI !== "X" ? MUI : UI] 336 | const metricWeightMS = this.weight.S[MS !== "X" ? MS : S] 337 | const metricWeightMC = this.weight.CIA[MC !== "X" ? MC : C] 338 | const metricWeightMI = this.weight.CIA[MI !== "X" ? MI : I] 339 | const metricWeightMA = this.weight.CIA[MA !== "X" ? MA : A] 340 | 341 | const iss = (1 - ((1 - metricWeightC) * (1 - metricWeightI) * (1 - metricWeightA))) 342 | const impact = S === 'U' 343 | ? metricWeightS * iss 344 | : metricWeightS * (iss - 0.029) - 3.25 * Math.pow(iss - 0.02, 15) 345 | 346 | const exploitability = this.exploitabilityCoefficient * metricWeightAV * metricWeightAC * metricWeightPR * metricWeightUI 347 | const baseScore = impact <= 0 ? 0 : S === 'U' 348 | ? this.roundUp(Math.min((exploitability + impact), 10)) 349 | : this.roundUp(Math.min(this.scopeCoefficient * (exploitability + impact), 10)) 350 | 351 | const temporalScore = this.roundUp(baseScore * metricWeightE * metricWeightRL * metricWeightRC) 352 | const miss = Math.min(1 - ((1 - metricWeightMC * metricWeightCR) * (1 - metricWeightMI * metricWeightIR) * (1 - metricWeightMA * metricWeightAR)), 0.915) 353 | const modifiedImpact = MS === "U" || (MS === "X" && S === "U") 354 | ? metricWeightMS * miss 355 | : metricWeightMS * (miss - 0.029) - 3.25 * Math.pow(miss * 0.9731 - 0.02, 13) 356 | 357 | const modifiedExploitability = this.exploitabilityCoefficient * metricWeightMAV * metricWeightMAC * metricWeightMPR * metricWeightMUI 358 | const envScore = modifiedImpact <= 0 ? 0 : MS === "U" || (MS === "X" && S === "U") 359 | ? this.roundUp(this.roundUp(Math.min((modifiedImpact + modifiedExploitability), 10)) * metricWeightE * metricWeightRL * metricWeightRC) 360 | : this.roundUp(this.roundUp(Math.min(this.scopeCoefficient * (modifiedImpact + modifiedExploitability), 10)) * metricWeightE * metricWeightRL * metricWeightRC) 361 | 362 | const vectorString = version + "/AV:" + AV + "/AC:" + AC + "/PR:" + PR + "/UI:" + UI + "/S:" + S + "/C:" + C + "/I:" + I + "/A:" + A + "/E:" + E + "/RL:" + RL + "/RC:" + RC + "/CR:" + CR + "/IR:" + IR + "/AR:" + AR + "/MAV:" + MAV + "/MAC:" + MAC + "/MPR:" + MPR + "/MUI:" + MUI + "/MS:" + MS + "/MC:" + MC + "/MI:" + MI + "/MA:" + MA 363 | 364 | return { 365 | baseMetricScore: Number(baseScore.toFixed(1)), 366 | baseSeverity: this.severityRating(baseScore.toFixed(1)), 367 | baseImpact: impact, 368 | baseExploitability: exploitability, 369 | temporalMetricScore: Number(temporalScore.toFixed(1)), 370 | temporalSeverity: this.severityRating(temporalScore.toFixed(1)), 371 | environmentalMetricScore: Number(envScore.toFixed(1)), 372 | environmentalSeverity: this.severityRating(envScore.toFixed(1)), 373 | environmentalModifiedImpact: modifiedImpact, 374 | metricValues, 375 | vectorString, 376 | version, 377 | adjust:(metrics={}) => this.calculateFromMetrics( Object.assign(metricValues, metrics), version ) 378 | } 379 | } 380 | 381 | calculateFromVector(vectorString) { 382 | if( !this.#vectorRegex.test(vectorString) ) 383 | return this.error("Malformed V3.x Vector String") 384 | 385 | const version = vectorString.match(/CVSS:3(\.\d){0,1}/)[0] 386 | const metricNameValue = vectorString.substring(version.length).split("/").slice(1) 387 | const metricValues = {} 388 | 389 | for( const i in metricNameValue ) { 390 | if( !metricNameValue.hasOwnProperty(i) ) continue 391 | const singleMetric = metricNameValue[i].split(":") 392 | metricValues[singleMetric[0]] = singleMetric[1] 393 | } 394 | 395 | return this.calculateFromMetrics(metricValues, version) 396 | } 397 | 398 | describe(vectorOrMetrics) { 399 | return typeof vectorOrMetrics === 'string' 400 | ? this.describeVector(vectorOrMetrics) 401 | : this.describeMetrics(vectorOrMetrics) 402 | } 403 | 404 | describeVector(vectorString) { 405 | if( typeof vectorString !== 'string' || !vectorString?.length ) 406 | throw new Error('CVSS vector string required') 407 | 408 | if( !this.#vectorRegex.test(vectorString) ) 409 | return this.error("Malformed V3.x Vector String") 410 | 411 | const version = vectorString.match(/CVSS:3(\.\d){0,1}/)[0] 412 | const metricNameValue = vectorString.substring(version.length).split("/").slice(1) 413 | const metricValues = {} 414 | 415 | for( const i in metricNameValue ) { 416 | if( !metricNameValue.hasOwnProperty(i) ) continue 417 | const singleMetric = metricNameValue[i].split(":") 418 | metricValues[singleMetric[0]] = singleMetric[1] 419 | } 420 | 421 | return this.describeMetrics(metricValues, version) 422 | } 423 | 424 | describeMetrics(metricValues, version = this.version) { 425 | if( typeof metricValues !== 'object' ) 426 | throw new Error('Metrics object required') 427 | 428 | if( !metricValues.AV || !metricValues.AC || !metricValues.PR || !metricValues.UI || !metricValues.S || !metricValues.C || !metricValues.I || !metricValues.A ) 429 | return this.error("Malformed V3.x Metrics") 430 | 431 | return this.metricKeys.reduce((acc,key) => { 432 | acc[this.metricNames[key]] = this.metricValues[key][metricValues[key]] 433 | return acc 434 | },{}) 435 | } 436 | } 437 | 438 | class CVSS { 439 | constructor() { 440 | this.v2 = new CVSS2() 441 | this.v3 = new CVSS3() 442 | } 443 | 444 | calculate(vectorOrMetrics) { 445 | vectorOrMetrics = vectorOrMetrics || '' 446 | return typeof vectorOrMetrics === 'string' 447 | ? this.calculateFromVector(vectorOrMetrics) 448 | : this.calculateFromMetrics(vectorOrMetrics) 449 | } 450 | calculateFromVector(vectorString = '') { 451 | return !vectorString?.startsWith?.('CVSS:3') || vectorString?.match?.(/Au:[MSN]/) 452 | ? this.v2.calculateFromVector(vectorString) 453 | : this.v3.calculateFromVector(vectorString) 454 | } 455 | calculateFromMetrics(metricValues = {}) { 456 | return metricValues?.hasOwnProperty?.("Au") 457 | ? this.v2.calculateFromMetrics(metricValues) 458 | : this.v3.calculateFromMetrics(metricValues) 459 | } 460 | 461 | describe(vectorOrMetrics) { 462 | vectorOrMetrics = vectorOrMetrics || '' 463 | return typeof vectorOrMetrics === 'string' 464 | ? this.describeVector(vectorOrMetrics) 465 | : this.describeMetrics(vectorOrMetrics) 466 | } 467 | describeVector(vectorString = '') { 468 | return !vectorString?.startsWith?.('CVSS:3') || vectorString?.match?.(/Au:[MSN]/) 469 | ? this.v2.describeVector(vectorString) 470 | : this.v3.describeVector(vectorString) 471 | } 472 | describeMetrics(metricValues = {}) { 473 | return metricValues?.hasOwnProperty?.("Au") 474 | ? this.v2.describeMetrics(metricValues) 475 | : this.v3.describeMetrics(metricValues) 476 | } 477 | } 478 | 479 | module.exports = { 480 | CVSS2, 481 | CVSS3, 482 | CVSS 483 | } 484 | 485 | -------------------------------------------------------------------------------- /lib/helpers.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the difference between two dates (in days) 3 | * @param {Date} date1 4 | * @param {Date} date2 Optional second date to use instead of current date 5 | * @return {number} Difference in days (floating point) 6 | */ 7 | const diffInDays = (date1, date2 = Date.now()) => { 8 | const last = new Date(date1) 9 | const now = new Date(date2) 10 | const Difference_In_Time = now.getTime() - last.getTime() 11 | const Difference_In_Days = Difference_In_Time / (1000 * 3600 * 24) 12 | return Difference_In_Days 13 | } 14 | 15 | /** 16 | * Compare a value against a condition (optional format func) 17 | * @param {*} val The value from the aggregate 18 | * @param {object} option The condition to compare with 19 | * @param {func} format Optional formatting function for normalizing both sides of the condition 20 | * @return {bool} Whether the value matches the condition 21 | */ 22 | const compare = (val, option, format) => { 23 | const key = Object.keys(option||{})[0] 24 | return !key ? false : typeof format === 'function' 25 | ? compareFunc[key]( val === null ? val : format(val), format(option[key]) ) 26 | : compareFunc[key]( val, option[key] ) 27 | } 28 | 29 | /** 30 | * Comparison functions mapped by key (gt,gte,lt,lte,eq,ne,neq) 31 | */ 32 | const compareFunc = { 33 | gt: (v1, v2) => v1 > v2, //Greater than 34 | gte:(v1, v2) => v1 >= v2, //Greater than, or equal 35 | lt: (v1, v2) => v1 < v2, //Less than 36 | lte:(v1, v2) => v1 <= v2, //Less than, or equal 37 | eq: (v1, v2) => v1 === v2, //Is equal 38 | ne: (v1, v2) => v1 !== v2, //Not equal 39 | neq:(v1, v2) => v1 !== v2, //Same as ne 40 | } 41 | 42 | module.exports = { 43 | diffInDays, 44 | compare, 45 | compareFunc 46 | } 47 | 48 | -------------------------------------------------------------------------------- /lib/index.cjs: -------------------------------------------------------------------------------- 1 | const { writeFileSync, readFileSync, existsSync } = require('fs') 2 | const { fileURLToPath } = require('url') 3 | const { join, dirname } = require('path') 4 | 5 | const { CVSS } = require(join(__dirname, 'cvss.cjs')) 6 | const { Logger } = require(join(__dirname, 'logger.cjs')) 7 | const { diffInDays, compare } = require(join(__dirname, 'helpers.cjs')) 8 | 9 | class CVEAggregate { 10 | #urlCVES = "https://cve.mitre.org/data/downloads/allitems.csv" 11 | #urlCISA = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json" 12 | #urlEPSS = "https://api.first.org/data/v1/epss" 13 | #urlCVSS = "https://services.nvd.nist.gov/rest/json/cves/2.0" 14 | #CVSS = new CVSS() 15 | #weight = { cisa:0.8, epss:0.8, cvss:0.5 } 16 | 17 | constructor(filepath, verbose = false){ 18 | this.filepath = filepath?.length ? filepath : null //join(__dirname, 'cves.json') 19 | this.verbose = verbose 20 | this.logger = verbose ? new Logger() : { log:() => {} } 21 | this.cves = {} 22 | this.lastUpdated = null 23 | this.cvesUpdated = null 24 | this.cisaUpdated = null 25 | this.epssUpdated = null 26 | this.cvssUpdated = null 27 | this.lastCount = null 28 | this.daysdiff = 0.1 //Skip epss and cvss update if less than this many days since last update 29 | this.load() 30 | } 31 | 32 | /** 33 | * Log to console 34 | * @param {array} lines a list of strings to replace the last n-lines in console if the last log was array 35 | * @param {Error} lines an error object to throw in console 36 | * @param {any} lines any other value/type to log in console 37 | */ 38 | log(...lines) { 39 | this.logger.log(...lines) 40 | } 41 | 42 | /** 43 | * Dump internal details 44 | * @return {object} contents of the current cve json 45 | */ 46 | dump() { 47 | return { 48 | lastUpdated:this.lastUpdated, 49 | cvesUpdated:this.cvesUpdated, 50 | cisaUpdated:this.cisaUpdated, 51 | epssUpdated:this.epssUpdated, 52 | cvssUpdated:this.cvssUpdated, 53 | lastCount:Object.keys(this.cves).length, 54 | cves:this.cves, 55 | } 56 | } 57 | 58 | /** 59 | * Save current cves to filepath 60 | */ 61 | save(filepath = this.filepath) { 62 | this.lastUpdated = (new Date()).toISOString() 63 | if( filepath?.length ) 64 | writeFileSync(filepath, JSON.stringify(this.dump()), 'utf8') 65 | } 66 | 67 | /** 68 | * Load cves from a filepath 69 | */ 70 | load(filepath = this.filepath) { 71 | try { 72 | if( !existsSync(filepath) ) throw new Error('No cve list') 73 | //Load the content of last save 74 | const json = JSON.parse(readFileSync(filepath, 'utf8')) 75 | this.cves = json.cves || this.cves 76 | this.lastUpdated = json.lastUpdated || this.lastUpdated 77 | this.lastCount = json.lastCount || this.lastCount 78 | this.cvesUpdated = json.cvesUpdated || this.cvesUpdated 79 | this.cisaUpdated = json.cisaUpdated || this.cisaUpdated 80 | this.epssUpdated = json.epssUpdated || this.epssUpdated 81 | this.cvssUpdated = json.cvssUpdated || this.cvssUpdated 82 | } catch(e) { 83 | //No file or error loading, create fresh 84 | this.cves = this.cves || {} 85 | this.lastCount = this.lastCount || null 86 | this.resetTimestamps() 87 | } 88 | this.resetCounters() 89 | } 90 | 91 | resetCounters() { 92 | //reset the new item counters 93 | this.newCVES = new Set() 94 | this.newCISA = new Set() 95 | this.newEPSS = new Set() 96 | this.newCVSS2 = new Set() 97 | this.newCVSS3 = new Set() 98 | } 99 | 100 | resetTimestamps() { 101 | this.lastUpdated = null 102 | this.cvesUpdated = null 103 | this.cisaUpdated = null 104 | this.epssUpdated = null 105 | this.cvssUpdated = null 106 | } 107 | 108 | /** 109 | * Report update details since last load 110 | * @return {object} collection of data details 111 | */ 112 | report(reportZero) { 113 | if( reportZero || this.newCVES.size ) this.logger.log(`Found ${this.newCVES.size.toLocaleString()} new CVEs`) 114 | if( reportZero || this.newCISA.size ) this.logger.log(`Found ${this.newCISA.size.toLocaleString()} new CISA entries`) 115 | if( reportZero || this.newEPSS.size ) this.logger.log(`Found ${this.newEPSS.size.toLocaleString()} new EPSS scores`) 116 | if( reportZero || this.newCVSS2.size ) this.logger.log(`Found ${this.newCVSS2.size.toLocaleString()} new CVSSv2 vectors`) 117 | if( reportZero || this.newCVSS3.size ) this.logger.log(`Found ${this.newCVSS3.size.toLocaleString()} new CVSSv3 vectors`) 118 | //If anything found, add a divider line 119 | if( reportZero || this.newCVES.size || this.newCISA.size || this.newEPSS.size || this.newCVSS2.size || this.newCVSS3.size ) 120 | this.logger.log(`-`.repeat(30)) 121 | 122 | //Collect and return details in one object 123 | const data = this.dump() 124 | 125 | data.newCVES = this.newCVES 126 | data.newCISA = this.newCISA 127 | data.newEPSS = this.newEPSS 128 | data.newCVSS2 = this.newCVSS2 129 | data.newCVSS3 = this.newCVSS3 130 | data.totalCVES = Object.keys(this.cves).length 131 | data.totalCISA = Object.values(this.cves).filter(i => i.cisa).length 132 | data.totalEPSS = Object.values(this.cves).filter(i => i.epss).length 133 | data.totalCVSS = Object.values(this.cves).filter(i => i.cvss2 || i.cvss3).length 134 | 135 | this.logger.log(`Total CVEs: ${data.totalCVES.toLocaleString()}`) 136 | this.logger.log(`Total CISA entries: ${data.totalCISA.toLocaleString()}`) 137 | this.logger.log(`Total EPSS scores: ${data.totalEPSS.toLocaleString()}`) 138 | this.logger.log(`Total CVSS vectors: ${data.totalCVSS.toLocaleString()}`) 139 | 140 | return data 141 | } 142 | 143 | /* ACCESSORS */ 144 | 145 | /** 146 | * Search one or more CVEs to see if they're in the CISA KEV 147 | * @param {...string} cveIds CVE ids to search against 148 | * @return {bool} true if any cve is in CISA 149 | */ 150 | getCISA(...cveIds) { 151 | for(const cveId of cveIds) { 152 | if( this.cves[cveId]?.cisa?.length ) return true 153 | } 154 | return false 155 | } 156 | 157 | /** 158 | * Get the scaled EPSS score of one or more CVEs 159 | * @param {...string} cveIds CVE ids to search against 160 | * @return {number} EPSS score 161 | */ 162 | getEPSS(...cveIds) { 163 | return (1 - cveIds.map(cveId => this.cves[cveId]?.epss || 0).filter(v => v).reduce((p,v) => p * (1-v),1)) 164 | } 165 | 166 | /** 167 | * Get the maximum CVSS score of one or more CVEs 168 | * @param {...string} cveIds CVE ids to search against 169 | * @return {number} CVSS score 170 | */ 171 | getCVSS(...cveIds) { 172 | return cveIds.reduce((max,cveId) => { 173 | const score = this.#CVSS.calculateFromVector(this.cves[cveId]?.cvss3 || this.cves[cveId]?.cvss2) 174 | return Math.max(max, score.environmentalMetricScore) 175 | },0) 176 | } 177 | 178 | /* MAPPERS */ 179 | 180 | /** 181 | * Map the CISA due dates to one or more CVEs 182 | * @param {...string} cveIds CVE ids to search against 183 | * @return {object} due dates keyed by CVE id 184 | */ 185 | mapCISA(...cveIds) { 186 | return cveIds.reduce((map,cveId) => { 187 | map[cveId] = this.cves[cveId]?.cisa || null 188 | return map 189 | },{}) 190 | } 191 | 192 | /** 193 | * Map the EPSS score to one or more CVEs 194 | * @param {...string} cveIds CVE ids to search against 195 | * @return {object} EPSS scores keyed by CVE id 196 | */ 197 | mapEPSS(...cveIds) { 198 | return cveIds.reduce((map,cveId) => { 199 | map[cveId] = this.cves[cveId]?.epss || 0 200 | return map 201 | },{}) 202 | } 203 | 204 | /** 205 | * Map the CVSS score to one or more CVEs (v3 if exists, else v2) 206 | * @param {...string} cveIds CVE ids to search against 207 | * @return {object} CVSS scores keyed by CVE id 208 | */ 209 | mapCVSS(...cveIds) { 210 | return cveIds.reduce((map,cveId) => { 211 | map[cveId] = this.cves[cveId]?.cvss3 || this.cves[cveId]?.cvss2 || null 212 | return map 213 | },{}) 214 | } 215 | 216 | /* PARSERS */ 217 | 218 | /** 219 | * Parse a line from the CVE-CSV - looking for CVE ids 220 | * @param {string} line one record from the CVE.csv 221 | */ 222 | parseCSVLine(line) { 223 | if( !line.length ) return 224 | 225 | const cveId = line?.match?.(/^(CVE-\d{4}-\d{4,})\s*,/)?.[1] 226 | if( !cveId?.length ) return 227 | if( !(cveId in this.cves) ) { 228 | this.cves[cveId] = { days:0, cisa:null, epss:0, cvss2:null, cvss3:null } 229 | this.newCVES.add(cveId) 230 | } 231 | } 232 | 233 | /** 234 | * Parse the due date from CISA entry 235 | * @param {object} item Entry from the CISA KEV 236 | */ 237 | parseCISA(item) { 238 | const cveId = item?.cveID 239 | if( !cveId?.length ) return 240 | if( !(cveId in this.cves) ) { 241 | this.cves[cveId] = { days:0, cisa:null, epss:0, cvss2:null, cvss3:null } 242 | this.newCVES.add(cveId) 243 | } 244 | 245 | if( this.cves[cveId].cisa === item.dueDate ) 246 | return //Already the same 247 | 248 | if( !this.cves[cveId].cisa ) { 249 | this.newCISA.add(cveId) 250 | } 251 | this.cves[cveId].cisa = item.dueDate 252 | this.cves[cveId].days = diffInDays(item.dateAdded, item.dueDate) 253 | } 254 | 255 | /** 256 | * Parse the EPSS score from first.org response item 257 | * @param {object} item Entry from the EPSS response 258 | */ 259 | parseEPSS(item) { 260 | const cveId = item?.cve 261 | if( !cveId?.length ) return 262 | if( !(cveId in this.cves) ) { 263 | this.cves[cveId] = { days:0, cisa:null, epss:0, cvss2:null, cvss3:null } 264 | this.newCVES.add(cveId) 265 | } 266 | 267 | if( this.cves[cveId].epss === item.epss ) 268 | return //Already the same 269 | 270 | if( !this.cves[cveId].epss ) { 271 | this.newEPSS.add(cveId) 272 | } 273 | this.cves[cveId].epss = Number(item.epss) 274 | } 275 | 276 | /** 277 | * Parse the CVSS scores from first.org response item 278 | * @param {object} item Entry from the CVSS response 279 | */ 280 | parseCVSS(item) { 281 | const cveId = item?.cve?.id 282 | if( !cveId?.length ) return 283 | if( !(cveId in this.cves) ) { 284 | this.cves[cveId] = { days:0, cisa:null, epss:0, cvss2:null, cvss3:null } 285 | this.newCVES.add(cveId) 286 | } 287 | 288 | const { cvssMetricV2, cvssMetricV31 } = item?.cve?.metrics || {} 289 | 290 | const v2vector = cvssMetricV2?.[0]?.cvssData?.vectorString 291 | if( v2vector?.length && this.cves[cveId].cvss2 !== v2vector ) { 292 | if( !this.cves[cveId].cvss2 ) this.newCVSS2.add(cveId) 293 | this.cves[cveId].cvss2 = v2vector 294 | } 295 | 296 | const v3vector = cvssMetricV31?.[0]?.cvssData?.vectorString 297 | if( v3vector?.length && this.cves[cveId].cvss3 !== v3vector ) { 298 | if( !this.cves[cveId].cvss3 ) this.newCVSS3.add(cveId) 299 | this.cves[cveId].cvss3 = v3vector 300 | } 301 | } 302 | 303 | /* UPDATE ROUTINES */ 304 | 305 | /** 306 | * Stream the CVE-CSV from mitre.org and extract new entries 307 | * @param {array} feedback array of console lines for updating feedback 308 | * @param {number} index index of the feedback array for this function 309 | * @return {Promise} 310 | */ 311 | async update_cves (feedback=[], index=0, saveAtEachStage) { 312 | if( this.cvesUpdated?.length && diffInDays(this.cvesUpdated) < this.daysdiff ) 313 | return feedback[index] = `Updating CVEs ... [skip]` 314 | 315 | const https = require('node:https') 316 | const readline = require('node:readline') 317 | const cancelRequest = new AbortController() 318 | let fileDate = new Date().toISOString() 319 | return new Promise((resolve, reject) => { 320 | https.get(this.#urlCVES, { signal: cancelRequest.signal }, (res) => { 321 | fileDate = new Date(res.headers['last-modified']).toISOString() 322 | if( fileDate === this.cvesUpdated ) { 323 | feedback[index] = `Updating CVEs ... [skip]` 324 | cancelRequest.abort() 325 | return reject() 326 | } 327 | 328 | let len = 0 329 | feedback[index] = `Updating CVEs ... ` 330 | const size = Number(res.headers['content-length']) 331 | const readStream = readline.createInterface({ input:res }) 332 | readStream.on('close', resolve) 333 | readStream.on('error', reject) 334 | readStream.on("line", line => this.parseCSVLine(line)) 335 | res.on('data', (data) => { 336 | const pct = ((len += data.length) / size * 100).toFixed(1) 337 | feedback[index] = `Updating CVEs ... ${pct}%` 338 | }) 339 | 340 | }) 341 | }) 342 | .then(() => { 343 | feedback[index] = `Updating CVEs ... 100.0%` 344 | this.cvesUpdated = fileDate 345 | }) 346 | .catch(e => this.logger.log(e)) 347 | .finally(() => saveAtEachStage ? this.save() : null) 348 | } 349 | 350 | /** 351 | * Fetch the CISA-KEV from cisa.gov and extract new entries 352 | * @param {array} feedback array of console lines for updating feedback 353 | * @param {number} index index of the feedback array for this function 354 | * @return {Promise} 355 | */ 356 | async update_cisa (feedback=[], index=0, saveAtEachStage) { 357 | if( this.cisaUpdated?.length && diffInDays(this.cisaUpdated) < this.daysdiff ) 358 | return feedback[index] = `Updating CISA ... [skip]` 359 | 360 | let fileDate = new Date().toISOString() 361 | return new Promise(async (resolve, reject) => { 362 | 363 | feedback[index] = `Updating CISA ... ` 364 | const kev = await fetch(this.#urlCISA) 365 | .then(res => res.json()) 366 | .catch(e => this.logger.log(e)) 367 | 368 | const { count = 0, vulnerabilities = [] } = kev || {} 369 | kev?.vulnerabilities?.forEach((item,i) => { 370 | let pct = (i / (count || 1) * 100).toFixed(1) 371 | feedback[index] = `Updating CISA ... ${pct}%` 372 | this.parseCISA(item) 373 | }) 374 | resolve() 375 | 376 | }) 377 | .then(() => { 378 | feedback[index] = `Updating CISA ... 100.0%` 379 | this.cisaUpdated = fileDate 380 | }) 381 | .catch(e => this.logger.log(e)) 382 | .finally(() => saveAtEachStage ? this.save() : null) 383 | } 384 | 385 | /** 386 | * Fetch the EPSS scores from first.org and extract new entries 387 | * @param {array} feedback array of console lines for updating feedback 388 | * @param {number} index index of the feedback array for this function 389 | * @return {Promise} 390 | */ 391 | async update_epss (feedback=[], index=0, saveAtEachStage) { 392 | if( this.epssUpdated?.length && diffInDays(this.epssUpdated) < this.daysdiff ) 393 | return feedback[index] = `Updating EPSS ... [skip]` 394 | 395 | let fileDate = new Date().toISOString() 396 | return new Promise(async (resolve, reject) => { 397 | const lastUpdated = this.epssUpdated 398 | const daysLimit = (() => { 399 | if( !lastUpdated ) return '' 400 | const last = new Date(lastUpdated) 401 | const now = new Date() 402 | const Difference_In_Time = now.getTime() - last.getTime() 403 | const Difference_In_Days = Difference_In_Time / (1000 * 3600 * 24) 404 | const Extra_day = Math.ceil(Difference_In_Days+1) || 1 //extra day 405 | return `&days=${Extra_day}` 406 | })() 407 | 408 | feedback[index] = `Updating EPSS ... ` 409 | const firstBatch = await fetch(`${this.#urlEPSS}?envelope=true${daysLimit}`) 410 | .then(res => res.json()) 411 | .catch(e => this.logger.log(e)) 412 | 413 | const { total = 1, data = [] } = firstBatch || {} 414 | 415 | let offset = data?.length || total || 1 416 | let pct = (offset / (total || 1) * 100).toFixed(1) 417 | 418 | feedback[index] = `Updating EPSS ... ${pct}%` 419 | data?.forEach?.(item => this.parseEPSS(item)) 420 | 421 | let fails = 0 422 | while(total > offset) { 423 | const loopBatch = await fetch(`${this.#urlEPSS}?envelope=true&offset=${offset}${daysLimit}`) 424 | .then(res => res.status < 400 ? res.json() : null) 425 | .catch(e => this.logger.log(e)) 426 | 427 | if( !loopBatch?.data ) { 428 | //Failed more than 5 times 429 | if( ++fails > 5 ) break 430 | feedback[index] = `Updating EPSS ... ${pct}% [Fetch failure (${fails})]` 431 | //Failed - wait 5 seconds and try again 432 | await new Promise(done => setTimeout(() => done(), 5000*fails)) 433 | continue 434 | } else { 435 | fails = 0 436 | offset += loopBatch.data.length || 0 437 | pct = (offset / (total || 1) * 100).toFixed(1) 438 | feedback[index] = `Updating EPSS ... ${pct}% ${' '.repeat(20)}` 439 | loopBatch.data?.forEach?.(item => this.parseEPSS(item)) 440 | } 441 | 442 | if( loopBatch.data.length < loopBatch.limit ) break 443 | } 444 | resolve() 445 | }) 446 | .then(() => { 447 | feedback[index] = `Updating EPSS ... 100.0%` 448 | this.epssUpdated = fileDate 449 | }) 450 | .catch(e => this.logger.log(e)) 451 | .finally(() => saveAtEachStage ? this.save() : null) 452 | } 453 | 454 | /** 455 | * Fetch the CVSS vectors from nist.gov and extract new entries 456 | * @param {array} feedback array of console lines for updating feedback 457 | * @param {number} index index of the feedback array for this function 458 | * @return {Promise} 459 | */ 460 | async update_cvss (feedback=[], index=0, saveAtEachStage) { 461 | if( this.cvssUpdated?.length && diffInDays(this.cvssUpdated) < this.daysdiff ) 462 | return feedback[index] = `Updating CVSS ... [skip]` 463 | 464 | let fileDate = new Date().toISOString() 465 | return new Promise(async (resolve, reject) => { 466 | const lastUpdated = this.cvssUpdated 467 | const daysLimit = (() => { 468 | if( !lastUpdated ) return '' 469 | const lastDate = new Date(lastUpdated) 470 | lastDate.setDate(lastDate.getDate()-1) //extra day 471 | const lastModStartDate = lastDate.toISOString().split('.')[0]+"Z" 472 | const lastModEndDate = new Date().toISOString().split('.')[0]+"Z" 473 | return `&lastModStartDate=${lastModStartDate}&lastModEndDate=${lastModEndDate}` 474 | })() 475 | 476 | feedback[index] = `Updating CVSS ... ` 477 | const firstBatch = await fetch(`${this.#urlCVSS}?startIndex=0&${daysLimit}`) 478 | .then(res => res.json()) 479 | .catch(e => this.logger.log(e)) 480 | 481 | const { resultsPerPage = 0, totalResults = 0, vulnerabilities = [] } = firstBatch || {} 482 | 483 | let offset = vulnerabilities?.length || totalResults || 1 484 | let pct = (offset / (totalResults || 1) * 100).toFixed(1) 485 | 486 | feedback[index] = `Updating CVSS ... ${pct}%` 487 | vulnerabilities?.forEach?.(item => this.parseCVSS(item)) 488 | 489 | let fails = 0 490 | while(totalResults > offset) { 491 | await new Promise(done => setTimeout(() => done(), 4000)) //Fetch throttle 492 | 493 | const loopBatch = await fetch(`${this.#urlCVSS}?startIndex=${offset}${daysLimit}`) 494 | .then(res => res.status < 400 ? res.json() : null) 495 | .catch(e => this.logger.log(e)) 496 | 497 | if( !loopBatch?.vulnerabilities ) { 498 | //Failed more than 5 times 499 | if( ++fails > 5 ) break 500 | feedback[index] = `Updating CVSS ... ${pct}% [Fetch failure (${fails})]` 501 | //Failed - throttle and try again 502 | await new Promise(done => setTimeout(() => done(), 5000*fails)) 503 | continue 504 | } else { 505 | fails = 0 506 | offset += loopBatch.vulnerabilities.length || 0 507 | pct = (offset / (totalResults || 1) * 100).toFixed(1) 508 | feedback[index] = `Updating CVSS ... ${pct}% ${' '.repeat(20)}` 509 | loopBatch.vulnerabilities?.forEach?.(item => this.parseCVSS(item)) 510 | } 511 | 512 | if( loopBatch.vulnerabilities.length < resultsPerPage ) break 513 | } 514 | resolve() 515 | }) 516 | .then(() => { 517 | feedback[index] = `Updating CVSS ... 100.0%` 518 | this.cvssUpdated = fileDate 519 | }) 520 | .catch(e => this.logger.log(e)) 521 | .finally(() => saveAtEachStage ? this.save() : null) 522 | } 523 | 524 | /* Generate aggregate source */ 525 | 526 | /** 527 | * Build with full CVE list 528 | * Includes the CVE csv where some CVEs will not have matching data 529 | * @return {Promise.} 530 | */ 531 | async build(saveAtEachStage) { 532 | const feedback = new Array(4).fill('...') 533 | const interval = setInterval(() => this.logger.log(feedback), 1000) 534 | return Promise.all([ 535 | this.update_cves(feedback, 0, saveAtEachStage), 536 | this.update_cisa(feedback, 1, saveAtEachStage), 537 | this.update_epss(feedback, 2, saveAtEachStage), 538 | this.update_cvss(feedback, 3, saveAtEachStage), 539 | ]) 540 | .then(() => this.cves) 541 | .finally(() => { 542 | clearInterval(interval) 543 | this.logger.log(feedback) 544 | }) 545 | } 546 | async forceBuild(saveAtEachStage) { 547 | this.resetTimestamps() 548 | return this.build(saveAtEachStage) 549 | } 550 | 551 | /** 552 | * Build with only applicable CVEs 553 | * Only generate a list of CVEs that have data 554 | * @return {Promise.} 555 | */ 556 | async update(saveAtEachStage) { 557 | const feedback = new Array(3).fill('...') 558 | const interval = setInterval(() => this.logger.log(feedback), 1000) 559 | return Promise.all([ 560 | this.update_cisa(feedback, 0, saveAtEachStage), 561 | this.update_epss(feedback, 1, saveAtEachStage), 562 | this.update_cvss(feedback, 2, saveAtEachStage), 563 | ]) 564 | .then(() => this.cves) 565 | .finally(() => { 566 | clearInterval(interval) 567 | this.logger.log(feedback) 568 | }) 569 | } 570 | async forceUpdate(saveAtEachStage) { 571 | this.resetTimestamps() 572 | return this.update(saveAtEachStage) 573 | } 574 | 575 | 576 | /* Helper tools and calculators */ 577 | 578 | /** 579 | * Calculate a CVSS scoring from a vector string 580 | * @param {string} vectorOrMetrics The CVSS (v2 or v3) vector string 581 | * @param {object} vectorOrMetrics The CVSS metrics object 582 | * @return {object} calculation results 583 | */ 584 | calculateCVSS(vectorOrMetrics) { 585 | return this.#CVSS.calculate(vectorOrMetrics) 586 | } 587 | 588 | /** 589 | * Describe a CVSS vector or metrics object 590 | * @param {string} vectorOrMetrics The CVSS (v2 or v3) vector string 591 | * @param {object} vectorOrMetrics The CVSS metrics object 592 | * @return {object} 593 | */ 594 | describeCVSS(vectorOrMetrics) { 595 | return this.#CVSS.describe(vectorOrMetrics) 596 | } 597 | 598 | /** 599 | * Search the aggregate with fields and conditions 600 | * ex. search({ epss:{ gt:0.5 } }) 601 | * @param {object} options condition objects keyed by data field 602 | * @return {object} full entries that match the given criteria 603 | */ 604 | search(options={}) { 605 | const critical = {} 606 | for(const cveId in this.cves) { 607 | const { cisa, epss, cvss2, cvss3 } = this.cves[cveId] 608 | 609 | //First: Lightest compare 610 | const matchEPSS = !options?.epss || compare(epss, options?.epss, (v) => Number(v)) 611 | if( !matchEPSS ) continue 612 | 613 | //Second: Date conversions 614 | const matchCISA = !options?.cisa || compare(cisa, options?.cisa, (v) => v ? new Date(v).getTime() : v) 615 | if( !matchCISA ) continue 616 | 617 | //Last: CVSS calculation 618 | const score = this.calculateCVSS(cvss3 || cvss2) 619 | const matchCVSS = !options?.cvss || compare(score.environmentalMetricScore, options?.cvss, (v) => Number(v)) 620 | if( !matchCVSS ) continue 621 | 622 | const daysUntilDue = Math.round(diffInDays(Date.now(), cisa)) 623 | 624 | //If here, we match, return the calculated cvss instead of any vectors 625 | critical[cveId] = { daysUntilDue, cisa, epss, cvss:score.environmentalMetricScore } 626 | } 627 | return critical 628 | } 629 | 630 | /** 631 | * Fetch entries from the aggregate for one or more CVEs 632 | * @param {...string} cveIds CVE ids to search against 633 | * @return {object} full entries for the given CVEs 634 | */ 635 | map(...cveIds) { 636 | return cveIds.reduce((map,cveId) => { 637 | if( cveId in this.cves ) { 638 | map[cveId] = { ...this.cves[cveId] } 639 | } 640 | return map 641 | },{}) 642 | } 643 | 644 | /** 645 | * Fetch entries from the aggregate for one or more CVEs 646 | * @param {...string} cveIds CVE ids to search against 647 | * @return {array} full entries for the given CVEs 648 | */ 649 | list(...cveIds) { 650 | return cveIds.reduce((arr,cveId) => { 651 | if( cveId in this.cves ) { 652 | arr.push({ id:cveId, ...this.cves[cveId] }) 653 | } 654 | return arr 655 | },[]) 656 | } 657 | 658 | /** 659 | * Get list of all CVE Ids 660 | * @return {string[]} list of CVE ids 661 | */ 662 | cveList() { 663 | return Object.keys(this.cves) 664 | } 665 | 666 | /** 667 | * Search one or more CVEs to check risk 668 | * @param {...string} cveIds CVE ids to search against 669 | * @return {object} risk 670 | */ 671 | check(...cveIds) { 672 | const peak = { cisa:0, epss:0, cvss:0 } 673 | const list = { cisa:[], epss:[], cvss:[] } 674 | for(const cveId of cveIds) { 675 | const ref = this.cves[cveId] 676 | if( !ref ) continue 677 | if( ref.cisa ) { 678 | const days = Math.round(diffInDays(Date.now(), ref.cisa)) 679 | if( days <= 0 ) list.cisa.push({date:ref.cisa, days, risk:100}) 680 | else list.cisa.push({date:ref.cisa, days, risk:days / ref.days * 100}) 681 | } 682 | if( ref.epss ) list.epss.push({score:ref.epss}) 683 | if( ref.cvss3 || ref.cvss2 ) list.cvss.push({vector:ref.cvss3 || ref.cvss2}) 684 | } 685 | 686 | peak.epss = (1 - list.epss.reduce((p,v) => p * (1-v.score),1)) 687 | peak.cvss = list.cvss.reduce((max,{vector}) => { 688 | if( max === 10 ) return max 689 | const score = this.calculateCVSS(vector).environmentalMetricScore 690 | return Math.max(max, score) 691 | },0) 692 | 693 | peak.epss = { epss:peak.epss, risk:Math.round(peak.epss*100) } 694 | peak.cvss = { cvss:peak.cvss, risk:Math.round(peak.cvss*10) } 695 | peak.cisa = list.cisa.reduce((max,entry) => { 696 | if( entry.risk > max.risk ) return entry 697 | return max 698 | },{date:null,days:0,risk:0}) 699 | 700 | let epss = peak.epss.risk * this.#weight.epss 701 | let cvss = peak.cvss.risk * this.#weight.cvss 702 | let cisa = peak.cisa.risk * this.#weight.cisa 703 | 704 | let num = epss + cvss + cisa 705 | let den = this.#weight.epss + this.#weight.cvss + this.#weight.cisa 706 | 707 | return { peak, risk:Math.round(num / (den || 1)) } 708 | } 709 | 710 | } 711 | 712 | module.exports = { 713 | CVEAggregate 714 | } 715 | 716 | -------------------------------------------------------------------------------- /lib/logger.cjs: -------------------------------------------------------------------------------- 1 | class Logger { 2 | constructor() { 3 | this.logging = false 4 | } 5 | 6 | /** 7 | * Log to console 8 | * @param {Error} lines an error object to throw in console 9 | */ 10 | error(err) { 11 | this.logging = false 12 | console.error(err) 13 | } 14 | 15 | /** 16 | * Log multiple lines console 17 | * @param {array} lines a list of strings to replace the last n-lines in console if the last log was array 18 | */ 19 | multi(lines) { 20 | if( this.logging ) { 21 | process.stdout.moveCursor(0, -1*(lines.length)) 22 | process.stdout.clearLine(0) 23 | process.stdout.cursorTo(0) 24 | } 25 | process.stdout.write(lines.join('\n')+'\n') 26 | this.logging = true 27 | } 28 | 29 | /** 30 | * Log to console 31 | * @param {array} lines a list of strings to replace the last n-lines in console if the last log was array 32 | * @param {Error} lines an error object to throw in console 33 | * @param {any} lines any other value/type to log in console 34 | */ 35 | log(lines, ...other) { 36 | if( lines === undefined ) return 37 | if( lines instanceof Error ) return this.error(lines) 38 | if( Array.isArray(lines) ) return this.multi(lines) 39 | 40 | this.logging = false 41 | console.log(lines, ...other) 42 | } 43 | } 44 | module.exports = { 45 | Logger 46 | } 47 | 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cveaggregate", 3 | "version": "1.0.0", 4 | "description": "Build a CVE library with aggregated CISA, EPSS and CVSS data", 5 | "main": "lib/index.cjs", 6 | "module": "src/index.js", 7 | "exports": { 8 | ".": { 9 | "import": "./src/index.js", 10 | "require": "./lib/index.cjs" 11 | } 12 | }, 13 | "type":"module", 14 | "scripts": { 15 | "test": "echo \"Error: try test_common or test_esm\" && exit 1", 16 | "test_esm": "node ./test/test_esm.mjs", 17 | "test_common": "node ./test/test_common.cjs" 18 | }, 19 | "author": "r3volved", 20 | "license": "ISC" 21 | } 22 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # CVEAggregate 2 | Build a CVE library with aggregated CISA, EPSS and CVSS data 3 | 4 | - **CISA Values** : The remediation due date (or null) 5 | - **EPSS Values** : The EPSS probability score (or 0) 6 | - **CVSS Values** : V2 and/or V3 vector strings (or null) 7 | 8 | ```js 9 | import { CVEAggregate } from "/path/to/CVEAggregate/src/index.js" 10 | 11 | /* If verbose, will log stuff to console */ 12 | const verbose = true 13 | const cves = new CVEAggregate('/path/to/cves.json', verbose) 14 | ``` 15 | 16 | ## Building the aggregate 17 | 18 | The path provided to the constructor will load file if exists and will save updates to same location. 19 | 20 | The build process will collect all existing CVE Ids regardless of their state or age. 21 | 22 | The update process will collect only the CVE Ids that have associated aggregate data (epps, cvss, cisa). 23 | 24 | Note: *Once the initial aggregate has been created, subsequent build or update calls will only collect new items since last save.* 25 | 26 | ```js 27 | /* Build full list */ 28 | await cves.build() 29 | 30 | /* Build short list */ 31 | await cves.update() 32 | 33 | /* List new items since last load, plus aggregate totals and details */ 34 | cves.report() 35 | 36 | /* Return the full json aggregate */ 37 | const data = cves.dump() 38 | 39 | /* Force save (to the filepath provided) */ 40 | cves.save() 41 | 42 | /* Force load (from the filepath provided) */ 43 | cves.load() 44 | ``` 45 | 46 | ## Accessing the aggregate 47 | 48 | Helper functions are provided to help access and reference the aggregate 49 | 50 | ```js 51 | const listOfCves = ['CVE-2023-35390','CVE-2023-35391','CVE-2023-38180'] 52 | 53 | /* Get matching cve entries as an object */ 54 | const map = cves.map(...listOfCves) 55 | //> { 56 | //> 'CVE-2023-35390': { 57 | //> days: 0, 58 | //> cisa: null, 59 | //> epss: 0.00564, 60 | //> cvss2: null, 61 | //> cvss3: 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H' 62 | //> }, 63 | //> 'CVE-2023-35391': { 64 | //> days: 0, 65 | //> cisa: null, 66 | //> epss: 0.00114, 67 | //> cvss2: null, 68 | //> cvss3: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N' 69 | //> }, 70 | //> 'CVE-2023-38180': { 71 | //> days: 21, 72 | //> cisa: '2023-08-30', 73 | //> epss: 0.00484, 74 | //> cvss2: null, 75 | //> cvss3: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H' 76 | //> } 77 | //> } 78 | 79 | /* Get matching cve entries as an array */ 80 | const list = cves.list(...listOfCves) 81 | //> [ 82 | //> { 83 | //> id: 'CVE-2023-35390', 84 | //> days: 0, 85 | //> cisa: null, 86 | //> epss: 0.00564, 87 | //> cvss2: null, 88 | //> cvss3: 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H' 89 | //> }, 90 | //> { 91 | //> id: 'CVE-2023-35391', 92 | //> days: 0, 93 | //> cisa: null, 94 | //> epss: 0.00114, 95 | //> cvss2: null, 96 | //> cvss3: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N' 97 | //> }, 98 | //> { 99 | //> id: 'CVE-2023-38180', 100 | //> days: 21, 101 | //> cisa: '2023-08-30', 102 | //> epss: 0.00484, 103 | //> cvss2: null, 104 | //> cvss3: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H' 105 | //> } 106 | //> ] 107 | 108 | /* Get the whole list of CVE IDs in the cache */ 109 | const allCVEs = cves.cveList() 110 | ``` 111 | 112 | Get a value reduced/scaled across one or more CVE Ids 113 | 114 | ```js 115 | /* Check one or more CVE Ids if (any) in the CISA KEV */ 116 | const inKEV = cves.getCISA(...listOfCves) 117 | //> true 118 | 119 | /* Get the scaled EPSS score for one or more CVE Ids */ 120 | const epssScore = cves.getEPSS(...listOfCves) 121 | //> 0.011580786319263958 122 | 123 | /* Get the maximum CVSS score across one or more CVE Ids */ 124 | const cvssScore = cves.getCVSS(...listOfCves) 125 | //> 7.8 126 | ``` 127 | 128 | Get the full mapping of CVE Ids -to- values 129 | 130 | ```js 131 | const cisaMap = cves.mapCISA(...listOfCves) 132 | //> { 133 | //> 'CVE-2023-35390': null, 134 | //> 'CVE-2023-35391': null, 135 | //> 'CVE-2023-38180': '2023-08-30' 136 | //> } 137 | 138 | const epssMap = cves.mapEPSS(...listOfCves) 139 | //> { 140 | //> 'CVE-2023-35390': 0.00564, 141 | //> 'CVE-2023-35391': 0.00114, 142 | //> 'CVE-2023-38180': 0.00484 143 | //> } 144 | 145 | const cvssMap = cves.mapCVSS(...listOfCves) 146 | //> { 147 | //> 'CVE-2023-35390': 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H', 148 | //> 'CVE-2023-35391': 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N', 149 | //> 'CVE-2023-38180': 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H' 150 | //> } 151 | ``` 152 | 153 | Search the aggregate with criteria (gt, gte, lt, lte, eq, ne) 154 | 155 | ```js 156 | const critical = cves.search({ 157 | epss:{ gt:0.7 }, 158 | cvss:{ gt:9.0 }, 159 | cisa:{ gte:'2023-09-01' } 160 | }) 161 | //> { 162 | //> 'CVE-2023-24489': { daysUntilDue: -14, cisa: '2023-09-06', epss: 0.97441, cvss: 9.8 }, 163 | //> 'CVE-2023-38035': { daysUntilDue: -8, cisa: '2023-09-12', epss: 0.96013, cvss: 9.8 }, 164 | //> 'CVE-2023-33246': { daysUntilDue: 7, cisa: '2023-09-27', epss: 0.97146, cvss: 9.8 }, 165 | //> 'CVE-2021-3129': { daysUntilDue: 19, cisa: '2023-10-09', epss: 0.97515, cvss: 9.8 } 166 | //> } 167 | ``` 168 | 169 | 170 | ## Calculations 171 | 172 | The aggregate uses CVSS vectors and calculates the CVSS scores as needed 173 | 174 | This allows the ability to manipulate vectors with optional temporal and environmental metrics 175 | 176 | ```js 177 | //Calculate a CVSSv2 vector details 178 | const cvss2 = cves.calculateCVSS("AV:N/AC:L/Au:N/C:C/I:C/A:C") 179 | //> { 180 | //> baseMetricScore: 7.2, 181 | //> baseSeverity: 'High', 182 | //> baseImpact: 10.00084536, 183 | //> baseExploitability: 4.1086848, 184 | //> temporalMetricScore: 7.2, 185 | //> temporalSeverity: 'High', 186 | //> environmentalMetricScore: 7.2, 187 | //> environmentalSeverity: 'High', 188 | //> environmentalModifiedImpact: 10, 189 | //> metricValues: { AV:'N', AC:'L', Au:'N', C:'C', I:'C', A:'C' }, 190 | //> vectorString: 'AV:N/AC:L/Au:N/C:C/I:C/A:C/E:ND/RL:ND/RC:ND/CDP:ND/TD:ND/CR:ND/IR:ND/AR:ND', 191 | //> version: 'CVSS:2' 192 | //> } 193 | 194 | //Calculate a CVSSv3 vector details 195 | const cvss3 = cves.calculateCVSS("CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H") 196 | //> { 197 | //> baseMetricScore: 7.8, 198 | //> baseSeverity: 'High', 199 | //> baseImpact: 5.873118720000001, 200 | //> baseExploitability: 1.8345765900000002, 201 | //> temporalMetricScore: 7.8, 202 | //> temporalSeverity: 'High', 203 | //> environmentalMetricScore: 7.8, 204 | //> environmentalSeverity: 'High', 205 | //> environmentalModifiedImpact: 5.873118720000001, 206 | //> metricValues: { AV:'L', AC:'L', PR:'N', UI:'R', S:'U', C:'H', I:'H', A:'H' }, 207 | //> vectorString: 'CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H/E:X/RL:X/RC:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MPR:X/MUI:X/MS:X/MC:X/MI:X/MA:X', 208 | //> version: 'CVSS:3.1' 209 | //> } 210 | ``` 211 | 212 | ## Aggregate structure 213 | 214 | Example of the aggregated cves.json 215 | 216 | ```json 217 | { 218 | "lastUpdated": "2023-08-31T14:41:33.076Z", 219 | "cvesUpdated": null, 220 | "cisaUpdated": "2023-08-31T14:35:31.532Z", 221 | "epssUpdated": "2023-08-31T14:41:33.076Z", 222 | "cvssUpdated": "2023-08-31T14:49:33.076Z", 223 | "lastCount": 216857, 224 | "cves": { 225 | "CVE-2018-4939": { 226 | "days": 181, 227 | "cisa": "2022-05-03", 228 | "epss": 0.97236, 229 | "cvss2": "AV:N/AC:L/Au:N/C:C/I:C/A:C", 230 | "cvss3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 231 | }, 232 | "CVE-2018-4878": { 233 | "days": 181, 234 | "cisa": "2022-05-03", 235 | "epss": 0.9742, 236 | "cvss2": "AV:N/AC:L/Au:N/C:P/I:P/A:P", 237 | "cvss3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" 238 | }, 239 | ... 240 | } 241 | } 242 | ``` -------------------------------------------------------------------------------- /src/cvss.js: -------------------------------------------------------------------------------- 1 | export class CVSS2 { 2 | #vectorRegex = /((AV:[NAL]|AC:[LHM]|Au:[MSN]|[CIA]:[NPC]|E:[ND|U|POC|F|H]{1,3}|RL:[ND|OF|TF|W|U]{1,2}|RC:[ND|UC|UR|C]{1,2}|CDP:[ND|N|L|LM|MH|H]{1,2}|TD:[ND|N|L|M|H]{1,2}|CR:[ND|L|M|H]{1,2}|IR:[ND|L|M|H]{1,2}|AR:[ND|L|M|H]{1,2})\/)*(AV:[NAL]|AC:[LHM]|Au:[MSN]|[CIA]:[NPC]|E:[ND|U|POC|F|H]{1,3}|RL:[ND|OF|TF|W|U]{1,2}|RC:[ND|UC|UR|C]{1,2}|CDP:[ND|N|L|LM|MH|H]{1,2}|TD:[ND|N|L|M|H]{1,2}|CR:[ND|L|M|H]{1,2}|IR:[ND|L|M|H]{1,2}|AR:[ND|L|M|H]{1,2})/ 3 | #vectorPattern = /[A-Za-z]{1,3}:[A-Za-z]{1,3}/ig 4 | 5 | constructor(options={}) { 6 | this.version = options.version || "CVSS:2" 7 | this.exploitabilityCoefficient = options.exploitabilityCoefficient || 8.22 8 | this.baseKeys = options.baseKeys || ["AV","AC","Au","C","I","A"] 9 | this.temporalKeys = options.temporalKeys || ["E","RL","RC"] 10 | this.environmentKeys = options.environmentKeys || ["CDP","TD","CR","IR","AR"] 11 | this.weight = { 12 | AV: { L:0.395, A:0.646, N:1.0 }, 13 | AC: { H:0.35, M:0.61, L:0.71 }, 14 | Au: { M:0.45, S:0.56, N:0.704 }, 15 | C: { N:0, P:0.275, C:0.660 }, 16 | I: { N:0, P:0.275, C:0.660 }, 17 | A: { N:0, P:0.275, C:0.660 }, 18 | E: { ND:1, U:0.85, POC:0.9, F:0.95, H:1 }, 19 | RL: { ND:1, OF:0.97, TF:0.9, W:0.95, U:1 }, 20 | RC: { ND:1, UC:0.9, UR:0.95, C:1 }, 21 | CDP:{ ND:0, N:0, L:0.1, LM:0.3, MH:0.4, H:0.5 }, 22 | TD: { ND:1, N:0, L:0.25, M:0.75, H:1 }, 23 | CR: { ND:1, L:0.5, M:1, H:1.51 }, 24 | IR: { ND:1, L:0.5, M:1, H:1.51 }, 25 | AR: { ND:1, L:0.5, M:1, H:1.51 }, 26 | } 27 | this.severityRatings = options.severityRatings || [ 28 | { name: "None", bottom: 0.0, top: 0.0 }, 29 | { name: "Low", bottom: 0.1, top: 3.9 }, 30 | { name: "Medium", bottom: 4.0, top: 6.9 }, 31 | { name: "High", bottom: 7.0, top: 8.9 }, 32 | { name: "Critical", bottom: 9.0, top: 10.0 } 33 | ] 34 | 35 | this.metricKeys = this.baseKeys.concat( this.temporalKeys ).concat( this.environmentKeys ) 36 | this.metricNames = { 37 | AV: 'Attack Vector', 38 | AC: 'Attack Complexity', 39 | Au: 'Authentication', 40 | C: 'Confidentiality', 41 | I: 'Integrity', 42 | A: 'Availability', 43 | E: 'Exploitability', 44 | RL: 'Remediation Level', 45 | RC: 'Report Confidence', 46 | CDP: 'Collateral Damage Potential', 47 | TD: 'Target Distribution', 48 | CR: 'Confidentiality Requirement', 49 | IR: 'Integrity Requirement', 50 | AR: 'Availability Requirement', 51 | } 52 | this.metricValues = { 53 | AV: { N: "NETWORK", A: "ADJACENT_NETWORK", L: "LOCAL" }, 54 | AC: { H: "HIGH", M: "MEDIUM", L: "LOW" }, 55 | Au: { N: "NONE", S: "SINGLE", M: "MULTIPLE" }, 56 | C: { N: "NONE", P: "PARTIAL", C: "COMPLETE" }, 57 | I: { N: "NONE", P: "PARTIAL", C: "COMPLETE" }, 58 | A: { N: "NONE", P: "PARTIAL", C: "COMPLETE" }, 59 | E: { ND: "NOT_DEFINED", U: "UNPROVEN", POC: "PROOF_OF_CONCEPT", F: "FUNCTIONAL", H: "HIGH" }, 60 | RL: { ND: "NOT_DEFINED", OF: "OFFICIAL_FIX", TF: "TEMPORARY_FIX", W: "WORKAROUND", U: "UNAVAILABLE" }, 61 | RC: { ND: "NOT_DEFINED", UC: "UNCONFIRMED", UR: "UNCORROBORATED", C: "CONFIRMED" }, 62 | CDP: { ND: "NOT_DEFINED", N: "NONE", L: "LOW", LM: "LOW_MEDIUM", M: "MEDIUM", H: "HIGH" }, 63 | TD: { ND: "NOT_DEFINED", N: "NONE", L: "LOW", M: "MEDIUM", H: "HIGH" }, 64 | CR: { ND: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 65 | IR: { ND: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 66 | AR: { ND: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 67 | } 68 | } 69 | 70 | error(reason) { 71 | return { 72 | baseMetricScore: 0, 73 | baseSeverity: null, 74 | baseImpact: 0, 75 | baseExploitability: 0, 76 | temporalMetricScore: 0, 77 | temporalSeverity: null, 78 | environmentalMetricScore: 0, 79 | environmentalSeverity: null, 80 | environmentalModifiedImpact: 0, 81 | vectorValues:{}, 82 | vectorString:reason, 83 | version:this.version 84 | } 85 | } 86 | 87 | severityRating(score) { 88 | const severityRatingLength = this.severityRatings.length 89 | const validatedScore = Number(score) 90 | if( isNaN(validatedScore) ) 91 | return validatedScore 92 | 93 | for( let i = 0; i < severityRatingLength; i++ ) { 94 | if( score >= this.severityRatings[i].bottom && score <= this.severityRatings[i].top ) 95 | return this.severityRatings[i].name 96 | } 97 | 98 | return undefined 99 | } 100 | 101 | calculateFromMetrics(metricValues) { 102 | const value = (key) => this.weight[key][metricValues[key]||"ND"] 103 | 104 | const impact = 10.41 * (1 - (1 - value('C')) * (1 - value("I")) * (1 - value('A'))) 105 | const exploitability = this.exploitabilityCoefficient * value("AC") * value("Au") * value("AV") 106 | const baseScore = ((0.6 * impact) + (0.4 * exploitability) - 1.5) * (impact === 0 ? 0 : 1.176) 107 | const temporalScore = baseScore * value("E") * value("RL") * value("RC") 108 | const modifiedImpact = Math.min(10, 10.41 * (1 - (1 - value("C") * value("CR")) * (1 - value("I") * value("IR")) * (1 - value("A") * value("AR")))) 109 | const modifiedBase = ((0.6 * modifiedImpact) + (0.4 * exploitability) - 1.5) * (modifiedImpact === 0 ? 0 : 1.176) 110 | const modifiedTemporal = modifiedBase * value("E") * value("RL") * value("RC") 111 | const envScore = (modifiedTemporal + (10 - modifiedTemporal) * value("CDP")) * value("TD") 112 | 113 | const vectorString = this.baseKeys 114 | .concat( this.temporalKeys ) 115 | .concat( this.environmentKeys ) 116 | .map(key => `${key}:${metricValues[key]||"ND"}`) 117 | .join("/") 118 | 119 | return { 120 | baseMetricScore: Number(baseScore.toFixed(1)), 121 | baseSeverity: this.severityRating(baseScore.toFixed(1)), 122 | baseImpact: impact, 123 | baseExploitability: exploitability, 124 | temporalMetricScore: Number(temporalScore.toFixed(1)), 125 | temporalSeverity: this.severityRating(temporalScore.toFixed(1)), 126 | environmentalMetricScore: Number(envScore.toFixed(1)), 127 | environmentalSeverity: this.severityRating(envScore.toFixed(1)), 128 | environmentalModifiedImpact: modifiedImpact, 129 | metricValues, 130 | vectorString, 131 | version:this.version, 132 | adjust:(metrics={}) => this.calculateFromMetrics( Object.assign(metricValues, metrics) ) 133 | } 134 | } 135 | 136 | calculateFromVector(vectorString) { 137 | if( !this.#vectorRegex.test(vectorString) ) 138 | return this.error("Malformed V2 Vector String") 139 | 140 | const vectorMatches = vectorString.match(this.#vectorPattern) 141 | const metricValues = vectorMatches.reduce((acc,m) => { 142 | const [key, val] = m.split(':') 143 | if( key && val ) acc[key] = val 144 | return acc 145 | },{}) 146 | 147 | return this.calculateFromMetrics(metricValues) 148 | } 149 | 150 | describe(vectorOrMetrics) { 151 | return typeof vectorOrMetrics === 'string' 152 | ? this.describeVector(vectorOrMetrics) 153 | : this.describeMetrics(vectorOrMetrics) 154 | } 155 | 156 | describeVector(vectorString) { 157 | if( typeof vectorString !== 'string' || !vectorString?.length ) 158 | throw new Error('CVSS vector string required') 159 | 160 | if( !this.#vectorRegex.test(vectorString) ) 161 | return this.error("Malformed V2 Vector String") 162 | 163 | const vectorMatches = vectorString.match(this.#vectorPattern) 164 | const metricValues = vectorMatches.reduce((acc,m) => { 165 | const [key, val] = m.split(':') 166 | if( key && val ) acc[key] = val 167 | return acc 168 | },{}) 169 | 170 | return this.describeMetrics(metricValues) 171 | } 172 | 173 | describeMetrics(metricValues) { 174 | if( typeof metricValues !== 'object' ) 175 | throw new Error('Metrics object required') 176 | 177 | return this.metricKeys.reduce((acc,key) => { 178 | acc[this.metricNames[key]] = this.metricValues[key][metricValues[key]] 179 | return acc 180 | },{}) 181 | 182 | } 183 | } 184 | 185 | export class CVSS3 { 186 | #vectorRegex = /CVSS:3(\.\d){0,1}\/((AV:[NALP]|AC:[LH]|PR:[UNLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XUNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])\/)*(AV:[NALP]|AC:[LH]|PR:[UNLH]|UI:[NR]|S:[UC]|[CIA]:[NLH]|E:[XUPFH]|RL:[XOTWU]|RC:[XURC]|[CIA]R:[XLMH]|MAV:[XNALP]|MAC:[XLH]|MPR:[XUNLH]|MUI:[XNR]|MS:[XUC]|M[CIA]:[XNLH])/ 187 | 188 | constructor(options={}) { 189 | this.version = options.version || "CVSS:3.1" 190 | this.exploitabilityCoefficient = options.exploitabilityCoefficient || 8.22 191 | this.scopeCoefficient = options.scopeCoefficient || 1.08 192 | this.weight = options.weight || { 193 | AV: { N: 0.85, A: 0.62, L: 0.55, P: 0.2 }, 194 | AC: { H: 0.44, L: 0.77 }, 195 | PR: { 196 | U: { N: 0.85, L: 0.62, H: 0.27 }, 197 | C: { N: 0.85, L: 0.68, H: 0.5 } 198 | }, 199 | UI: { N: 0.85, R: 0.62 }, 200 | S: { U: 6.42, C: 7.52 }, 201 | CIA: { N: 0, L: 0.22, H: 0.56 }, 202 | E: { X: 1, U: 0.91, P: 0.94, F: 0.97, H: 1 }, 203 | RL: { X: 1, O: 0.95, T: 0.96, W: 0.97, U: 1 }, 204 | RC: { X: 1, U: 0.92, R: 0.96, C: 1 }, 205 | CIAR: { X: 1, L: 0.5, M: 1, H: 1.5 } 206 | } 207 | this.severityRatings = options.severityRatings || [ 208 | { name: "None", bottom: 0.0, top: 0.0 }, 209 | { name: "Low", bottom: 0.1, top: 3.9 }, 210 | { name: "Medium", bottom: 4.0, top: 6.9 }, 211 | { name: "High", bottom: 7.0, top: 8.9 }, 212 | { name: "Critical", bottom: 9.0, top: 10.0 } 213 | ] 214 | this.metricKeys = ['AV','AC','PR','UI','S','C','I','A','E','RL','RC','CR','IR','AR','MAV','MAC','MPR','MUI','MS','MC','MI','MA'] 215 | this.metricNames = { 216 | AV: 'Attack Vector', 217 | AC: 'Attack Complexity', 218 | PR: 'Privileges Required', 219 | UI: 'User Interaction', 220 | S: 'Scope', 221 | C: 'Confidentiality', 222 | I: 'Integrity', 223 | A: 'Availability', 224 | E: 'Exploit Code Maturity', 225 | RL: 'Remediation Level', 226 | RC: 'Report Confidence', 227 | CR: 'Confidentiality Requirement', 228 | IR: 'Integrity Requirement', 229 | AR: 'Availability Requirement', 230 | MAV: 'Modified Attack Vector', 231 | MAC: 'Modified Attack Complexity', 232 | MPR: 'Modified Privileges Required', 233 | MUI: 'Modified User Interaction', 234 | MS: 'Modified Scope', 235 | MC: 'Modified Confidentiality', 236 | MI: 'Modified Integrity', 237 | MA: 'Modified Availability' 238 | } 239 | this.metricValues = { 240 | E: { X: "NOT_DEFINED", U: "UNPROVEN", P: "PROOF_OF_CONCEPT", F: "FUNCTIONAL", H: "HIGH" }, 241 | RL: { X: "NOT_DEFINED", O: "OFFICIAL_FIX", T: "TEMPORARY_FIX", W: "WORKAROUND", U: "UNAVAILABLE" }, 242 | RC: { X: "NOT_DEFINED", U: "UNKNOWN", R: "REASONABLE", C: "CONFIRMED" }, 243 | CIAR: { X: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 244 | MAV: { X: "NOT_DEFINED", N: "NETWORK", A: "ADJACENT_NETWORK", L: "LOCAL", P: "PHYSICAL" }, 245 | MAC: { X: "NOT_DEFINED", H: "HIGH", L: "LOW" }, 246 | MPR: { X: "NOT_DEFINED", N: "NONE", L: "LOW", H: "HIGH" }, 247 | MUI: { X: "NOT_DEFINED", N: "NONE", R: "REQUIRED" }, 248 | MS: { X: "NOT_DEFINED", U: "UNCHANGED", C: "CHANGED" }, 249 | MCIA: { X: "NOT_DEFINED", N: "NONE", L: "LOW", H: "HIGH" }, 250 | /*duplicates*/ 251 | C: { X: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 252 | R: { X: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 253 | CR: { X: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 254 | IR: { X: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 255 | AR: { X: "NOT_DEFINED", L: "LOW", M: "MEDIUM", H: "HIGH" }, 256 | AV: { N: "NETWORK", A: "ADJACENT_NETWORK", L: "LOCAL", P: "PHYSICAL" }, 257 | AC: { H: "HIGH", L: "LOW" }, 258 | PR: { N: "NONE", L: "LOW", H: "HIGH" }, 259 | UI: { N: "NONE", R: "REQUIRED" }, 260 | S: { U: "UNCHANGED", C: "CHANGED" }, 261 | I: { L: "LOW", M: "MEDIUM", H: "HIGH" }, 262 | A: { L: "LOW", M: "MEDIUM", H: "HIGH" }, 263 | MC: { X: "NOT_DEFINED", N: "NONE", L: "LOW", H: "HIGH" }, 264 | MI: { X: "NOT_DEFINED", N: "NONE", L: "LOW", H: "HIGH" }, 265 | MA: { X: "NOT_DEFINED", N: "NONE", L: "LOW", H: "HIGH" }, 266 | } 267 | 268 | } 269 | 270 | error(reason, version = this.version) { 271 | return { 272 | baseMetricScore: 0, 273 | baseSeverity: null, 274 | baseImpact: 0, 275 | baseExploitability: 0, 276 | temporalMetricScore: 0, 277 | temporalSeverity: null, 278 | environmentalMetricScore: 0, 279 | environmentalSeverity: null, 280 | environmentalModifiedImpact: 0, 281 | vectorValues:{}, 282 | vectorString:reason, 283 | version 284 | } 285 | } 286 | 287 | roundUp(input) { 288 | const int_input = Math.round(input * 100000) 289 | return int_input % 10000 === 0 290 | ? int_input / 100000 291 | : (Math.floor(int_input / 10000) + 1) / 10 292 | } 293 | 294 | severityRating(score) { 295 | const severityRatingLength = this.severityRatings.length 296 | const validatedScore = Number(score) 297 | if( isNaN(validatedScore) ) 298 | return validatedScore 299 | 300 | for( let i = 0; i < severityRatingLength; i++ ) { 301 | if( score >= this.severityRatings[i].bottom && score <= this.severityRatings[i].top ) 302 | return this.severityRatings[i].name 303 | } 304 | 305 | return undefined 306 | } 307 | 308 | calculateFromMetrics(metricValues, version = this.version) { 309 | const { 310 | AV = null, AC = null, PR = null, UI = null, S = null, C = null, I = null, A = null, 311 | E = 'X', RL = 'X', RC = 'X', CR = 'X',IR = 'X', AR = 'X', 312 | MAV = 'X', MAC = 'X', MPR = 'X', MUI = 'X',MS = 'X', MC = 'X', MI = 'X', MA = 'X' 313 | } = metricValues 314 | 315 | if( !AV || !AC || !PR || !UI || !S || !C || !I || !A ) 316 | return this.error("Malformed V3.x Metrics") 317 | 318 | const metricWeightAV = this.weight.AV[AV] 319 | const metricWeightAC = this.weight.AC[AC] 320 | const metricWeightPR = this.weight.PR[S][PR] 321 | const metricWeightUI = this.weight.UI[UI] 322 | const metricWeightS = this.weight.S[S] 323 | const metricWeightC = this.weight.CIA[C] 324 | const metricWeightI = this.weight.CIA[I] 325 | const metricWeightA = this.weight.CIA[A] 326 | const metricWeightE = this.weight.E[E] 327 | const metricWeightRL = this.weight.RL[RL] 328 | const metricWeightRC = this.weight.RC[RC] 329 | const metricWeightCR = this.weight.CIAR[CR] 330 | const metricWeightIR = this.weight.CIAR[IR] 331 | const metricWeightAR = this.weight.CIAR[AR] 332 | const metricWeightMAV = this.weight.AV[MAV !== "X" ? MAV : AV] 333 | const metricWeightMAC = this.weight.AC[MAC !== "X" ? MAC : AC] 334 | const metricWeightMPR = this.weight.PR[MS !== "X" ? MS : S][MPR !== "X" ? MPR : PR] 335 | const metricWeightMUI = this.weight.UI[MUI !== "X" ? MUI : UI] 336 | const metricWeightMS = this.weight.S[MS !== "X" ? MS : S] 337 | const metricWeightMC = this.weight.CIA[MC !== "X" ? MC : C] 338 | const metricWeightMI = this.weight.CIA[MI !== "X" ? MI : I] 339 | const metricWeightMA = this.weight.CIA[MA !== "X" ? MA : A] 340 | 341 | const iss = (1 - ((1 - metricWeightC) * (1 - metricWeightI) * (1 - metricWeightA))) 342 | const impact = S === 'U' 343 | ? metricWeightS * iss 344 | : metricWeightS * (iss - 0.029) - 3.25 * Math.pow(iss - 0.02, 15) 345 | 346 | const exploitability = this.exploitabilityCoefficient * metricWeightAV * metricWeightAC * metricWeightPR * metricWeightUI 347 | const baseScore = impact <= 0 ? 0 : S === 'U' 348 | ? this.roundUp(Math.min((exploitability + impact), 10)) 349 | : this.roundUp(Math.min(this.scopeCoefficient * (exploitability + impact), 10)) 350 | 351 | const temporalScore = this.roundUp(baseScore * metricWeightE * metricWeightRL * metricWeightRC) 352 | const miss = Math.min(1 - ((1 - metricWeightMC * metricWeightCR) * (1 - metricWeightMI * metricWeightIR) * (1 - metricWeightMA * metricWeightAR)), 0.915) 353 | const modifiedImpact = MS === "U" || (MS === "X" && S === "U") 354 | ? metricWeightMS * miss 355 | : metricWeightMS * (miss - 0.029) - 3.25 * Math.pow(miss * 0.9731 - 0.02, 13) 356 | 357 | const modifiedExploitability = this.exploitabilityCoefficient * metricWeightMAV * metricWeightMAC * metricWeightMPR * metricWeightMUI 358 | const envScore = modifiedImpact <= 0 ? 0 : MS === "U" || (MS === "X" && S === "U") 359 | ? this.roundUp(this.roundUp(Math.min((modifiedImpact + modifiedExploitability), 10)) * metricWeightE * metricWeightRL * metricWeightRC) 360 | : this.roundUp(this.roundUp(Math.min(this.scopeCoefficient * (modifiedImpact + modifiedExploitability), 10)) * metricWeightE * metricWeightRL * metricWeightRC) 361 | 362 | const vectorString = version + "/AV:" + AV + "/AC:" + AC + "/PR:" + PR + "/UI:" + UI + "/S:" + S + "/C:" + C + "/I:" + I + "/A:" + A + "/E:" + E + "/RL:" + RL + "/RC:" + RC + "/CR:" + CR + "/IR:" + IR + "/AR:" + AR + "/MAV:" + MAV + "/MAC:" + MAC + "/MPR:" + MPR + "/MUI:" + MUI + "/MS:" + MS + "/MC:" + MC + "/MI:" + MI + "/MA:" + MA 363 | 364 | return { 365 | baseMetricScore: Number(baseScore.toFixed(1)), 366 | baseSeverity: this.severityRating(baseScore.toFixed(1)), 367 | baseImpact: impact, 368 | baseExploitability: exploitability, 369 | temporalMetricScore: Number(temporalScore.toFixed(1)), 370 | temporalSeverity: this.severityRating(temporalScore.toFixed(1)), 371 | environmentalMetricScore: Number(envScore.toFixed(1)), 372 | environmentalSeverity: this.severityRating(envScore.toFixed(1)), 373 | environmentalModifiedImpact: modifiedImpact, 374 | metricValues, 375 | vectorString, 376 | version, 377 | adjust:(metrics={}) => this.calculateFromMetrics( Object.assign(metricValues, metrics), version ) 378 | } 379 | } 380 | 381 | calculateFromVector(vectorString) { 382 | if( !this.#vectorRegex.test(vectorString) ) 383 | return this.error("Malformed V3.x Vector String") 384 | 385 | const version = vectorString.match(/CVSS:3(\.\d){0,1}/)[0] 386 | const metricNameValue = vectorString.substring(version.length).split("/").slice(1) 387 | const metricValues = {} 388 | 389 | for( const i in metricNameValue ) { 390 | if( !metricNameValue.hasOwnProperty(i) ) continue 391 | const singleMetric = metricNameValue[i].split(":") 392 | metricValues[singleMetric[0]] = singleMetric[1] 393 | } 394 | 395 | return this.calculateFromMetrics(metricValues, version) 396 | } 397 | 398 | describe(vectorOrMetrics) { 399 | return typeof vectorOrMetrics === 'string' 400 | ? this.describeVector(vectorOrMetrics) 401 | : this.describeMetrics(vectorOrMetrics) 402 | } 403 | 404 | describeVector(vectorString) { 405 | if( typeof vectorString !== 'string' || !vectorString?.length ) 406 | throw new Error('CVSS vector string required') 407 | 408 | if( !this.#vectorRegex.test(vectorString) ) 409 | return this.error("Malformed V3.x Vector String") 410 | 411 | const version = vectorString.match(/CVSS:3(\.\d){0,1}/)[0] 412 | const metricNameValue = vectorString.substring(version.length).split("/").slice(1) 413 | const metricValues = {} 414 | 415 | for( const i in metricNameValue ) { 416 | if( !metricNameValue.hasOwnProperty(i) ) continue 417 | const singleMetric = metricNameValue[i].split(":") 418 | metricValues[singleMetric[0]] = singleMetric[1] 419 | } 420 | 421 | return this.describeMetrics(metricValues, version) 422 | } 423 | 424 | describeMetrics(metricValues, version = this.version) { 425 | if( typeof metricValues !== 'object' ) 426 | throw new Error('Metrics object required') 427 | 428 | if( !metricValues.AV || !metricValues.AC || !metricValues.PR || !metricValues.UI || !metricValues.S || !metricValues.C || !metricValues.I || !metricValues.A ) 429 | return this.error("Malformed V3.x Metrics") 430 | 431 | return this.metricKeys.reduce((acc,key) => { 432 | acc[this.metricNames[key]] = this.metricValues[key][metricValues[key]] 433 | return acc 434 | },{}) 435 | } 436 | } 437 | 438 | export class CVSS { 439 | constructor() { 440 | this.v2 = new CVSS2() 441 | this.v3 = new CVSS3() 442 | } 443 | 444 | calculate(vectorOrMetrics) { 445 | vectorOrMetrics = vectorOrMetrics || '' 446 | return typeof vectorOrMetrics === 'string' 447 | ? this.calculateFromVector(vectorOrMetrics) 448 | : this.calculateFromMetrics(vectorOrMetrics) 449 | } 450 | calculateFromVector(vectorString = '') { 451 | return !vectorString?.startsWith?.('CVSS:3') || vectorString?.match?.(/Au:[MSN]/) 452 | ? this.v2.calculateFromVector(vectorString) 453 | : this.v3.calculateFromVector(vectorString) 454 | } 455 | calculateFromMetrics(metricValues = {}) { 456 | return metricValues?.hasOwnProperty?.("Au") 457 | ? this.v2.calculateFromMetrics(metricValues) 458 | : this.v3.calculateFromMetrics(metricValues) 459 | } 460 | 461 | describe(vectorOrMetrics) { 462 | vectorOrMetrics = vectorOrMetrics || '' 463 | return typeof vectorOrMetrics === 'string' 464 | ? this.describeVector(vectorOrMetrics) 465 | : this.describeMetrics(vectorOrMetrics) 466 | } 467 | describeVector(vectorString = '') { 468 | return !vectorString?.startsWith?.('CVSS:3') || vectorString?.match?.(/Au:[MSN]/) 469 | ? this.v2.describeVector(vectorString) 470 | : this.v3.describeVector(vectorString) 471 | } 472 | describeMetrics(metricValues = {}) { 473 | return metricValues?.hasOwnProperty?.("Au") 474 | ? this.v2.describeMetrics(metricValues) 475 | : this.v3.describeMetrics(metricValues) 476 | } 477 | } 478 | 479 | -------------------------------------------------------------------------------- /src/helpers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get the difference between two dates (in days) 3 | * @param {Date} date1 4 | * @param {Date} date2 Optional second date to use instead of current date 5 | * @return {number} Difference in days (floating point) 6 | */ 7 | export const diffInDays = (date1, date2 = Date.now()) => { 8 | const last = new Date(date1) 9 | const now = new Date(date2) 10 | const Difference_In_Time = now.getTime() - last.getTime() 11 | const Difference_In_Days = Difference_In_Time / (1000 * 3600 * 24) 12 | return Difference_In_Days 13 | } 14 | 15 | /** 16 | * Compare a value against a condition (optional format func) 17 | * @param {*} val The value from the aggregate 18 | * @param {object} option The condition to compare with 19 | * @param {func} format Optional formatting function for normalizing both sides of the condition 20 | * @return {bool} Whether the value matches the condition 21 | */ 22 | export const compare = (val, option, format) => { 23 | const key = Object.keys(option||{})[0] 24 | return !key ? false : typeof format === 'function' 25 | ? compareFunc[key]( val === null ? val : format(val), format(option[key]) ) 26 | : compareFunc[key]( val, option[key] ) 27 | } 28 | 29 | /** 30 | * Comparison functions mapped by key (gt,gte,lt,lte,eq,ne,neq) 31 | */ 32 | export const compareFunc = { 33 | gt: (v1, v2) => v1 > v2, //Greater than 34 | gte:(v1, v2) => v1 >= v2, //Greater than, or equal 35 | lt: (v1, v2) => v1 < v2, //Less than 36 | lte:(v1, v2) => v1 <= v2, //Less than, or equal 37 | eq: (v1, v2) => v1 === v2, //Is equal 38 | ne: (v1, v2) => v1 !== v2, //Not equal 39 | neq:(v1, v2) => v1 !== v2, //Same as ne 40 | } 41 | 42 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { writeFileSync, readFileSync, existsSync } from 'fs' 2 | import { fileURLToPath } from 'url' 3 | import { join, dirname } from 'path' 4 | 5 | const __dirname = dirname(fileURLToPath(import.meta.url)) 6 | const { CVSS } = await import(join(__dirname, 'cvss.js')) 7 | const { Logger } = await import(join(__dirname, 'logger.js')) 8 | const { diffInDays, compare } = await import(join(__dirname, 'helpers.js')) 9 | 10 | export class CVEAggregate { 11 | #urlCVES = "https://cve.mitre.org/data/downloads/allitems.csv" 12 | #urlCISA = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json" 13 | #urlEPSS = "https://api.first.org/data/v1/epss" 14 | #urlCVSS = "https://services.nvd.nist.gov/rest/json/cves/2.0" 15 | #CVSS = new CVSS() 16 | #weight = { cisa:0.8, epss:0.8, cvss:0.5 } 17 | 18 | constructor(filepath, verbose = false){ 19 | this.filepath = filepath?.length ? filepath : null //join(__dirname, 'cves.json') 20 | this.verbose = verbose 21 | this.logger = verbose ? new Logger() : { log:() => {} } 22 | this.cves = {} 23 | this.lastUpdated = null 24 | this.cvesUpdated = null 25 | this.cisaUpdated = null 26 | this.epssUpdated = null 27 | this.cvssUpdated = null 28 | this.lastCount = null 29 | this.daysdiff = 0.1 //Skip epss and cvss update if less than this many days since last update 30 | this.load() 31 | } 32 | 33 | /** 34 | * Log to console 35 | * @param {array} lines a list of strings to replace the last n-lines in console if the last log was array 36 | * @param {Error} lines an error object to throw in console 37 | * @param {any} lines any other value/type to log in console 38 | */ 39 | log(...lines) { 40 | this.logger.log(...lines) 41 | } 42 | 43 | /** 44 | * Dump internal details 45 | * @return {object} contents of the current cve json 46 | */ 47 | dump() { 48 | return { 49 | lastUpdated:this.lastUpdated, 50 | cvesUpdated:this.cvesUpdated, 51 | cisaUpdated:this.cisaUpdated, 52 | epssUpdated:this.epssUpdated, 53 | cvssUpdated:this.cvssUpdated, 54 | lastCount:Object.keys(this.cves).length, 55 | cves:this.cves, 56 | } 57 | } 58 | 59 | /** 60 | * Save current cves to filepath 61 | */ 62 | save(filepath = this.filepath) { 63 | this.lastUpdated = (new Date()).toISOString() 64 | if( filepath?.length ) 65 | writeFileSync(filepath, JSON.stringify(this.dump()), 'utf8') 66 | } 67 | 68 | /** 69 | * Load cves from a filepath 70 | */ 71 | load(filepath = this.filepath) { 72 | try { 73 | if( !existsSync(filepath) ) throw new Error('No cve list') 74 | //Load the content of last save 75 | const json = JSON.parse(readFileSync(filepath, 'utf8')) 76 | this.cves = json.cves || this.cves 77 | this.lastUpdated = json.lastUpdated || this.lastUpdated 78 | this.lastCount = json.lastCount || this.lastCount 79 | this.cvesUpdated = json.cvesUpdated || this.cvesUpdated 80 | this.cisaUpdated = json.cisaUpdated || this.cisaUpdated 81 | this.epssUpdated = json.epssUpdated || this.epssUpdated 82 | this.cvssUpdated = json.cvssUpdated || this.cvssUpdated 83 | } catch(e) { 84 | //No file or error loading, create fresh 85 | this.cves = this.cves || {} 86 | this.lastCount = this.lastCount || null 87 | this.resetTimestamps() 88 | } 89 | this.resetCounters() 90 | } 91 | 92 | resetCounters() { 93 | //reset the new item counters 94 | this.newCVES = new Set() 95 | this.newCISA = new Set() 96 | this.newEPSS = new Set() 97 | this.newCVSS2 = new Set() 98 | this.newCVSS3 = new Set() 99 | } 100 | 101 | resetTimestamps() { 102 | this.lastUpdated = null 103 | this.cvesUpdated = null 104 | this.cisaUpdated = null 105 | this.epssUpdated = null 106 | this.cvssUpdated = null 107 | } 108 | 109 | /** 110 | * Report update details since last load 111 | * @return {object} collection of data details 112 | */ 113 | report(reportZero) { 114 | if( reportZero || this.newCVES.size ) this.logger.log(`Found ${this.newCVES.size.toLocaleString()} new CVEs`) 115 | if( reportZero || this.newCISA.size ) this.logger.log(`Found ${this.newCISA.size.toLocaleString()} new CISA entries`) 116 | if( reportZero || this.newEPSS.size ) this.logger.log(`Found ${this.newEPSS.size.toLocaleString()} new EPSS scores`) 117 | if( reportZero || this.newCVSS2.size ) this.logger.log(`Found ${this.newCVSS2.size.toLocaleString()} new CVSSv2 vectors`) 118 | if( reportZero || this.newCVSS3.size ) this.logger.log(`Found ${this.newCVSS3.size.toLocaleString()} new CVSSv3 vectors`) 119 | //If anything found, add a divider line 120 | if( reportZero || this.newCVES.size || this.newCISA.size || this.newEPSS.size || this.newCVSS2.size || this.newCVSS3.size ) 121 | this.logger.log(`-`.repeat(30)) 122 | 123 | //Collect and return details in one object 124 | const data = this.dump() 125 | 126 | data.newCVES = this.newCVES 127 | data.newCISA = this.newCISA 128 | data.newEPSS = this.newEPSS 129 | data.newCVSS2 = this.newCVSS2 130 | data.newCVSS3 = this.newCVSS3 131 | data.totalCVES = Object.keys(this.cves).length 132 | data.totalCISA = Object.values(this.cves).filter(i => i.cisa).length 133 | data.totalEPSS = Object.values(this.cves).filter(i => i.epss).length 134 | data.totalCVSS = Object.values(this.cves).filter(i => i.cvss2 || i.cvss3).length 135 | 136 | this.logger.log(`Total CVEs: ${data.totalCVES.toLocaleString()}`) 137 | this.logger.log(`Total CISA entries: ${data.totalCISA.toLocaleString()}`) 138 | this.logger.log(`Total EPSS scores: ${data.totalEPSS.toLocaleString()}`) 139 | this.logger.log(`Total CVSS vectors: ${data.totalCVSS.toLocaleString()}`) 140 | 141 | return data 142 | } 143 | 144 | /* ACCESSORS */ 145 | 146 | /** 147 | * Search one or more CVEs to see if they're in the CISA KEV 148 | * @param {...string} cveIds CVE ids to search against 149 | * @return {bool} true if any cve is in CISA 150 | */ 151 | getCISA(...cveIds) { 152 | for(const cveId of cveIds) { 153 | if( this.cves[cveId]?.cisa?.length ) return true 154 | } 155 | return false 156 | } 157 | 158 | /** 159 | * Get the scaled EPSS score of one or more CVEs 160 | * @param {...string} cveIds CVE ids to search against 161 | * @return {number} EPSS score 162 | */ 163 | getEPSS(...cveIds) { 164 | return (1 - cveIds.map(cveId => this.cves[cveId]?.epss || 0).filter(v => v).reduce((p,v) => p * (1-v),1)) 165 | } 166 | 167 | /** 168 | * Get the maximum CVSS score of one or more CVEs 169 | * @param {...string} cveIds CVE ids to search against 170 | * @return {number} CVSS score 171 | */ 172 | getCVSS(...cveIds) { 173 | return cveIds.reduce((max,cveId) => { 174 | const score = this.#CVSS.calculateFromVector(this.cves[cveId]?.cvss3 || this.cves[cveId]?.cvss2) 175 | return Math.max(max, score.environmentalMetricScore) 176 | },0) 177 | } 178 | 179 | /* MAPPERS */ 180 | 181 | /** 182 | * Map the CISA due dates to one or more CVEs 183 | * @param {...string} cveIds CVE ids to search against 184 | * @return {object} due dates keyed by CVE id 185 | */ 186 | mapCISA(...cveIds) { 187 | return cveIds.reduce((map,cveId) => { 188 | map[cveId] = this.cves[cveId]?.cisa || null 189 | return map 190 | },{}) 191 | } 192 | 193 | /** 194 | * Map the EPSS score to one or more CVEs 195 | * @param {...string} cveIds CVE ids to search against 196 | * @return {object} EPSS scores keyed by CVE id 197 | */ 198 | mapEPSS(...cveIds) { 199 | return cveIds.reduce((map,cveId) => { 200 | map[cveId] = this.cves[cveId]?.epss || 0 201 | return map 202 | },{}) 203 | } 204 | 205 | /** 206 | * Map the CVSS score to one or more CVEs (v3 if exists, else v2) 207 | * @param {...string} cveIds CVE ids to search against 208 | * @return {object} CVSS scores keyed by CVE id 209 | */ 210 | mapCVSS(...cveIds) { 211 | return cveIds.reduce((map,cveId) => { 212 | map[cveId] = this.cves[cveId]?.cvss3 || this.cves[cveId]?.cvss2 || null 213 | return map 214 | },{}) 215 | } 216 | 217 | /* PARSERS */ 218 | 219 | /** 220 | * Parse a line from the CVE-CSV - looking for CVE ids 221 | * @param {string} line one record from the CVE.csv 222 | */ 223 | parseCSVLine(line) { 224 | if( !line.length ) return 225 | 226 | const cveId = line?.match?.(/^(CVE-\d{4}-\d{4,})\s*,/)?.[1] 227 | if( !cveId?.length ) return 228 | if( !(cveId in this.cves) ) { 229 | this.cves[cveId] = { days:0, cisa:null, epss:0, cvss2:null, cvss3:null } 230 | this.newCVES.add(cveId) 231 | } 232 | } 233 | 234 | /** 235 | * Parse the due date from CISA entry 236 | * @param {object} item Entry from the CISA KEV 237 | */ 238 | parseCISA(item) { 239 | const cveId = item?.cveID 240 | if( !cveId?.length ) return 241 | if( !(cveId in this.cves) ) { 242 | this.cves[cveId] = { days:0, cisa:null, epss:0, cvss2:null, cvss3:null } 243 | this.newCVES.add(cveId) 244 | } 245 | 246 | if( this.cves[cveId].cisa === item.dueDate ) 247 | return //Already the same 248 | 249 | if( !this.cves[cveId].cisa ) { 250 | this.newCISA.add(cveId) 251 | } 252 | this.cves[cveId].cisa = item.dueDate 253 | this.cves[cveId].days = diffInDays(item.dateAdded, item.dueDate) 254 | } 255 | 256 | /** 257 | * Parse the EPSS score from first.org response item 258 | * @param {object} item Entry from the EPSS response 259 | */ 260 | parseEPSS(item) { 261 | const cveId = item?.cve 262 | if( !cveId?.length ) return 263 | if( !(cveId in this.cves) ) { 264 | this.cves[cveId] = { days:0, cisa:null, epss:0, cvss2:null, cvss3:null } 265 | this.newCVES.add(cveId) 266 | } 267 | 268 | if( this.cves[cveId].epss === item.epss ) 269 | return //Already the same 270 | 271 | if( !this.cves[cveId].epss ) { 272 | this.newEPSS.add(cveId) 273 | } 274 | this.cves[cveId].epss = Number(item.epss) 275 | } 276 | 277 | /** 278 | * Parse the CVSS scores from first.org response item 279 | * @param {object} item Entry from the CVSS response 280 | */ 281 | parseCVSS(item) { 282 | const cveId = item?.cve?.id 283 | if( !cveId?.length ) return 284 | if( !(cveId in this.cves) ) { 285 | this.cves[cveId] = { days:0, cisa:null, epss:0, cvss2:null, cvss3:null } 286 | this.newCVES.add(cveId) 287 | } 288 | 289 | const { cvssMetricV2, cvssMetricV31 } = item?.cve?.metrics || {} 290 | 291 | const v2vector = cvssMetricV2?.[0]?.cvssData?.vectorString 292 | if( v2vector?.length && this.cves[cveId].cvss2 !== v2vector ) { 293 | if( !this.cves[cveId].cvss2 ) this.newCVSS2.add(cveId) 294 | this.cves[cveId].cvss2 = v2vector 295 | } 296 | 297 | const v3vector = cvssMetricV31?.[0]?.cvssData?.vectorString 298 | if( v3vector?.length && this.cves[cveId].cvss3 !== v3vector ) { 299 | if( !this.cves[cveId].cvss3 ) this.newCVSS3.add(cveId) 300 | this.cves[cveId].cvss3 = v3vector 301 | } 302 | } 303 | 304 | /* UPDATE ROUTINES */ 305 | 306 | /** 307 | * Stream the CVE-CSV from mitre.org and extract new entries 308 | * @param {array} feedback array of console lines for updating feedback 309 | * @param {number} index index of the feedback array for this function 310 | * @return {Promise} 311 | */ 312 | async update_cves (feedback=[], index=0, saveAtEachStage) { 313 | if( this.cvesUpdated?.length && diffInDays(this.cvesUpdated) < this.daysdiff ) 314 | return feedback[index] = `Updating CVEs ... [skip]` 315 | 316 | const https = await import('node:https') 317 | const readline = await import('node:readline') 318 | const cancelRequest = new AbortController() 319 | let fileDate = new Date().toISOString() 320 | return new Promise((resolve, reject) => { 321 | https.get(this.#urlCVES, { signal: cancelRequest.signal }, (res) => { 322 | fileDate = new Date(res.headers['last-modified']).toISOString() 323 | if( fileDate === this.cvesUpdated ) { 324 | feedback[index] = `Updating CVEs ... [skip]` 325 | cancelRequest.abort() 326 | return reject() 327 | } 328 | 329 | let len = 0 330 | feedback[index] = `Updating CVEs ... ` 331 | const size = Number(res.headers['content-length']) 332 | const readStream = readline.createInterface({ input:res }) 333 | readStream.on('close', resolve) 334 | readStream.on('error', reject) 335 | readStream.on("line", line => this.parseCSVLine(line)) 336 | res.on('data', (data) => { 337 | const pct = ((len += data.length) / size * 100).toFixed(1) 338 | feedback[index] = `Updating CVEs ... ${pct}%` 339 | }) 340 | 341 | }) 342 | }) 343 | .then(() => { 344 | feedback[index] = `Updating CVEs ... 100.0%` 345 | this.cvesUpdated = fileDate 346 | }) 347 | .catch(e => this.logger.log(e)) 348 | .finally(() => saveAtEachStage ? this.save() : null) 349 | } 350 | 351 | /** 352 | * Fetch the CISA-KEV from cisa.gov and extract new entries 353 | * @param {array} feedback array of console lines for updating feedback 354 | * @param {number} index index of the feedback array for this function 355 | * @return {Promise} 356 | */ 357 | async update_cisa (feedback=[], index=0, saveAtEachStage) { 358 | if( this.cisaUpdated?.length && diffInDays(this.cisaUpdated) < this.daysdiff ) 359 | return feedback[index] = `Updating CISA ... [skip]` 360 | 361 | let fileDate = new Date().toISOString() 362 | return new Promise(async (resolve, reject) => { 363 | 364 | feedback[index] = `Updating CISA ... ` 365 | const kev = await fetch(this.#urlCISA) 366 | .then(res => res.json()) 367 | .catch(e => this.logger.log(e)) 368 | 369 | const { count = 0, vulnerabilities = [] } = kev || {} 370 | kev?.vulnerabilities?.forEach((item,i) => { 371 | let pct = (i / (count || 1) * 100).toFixed(1) 372 | feedback[index] = `Updating CISA ... ${pct}%` 373 | this.parseCISA(item) 374 | }) 375 | resolve() 376 | 377 | }) 378 | .then(() => { 379 | feedback[index] = `Updating CISA ... 100.0%` 380 | this.cisaUpdated = fileDate 381 | }) 382 | .catch(e => this.logger.log(e)) 383 | .finally(() => saveAtEachStage ? this.save() : null) 384 | } 385 | 386 | /** 387 | * Fetch the EPSS scores from first.org and extract new entries 388 | * @param {array} feedback array of console lines for updating feedback 389 | * @param {number} index index of the feedback array for this function 390 | * @return {Promise} 391 | */ 392 | async update_epss (feedback=[], index=0, saveAtEachStage) { 393 | if( this.epssUpdated?.length && diffInDays(this.epssUpdated) < this.daysdiff ) 394 | return feedback[index] = `Updating EPSS ... [skip]` 395 | 396 | let fileDate = new Date().toISOString() 397 | return new Promise(async (resolve, reject) => { 398 | const lastUpdated = this.epssUpdated 399 | const daysLimit = (() => { 400 | if( !lastUpdated ) return '' 401 | const last = new Date(lastUpdated) 402 | const now = new Date() 403 | const Difference_In_Time = now.getTime() - last.getTime() 404 | const Difference_In_Days = Difference_In_Time / (1000 * 3600 * 24) 405 | const Extra_day = Math.ceil(Difference_In_Days+1) || 1 //extra day 406 | return `&days=${Extra_day}` 407 | })() 408 | 409 | feedback[index] = `Updating EPSS ... ` 410 | const firstBatch = await fetch(`${this.#urlEPSS}?envelope=true${daysLimit}`) 411 | .then(res => res.json()) 412 | .catch(e => this.logger.log(e)) 413 | 414 | const { total = 1, data = [] } = firstBatch || {} 415 | 416 | let offset = data?.length || total || 1 417 | let pct = (offset / (total || 1) * 100).toFixed(1) 418 | 419 | feedback[index] = `Updating EPSS ... ${pct}%` 420 | data?.forEach?.(item => this.parseEPSS(item)) 421 | 422 | let fails = 0 423 | while(total > offset) { 424 | const loopBatch = await fetch(`${this.#urlEPSS}?envelope=true&offset=${offset}${daysLimit}`) 425 | .then(res => res.status < 400 ? res.json() : null) 426 | .catch(e => this.logger.log(e)) 427 | 428 | if( !loopBatch?.data ) { 429 | //Failed more than 5 times 430 | if( ++fails > 5 ) break 431 | feedback[index] = `Updating EPSS ... ${pct}% [Fetch failure (${fails})]` 432 | //Failed - wait 5 seconds and try again 433 | await new Promise(done => setTimeout(() => done(), 5000*fails)) 434 | continue 435 | } else { 436 | fails = 0 437 | offset += loopBatch.data.length || 0 438 | pct = (offset / (total || 1) * 100).toFixed(1) 439 | feedback[index] = `Updating EPSS ... ${pct}% ${' '.repeat(20)}` 440 | loopBatch.data?.forEach?.(item => this.parseEPSS(item)) 441 | } 442 | 443 | if( loopBatch.data.length < loopBatch.limit ) break 444 | } 445 | resolve() 446 | }) 447 | .then(() => { 448 | feedback[index] = `Updating EPSS ... 100.0%` 449 | this.epssUpdated = fileDate 450 | }) 451 | .catch(e => this.logger.log(e)) 452 | .finally(() => saveAtEachStage ? this.save() : null) 453 | } 454 | 455 | /** 456 | * Fetch the CVSS vectors from nist.gov and extract new entries 457 | * @param {array} feedback array of console lines for updating feedback 458 | * @param {number} index index of the feedback array for this function 459 | * @return {Promise} 460 | */ 461 | async update_cvss (feedback=[], index=0, saveAtEachStage) { 462 | if( this.cvssUpdated?.length && diffInDays(this.cvssUpdated) < this.daysdiff ) 463 | return feedback[index] = `Updating CVSS ... [skip]` 464 | 465 | let fileDate = new Date().toISOString() 466 | return new Promise(async (resolve, reject) => { 467 | const lastUpdated = this.cvssUpdated 468 | const daysLimit = (() => { 469 | if( !lastUpdated ) return '' 470 | const lastDate = new Date(lastUpdated) 471 | lastDate.setDate(lastDate.getDate()-1) //extra day 472 | const lastModStartDate = lastDate.toISOString().split('.')[0]+"Z" 473 | const lastModEndDate = new Date().toISOString().split('.')[0]+"Z" 474 | return `&lastModStartDate=${lastModStartDate}&lastModEndDate=${lastModEndDate}` 475 | })() 476 | 477 | feedback[index] = `Updating CVSS ... ` 478 | const firstBatch = await fetch(`${this.#urlCVSS}?startIndex=0&${daysLimit}`) 479 | .then(res => res.json()) 480 | .catch(e => this.logger.log(e)) 481 | 482 | const { resultsPerPage = 0, totalResults = 0, vulnerabilities = [] } = firstBatch || {} 483 | 484 | let offset = vulnerabilities?.length || totalResults || 1 485 | let pct = (offset / (totalResults || 1) * 100).toFixed(1) 486 | 487 | feedback[index] = `Updating CVSS ... ${pct}%` 488 | vulnerabilities?.forEach?.(item => this.parseCVSS(item)) 489 | 490 | let fails = 0 491 | while(totalResults > offset) { 492 | await new Promise(done => setTimeout(() => done(), 4000)) //Fetch throttle 493 | 494 | const loopBatch = await fetch(`${this.#urlCVSS}?startIndex=${offset}${daysLimit}`) 495 | .then(res => res.status < 400 ? res.json() : null) 496 | .catch(e => this.logger.log(e)) 497 | 498 | if( !loopBatch?.vulnerabilities ) { 499 | //Failed more than 5 times 500 | if( ++fails > 5 ) break 501 | feedback[index] = `Updating CVSS ... ${pct}% [Fetch failure (${fails})]` 502 | //Failed - throttle and try again 503 | await new Promise(done => setTimeout(() => done(), 5000*fails)) 504 | continue 505 | } else { 506 | fails = 0 507 | offset += loopBatch.vulnerabilities.length || 0 508 | pct = (offset / (totalResults || 1) * 100).toFixed(1) 509 | feedback[index] = `Updating CVSS ... ${pct}% ${' '.repeat(20)}` 510 | loopBatch.vulnerabilities?.forEach?.(item => this.parseCVSS(item)) 511 | } 512 | 513 | if( loopBatch.vulnerabilities.length < resultsPerPage ) break 514 | } 515 | resolve() 516 | }) 517 | .then(() => { 518 | feedback[index] = `Updating CVSS ... 100.0%` 519 | this.cvssUpdated = fileDate 520 | }) 521 | .catch(e => this.logger.log(e)) 522 | .finally(() => saveAtEachStage ? this.save() : null) 523 | } 524 | 525 | /* Generate aggregate source */ 526 | 527 | /** 528 | * Build with full CVE list 529 | * Includes the CVE csv where some CVEs will not have matching data 530 | * @return {Promise.} 531 | */ 532 | async build(saveAtEachStage) { 533 | const feedback = new Array(4).fill('...') 534 | const interval = setInterval(() => this.logger.log(feedback), 1000) 535 | return Promise.all([ 536 | this.update_cves(feedback, 0, saveAtEachStage), 537 | this.update_cisa(feedback, 1, saveAtEachStage), 538 | this.update_epss(feedback, 2, saveAtEachStage), 539 | this.update_cvss(feedback, 3, saveAtEachStage), 540 | ]) 541 | .then(() => this.cves) 542 | .finally(() => { 543 | clearInterval(interval) 544 | this.logger.log(feedback) 545 | }) 546 | } 547 | async forceBuild(saveAtEachStage) { 548 | this.resetTimestamps() 549 | return this.build(saveAtEachStage) 550 | } 551 | 552 | /** 553 | * Build with only applicable CVEs 554 | * Only generate a list of CVEs that have data 555 | * @return {Promise.} 556 | */ 557 | async update(saveAtEachStage) { 558 | const feedback = new Array(3).fill('...') 559 | const interval = setInterval(() => this.logger.log(feedback), 1000) 560 | return Promise.all([ 561 | this.update_cisa(feedback, 0, saveAtEachStage), 562 | this.update_epss(feedback, 1, saveAtEachStage), 563 | this.update_cvss(feedback, 2, saveAtEachStage), 564 | ]) 565 | .then(() => this.cves) 566 | .finally(() => { 567 | clearInterval(interval) 568 | this.logger.log(feedback) 569 | }) 570 | } 571 | async forceUpdate(saveAtEachStage) { 572 | this.resetTimestamps() 573 | return this.update(saveAtEachStage) 574 | } 575 | 576 | 577 | /* Helper tools and calculators */ 578 | 579 | /** 580 | * Calculate a CVSS scoring from a vector string 581 | * @param {string} vectorOrMetrics The CVSS (v2 or v3) vector string 582 | * @param {object} vectorOrMetrics The CVSS metrics object 583 | * @return {object} calculation results 584 | */ 585 | calculateCVSS(vectorOrMetrics) { 586 | return this.#CVSS.calculate(vectorOrMetrics) 587 | } 588 | 589 | /** 590 | * Describe a CVSS vector or metrics object 591 | * @param {string} vectorOrMetrics The CVSS (v2 or v3) vector string 592 | * @param {object} vectorOrMetrics The CVSS metrics object 593 | * @return {object} 594 | */ 595 | describeCVSS(vectorOrMetrics) { 596 | return this.#CVSS.describe(vectorOrMetrics) 597 | } 598 | 599 | /** 600 | * Search the aggregate with fields and conditions 601 | * ex. search({ epss:{ gt:0.5 } }) 602 | * @param {object} options condition objects keyed by data field 603 | * @return {object} full entries that match the given criteria 604 | */ 605 | search(options={}) { 606 | const critical = {} 607 | for(const cveId in this.cves) { 608 | const { cisa, epss, cvss2, cvss3 } = this.cves[cveId] 609 | 610 | //First: Lightest compare 611 | const matchEPSS = !options?.epss || compare(epss, options?.epss, (v) => Number(v)) 612 | if( !matchEPSS ) continue 613 | 614 | //Second: Date conversions 615 | const matchCISA = !options?.cisa || compare(cisa, options?.cisa, (v) => v ? new Date(v).getTime() : v) 616 | if( !matchCISA ) continue 617 | 618 | //Last: CVSS calculation 619 | const score = this.calculateCVSS(cvss3 || cvss2) 620 | const matchCVSS = !options?.cvss || compare(score.environmentalMetricScore, options?.cvss, (v) => Number(v)) 621 | if( !matchCVSS ) continue 622 | 623 | const daysUntilDue = Math.round(diffInDays(Date.now(), cisa)) 624 | 625 | //If here, we match, return the calculated cvss instead of any vectors 626 | critical[cveId] = { daysUntilDue, cisa, epss, cvss:score.environmentalMetricScore } 627 | } 628 | return critical 629 | } 630 | 631 | /** 632 | * Fetch entries from the aggregate for one or more CVEs 633 | * @param {...string} cveIds CVE ids to search against 634 | * @return {object} full entries for the given CVEs 635 | */ 636 | map(...cveIds) { 637 | return cveIds.reduce((map,cveId) => { 638 | if( cveId in this.cves ) { 639 | map[cveId] = { ...this.cves[cveId] } 640 | } 641 | return map 642 | },{}) 643 | } 644 | 645 | /** 646 | * Fetch entries from the aggregate for one or more CVEs 647 | * @param {...string} cveIds CVE ids to search against 648 | * @return {array} full entries for the given CVEs 649 | */ 650 | list(...cveIds) { 651 | return cveIds.reduce((arr,cveId) => { 652 | if( cveId in this.cves ) { 653 | arr.push({ id:cveId, ...this.cves[cveId] }) 654 | } 655 | return arr 656 | },[]) 657 | } 658 | 659 | /** 660 | * Get list of all CVE Ids 661 | * @return {string[]} list of CVE ids 662 | */ 663 | cveList() { 664 | return Object.keys(this.cves) 665 | } 666 | 667 | /** 668 | * Search one or more CVEs to check risk 669 | * @param {...string} cveIds CVE ids to search against 670 | * @return {object} risk 671 | */ 672 | check(...cveIds) { 673 | const peak = { cisa:0, epss:0, cvss:0 } 674 | const list = { cisa:[], epss:[], cvss:[] } 675 | for(const cveId of cveIds) { 676 | const ref = this.cves[cveId] 677 | if( !ref ) continue 678 | if( ref.cisa ) { 679 | const days = Math.round(diffInDays(Date.now(), ref.cisa)) 680 | if( days <= 0 ) list.cisa.push({date:ref.cisa, days, risk:100}) 681 | else list.cisa.push({date:ref.cisa, days, risk:days / ref.days * 100}) 682 | } 683 | if( ref.epss ) list.epss.push({score:ref.epss}) 684 | if( ref.cvss3 || ref.cvss2 ) list.cvss.push({vector:ref.cvss3 || ref.cvss2}) 685 | } 686 | 687 | peak.epss = (1 - list.epss.reduce((p,v) => p * (1-v.score),1)) 688 | peak.cvss = list.cvss.reduce((max,{vector}) => { 689 | if( max === 10 ) return max 690 | const score = this.calculateCVSS(vector).environmentalMetricScore 691 | return Math.max(max, score) 692 | },0) 693 | 694 | peak.epss = { epss:peak.epss, risk:Math.round(peak.epss*100) } 695 | peak.cvss = { cvss:peak.cvss, risk:Math.round(peak.cvss*10) } 696 | peak.cisa = list.cisa.reduce((max,entry) => { 697 | if( entry.risk > max.risk ) return entry 698 | return max 699 | },{date:null,days:0,risk:0}) 700 | 701 | let epss = peak.epss.risk * this.#weight.epss 702 | let cvss = peak.cvss.risk * this.#weight.cvss 703 | let cisa = peak.cisa.risk * this.#weight.cisa 704 | 705 | let num = epss + cvss + cisa 706 | let den = this.#weight.epss + this.#weight.cvss + this.#weight.cisa 707 | 708 | return { peak, risk:Math.round(num / (den || 1)) } 709 | } 710 | 711 | } 712 | 713 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | export class Logger { 2 | constructor() { 3 | this.logging = false 4 | } 5 | 6 | /** 7 | * Log to console 8 | * @param {Error} lines an error object to throw in console 9 | */ 10 | error(err) { 11 | this.logging = false 12 | console.error(err) 13 | } 14 | 15 | /** 16 | * Log multiple lines console 17 | * @param {array} lines a list of strings to replace the last n-lines in console if the last log was array 18 | */ 19 | multi(lines) { 20 | if( this.logging ) { 21 | process.stdout.moveCursor(0, -1*(lines.length)) 22 | process.stdout.clearLine(0) 23 | process.stdout.cursorTo(0) 24 | } 25 | process.stdout.write(lines.join('\n')+'\n') 26 | this.logging = true 27 | } 28 | 29 | /** 30 | * Log to console 31 | * @param {array} lines a list of strings to replace the last n-lines in console if the last log was array 32 | * @param {Error} lines an error object to throw in console 33 | * @param {any} lines any other value/type to log in console 34 | */ 35 | log(lines, ...other) { 36 | if( lines === undefined ) return 37 | if( lines instanceof Error ) return this.error(lines) 38 | if( Array.isArray(lines) ) return this.multi(lines) 39 | 40 | this.logging = false 41 | console.log(lines, ...other) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/test_common.cjs: -------------------------------------------------------------------------------- 1 | const { join, dirname } = require('path') 2 | const { fileURLToPath } = require('url') 3 | 4 | const testCVEAggregate = async (...tests) => { 5 | const cveLibPath = join(__dirname, '..', 'lib', 'index.cjs') 6 | const { CVEAggregate } = require(cveLibPath) 7 | 8 | const verbose = true 9 | const filepath = join(__dirname, 'test_cves.json') 10 | const cves = new CVEAggregate(filepath, verbose) 11 | const cveList = ['CVE-2023-35390','CVE-2023-35391','CVE-2023-38180'] 12 | 13 | const testers = { 14 | build:async () => { 15 | cves.log('- Testing buld without save -') 16 | await cves.build(false) 17 | }, 18 | update:async () => { 19 | cves.log('- Testing update with save - ') 20 | await cves.update(true) 21 | }, 22 | report:async () => { 23 | cves.log('-'.repeat(30)) 24 | cves.report() 25 | }, 26 | gets:async () => { 27 | cves.log('-'.repeat(30)) 28 | cves.log('CVEs', 'queued', cveList) 29 | cves.log('CISA', 'in-KEV', cves.getCISA(...cveList)) //Is at least one of these CVEs in the KEV? 30 | cves.log('EPSS', 'scaled', cves.getEPSS(...cveList)) //Scaled product of all EPSS 31 | cves.log('CVSS', 'scored', cves.getCVSS(...cveList)) //Maximum score of all CVSS 32 | cves.log('-'.repeat(30)) 33 | cves.log('FULL', cves.list(...cveList)) 34 | }, 35 | maps:async () => { 36 | cves.log('-'.repeat(30)) 37 | cves.log('CVEs', 'queued', cveList) 38 | cves.log('CISA', cves.mapCISA(...cveList)) 39 | cves.log('EPSS', cves.mapEPSS(...cveList)) 40 | cves.log('CVSS', cves.mapCVSS(...cveList)) 41 | cves.log('-'.repeat(30)) 42 | cves.log('FULL', cves.map(...cveList)) 43 | }, 44 | cvss:async () => { 45 | cves.log('-'.repeat(30)) 46 | let v2vector = "AV:N/AC:L/Au:N/C:C/I:C/A:C" 47 | cves.log('Vector', v2vector) 48 | let v2score = cves.calculateCVSS(v2vector) 49 | cves.log('Score', v2score) 50 | let v2adjusted = v2score.adjust({ E:"F", RL:"U" }) 51 | cves.log('Adjusted', v2adjusted) 52 | cves.log('Description', cves.describeCVSS(v2adjusted.vectorString)) 53 | 54 | cves.log('-'.repeat(30)) 55 | let v3vector = "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H" 56 | cves.log('Vector', v3vector) 57 | let v3score = cves.calculateCVSS(v3vector) 58 | cves.log('Score', v3score) 59 | let v3adjusted = v3score.adjust({ E:"F", CR:"H" }) 60 | cves.log('Adjusted', v3adjusted) 61 | cves.log('Description', cves.describeCVSS(v3adjusted.vectorString)) 62 | }, 63 | search:async () => { 64 | cves.log('-'.repeat(30)) 65 | cves.log('Search', cves.search({ 66 | epss:{ gt:0.7 }, 67 | cvss:{ gt:9.0 }, 68 | cisa:{ gte:'2023-09-01' } 69 | // cisa:{ ne:null } 70 | })) 71 | }, 72 | check:async () => { 73 | cves.log('-'.repeat(30)) 74 | cves.log(cves.check(...cveList)) 75 | }, 76 | chart:async () => { 77 | cves.log('-'.repeat(30)) 78 | 79 | const aggregate = cves.cveList().map(id => { 80 | const check = cves.check(id) 81 | return { id, ...check } 82 | }) 83 | 84 | const s_lim = [ 10, 1 ]//.map(v => v * 2) 85 | const p_lim = [ 10, 1 ].map(v => v * 2) 86 | 87 | process.stdout.write(' ') 88 | for(let p = 0; p <= p_lim[0]; ++p) { 89 | process.stdout.write(p==p_lim[0] ? '*' : !(p % p_lim[1]) ? (p/p_lim[1]).toString() : '\u252C') 90 | } 91 | process.stdout.write('\n') 92 | 93 | for(let s = s_lim[0]; s >= 0; --s) { 94 | process.stdout.write(s==s_lim[0] ? '*' : !(s % s_lim[1]) ? (s/s_lim[1]).toString() : '\u251C') 95 | const severity = aggregate.filter(a => Math.floor(a.peak.cvss * s_lim[1]) == s) 96 | // process.stdout.write(`${severity.length}: ${severity[0]}`) 97 | for(let p = 0; p <= p_lim[0]; ++p) { 98 | const probability = severity.filter(a => Math.floor(a.peak.epss * p_lim[0]) == p) 99 | let color = !probability.length ? 40 //black 100 | : probability.length < 5 ? 47 //white 101 | : probability.length < 10 ? 42 //green 102 | : probability.length < 25 ? 46 //cyan 103 | : probability.length < 100 ? 44 //blue 104 | : probability.length < 500 ? 43 //yellow 105 | : 41 //red 106 | // process.stdout.write(`${probability.length} `) 107 | // let pct = Math.round(probability.length / (severity.length||1)) 108 | // const color = `48;2;${pct};${pct};${pct}` 109 | // process.stdout.write(`\x1b[${color}m${pct} \x1b[49m`) 110 | // process.stdout.write(`\x1b[${color}m \x1b[49m`) 111 | process.stdout.write(`\x1b[${color}m \x1b[49m`) 112 | } 113 | process.stdout.write(s==s_lim[0] ? '*' : !(s % s_lim[1]) ? (s/s_lim[1]).toString() : '\u2524') 114 | process.stdout.write('\n') 115 | // break 116 | } 117 | 118 | process.stdout.write(' ') 119 | for(let p = 0; p <= p_lim[0]; ++p) { 120 | process.stdout.write(p==p_lim[0] ? '*' : !(p % p_lim[1]) ? (p/p_lim[1]).toString() : '\u2534') 121 | } 122 | process.stdout.write('\n') 123 | 124 | } 125 | } 126 | 127 | //If no tests specified, test all 128 | if( !tests.length ) 129 | tests = Object.keys(testers) 130 | 131 | cves.log('-'.repeat(30)) 132 | cves.log('TESTING CVEAggregate') 133 | for(const test of tests) { 134 | await testers?.[test]?.() 135 | } 136 | } 137 | 138 | testCVEAggregate( 139 | // 'build', 'report', 140 | 'update', 'report', 141 | 'gets', 142 | 'maps', 143 | 'cvss', 144 | 'search', 145 | 'check', 146 | 'chart' 147 | ) 148 | 149 | -------------------------------------------------------------------------------- /test/test_esm.mjs: -------------------------------------------------------------------------------- 1 | import { join, dirname } from 'path' 2 | import { fileURLToPath } from 'url' 3 | 4 | const __dirname = dirname(fileURLToPath(import.meta.url)) 5 | 6 | const testCVEAggregate = async (...tests) => { 7 | const cveLibPath = join(__dirname, '..', 'src', 'index.js') 8 | const { CVEAggregate } = await import(cveLibPath) 9 | 10 | const verbose = true 11 | const filepath = join(__dirname, 'test_cves.json') 12 | const cves = new CVEAggregate(filepath, verbose) 13 | const cveList = ['CVE-2023-35390','CVE-2023-35391','CVE-2023-38180'] 14 | 15 | const testers = { 16 | build:async () => { 17 | cves.log('- Testing buld without save -') 18 | await cves.build(false) 19 | }, 20 | update:async () => { 21 | cves.log('- Testing update with save - ') 22 | await cves.update(true) 23 | }, 24 | report:async () => { 25 | cves.log('-'.repeat(30)) 26 | cves.report() 27 | }, 28 | gets:async () => { 29 | cves.log('-'.repeat(30)) 30 | cves.log('CVEs', 'queued', cveList) 31 | cves.log('CISA', 'in-KEV', cves.getCISA(...cveList)) //Is at least one of these CVEs in the KEV? 32 | cves.log('EPSS', 'scaled', cves.getEPSS(...cveList)) //Scaled product of all EPSS 33 | cves.log('CVSS', 'scored', cves.getCVSS(...cveList)) //Maximum score of all CVSS 34 | cves.log('-'.repeat(30)) 35 | cves.log('FULL', cves.list(...cveList)) 36 | }, 37 | maps:async () => { 38 | cves.log('-'.repeat(30)) 39 | cves.log('CVEs', 'queued', cveList) 40 | cves.log('CISA', cves.mapCISA(...cveList)) 41 | cves.log('EPSS', cves.mapEPSS(...cveList)) 42 | cves.log('CVSS', cves.mapCVSS(...cveList)) 43 | cves.log('-'.repeat(30)) 44 | cves.log('FULL', cves.map(...cveList)) 45 | }, 46 | cvss:async () => { 47 | cves.log('-'.repeat(30)) 48 | let v2vector = "AV:N/AC:L/Au:N/C:C/I:C/A:C" 49 | cves.log('Vector', v2vector) 50 | let v2score = cves.calculateCVSS(v2vector) 51 | cves.log('Score', v2score) 52 | let v2adjusted = v2score.adjust({ E:"F", RL:"U" }) 53 | cves.log('Adjusted', v2adjusted) 54 | cves.log('Description', cves.describeCVSS(v2adjusted.vectorString)) 55 | 56 | cves.log('-'.repeat(30)) 57 | let v3vector = "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H" 58 | cves.log('Vector', v3vector) 59 | let v3score = cves.calculateCVSS(v3vector) 60 | cves.log('Score', v3score) 61 | let v3adjusted = v3score.adjust({ E:"F", CR:"H" }) 62 | cves.log('Adjusted', v3adjusted) 63 | cves.log('Description', cves.describeCVSS(v3adjusted.vectorString)) 64 | }, 65 | search:async () => { 66 | cves.log('-'.repeat(30)) 67 | cves.log('Search', cves.search({ 68 | epss:{ gt:0.7 }, 69 | cvss:{ gt:9.0 }, 70 | cisa:{ gte:'2023-09-01' } 71 | // cisa:{ ne:null } 72 | })) 73 | }, 74 | check:async () => { 75 | cves.log('-'.repeat(30)) 76 | cves.log(cves.check(...cveList)) 77 | }, 78 | chart:async () => { 79 | cves.log('-'.repeat(30)) 80 | 81 | const aggregate = cves.cveList().map(id => { 82 | const check = cves.check(id) 83 | return { id, ...check } 84 | }) 85 | 86 | const s_lim = [ 10, 1 ]//.map(v => v * 2) 87 | const p_lim = [ 10, 1 ].map(v => v * 2) 88 | 89 | process.stdout.write(' ') 90 | for(let p = 0; p <= p_lim[0]; ++p) { 91 | process.stdout.write(p==p_lim[0] ? '*' : !(p % p_lim[1]) ? (p/p_lim[1]).toString() : '\u252C') 92 | } 93 | process.stdout.write('\n') 94 | 95 | for(let s = s_lim[0]; s >= 0; --s) { 96 | process.stdout.write(s==s_lim[0] ? '*' : !(s % s_lim[1]) ? (s/s_lim[1]).toString() : '\u251C') 97 | const severity = aggregate.filter(a => Math.floor(a.peak.cvss * s_lim[1]) == s) 98 | // process.stdout.write(`${severity.length}: ${severity[0]}`) 99 | for(let p = 0; p <= p_lim[0]; ++p) { 100 | const probability = severity.filter(a => Math.floor(a.peak.epss * p_lim[0]) == p) 101 | let color = !probability.length ? 40 //black 102 | : probability.length < 5 ? 47 //white 103 | : probability.length < 10 ? 42 //green 104 | : probability.length < 25 ? 46 //cyan 105 | : probability.length < 100 ? 44 //blue 106 | : probability.length < 500 ? 43 //yellow 107 | : 41 //red 108 | // process.stdout.write(`${probability.length} `) 109 | // let pct = Math.round(probability.length / (severity.length||1)) 110 | // const color = `48;2;${pct};${pct};${pct}` 111 | // process.stdout.write(`\x1b[${color}m${pct} \x1b[49m`) 112 | // process.stdout.write(`\x1b[${color}m \x1b[49m`) 113 | process.stdout.write(`\x1b[${color}m \x1b[49m`) 114 | } 115 | process.stdout.write(s==s_lim[0] ? '*' : !(s % s_lim[1]) ? (s/s_lim[1]).toString() : '\u2524') 116 | process.stdout.write('\n') 117 | // break 118 | } 119 | 120 | process.stdout.write(' ') 121 | for(let p = 0; p <= p_lim[0]; ++p) { 122 | process.stdout.write(p==p_lim[0] ? '*' : !(p % p_lim[1]) ? (p/p_lim[1]).toString() : '\u2534') 123 | } 124 | process.stdout.write('\n') 125 | 126 | } 127 | } 128 | 129 | //If no tests specified, test all 130 | if( !tests.length ) 131 | tests = Object.keys(testers) 132 | 133 | cves.log('-'.repeat(30)) 134 | cves.log('TESTING CVEAggregate') 135 | for(const test of tests) { 136 | await testers?.[test]?.() 137 | } 138 | } 139 | 140 | testCVEAggregate( 141 | // 'build', 'report', 142 | 'update', 'report', 143 | 'gets', 144 | 'maps', 145 | 'cvss', 146 | 'search', 147 | 'check', 148 | 'chart' 149 | ) 150 | 151 | -------------------------------------------------------------------------------- /update.js: -------------------------------------------------------------------------------- 1 | import { join, dirname } from 'path' 2 | import { fileURLToPath } from 'url' 3 | 4 | const __dirname = dirname(fileURLToPath(import.meta.url)) 5 | const fileJson = process.argv[2] 6 | if( !fileJson?.length ) { 7 | console.error('Error: No export file specified\n') 8 | console.log(`Build a full CVE aggregate with:\n$ node ${join(__dirname, 'update.js')} /path/to/cves.json`) 9 | process.exit(-1) 10 | } 11 | 12 | const cveLibPath = join(__dirname, 'src', 'index.js') 13 | const { CVEAggregate } = await import(cveLibPath) 14 | const aggregate = new CVEAggregate(fileJson, true) 15 | 16 | await aggregate.update(true) 17 | await aggregate.logger.log("-".repeat(30)) 18 | await aggregate.report() 19 | --------------------------------------------------------------------------------