Skip to content

Commit 19ebb0d

Browse files
feat: Add support for Javascript package scanning (Fixes #1453) (#1548)
1 parent 49b4b24 commit 19ebb0d

File tree

7 files changed

+1303
-3
lines changed

7 files changed

+1303
-3
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,12 @@ The scanner examines the `pom.xml` file within a Java package archive to identif
228228

229229
JAR, WAR and EAR archives are supported.
230230

231+
### Javascript
232+
233+
The scanner examines the `package-lock.json` file within a javascript application
234+
to identify components. The package names and versions are used to search the database for vulnerabilities.
235+
236+
231237
### Python
232238

233239
The scanner examines the `PKG-INFO` and `METADATA` files for an installed Python package to extract the component name and version which

cve_bin_tool/version_scanner.py

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# Copyright (C) 2021 Intel Corporation
22
# SPDX-License-Identifier: GPL-3.0-or-later
33

4+
import json
45
import os
56
import subprocess
67
import sys
78
from re import MULTILINE, compile, search
9+
from typing import List
810

911
import defusedxml.ElementTree as ET
1012

@@ -122,6 +124,7 @@ def is_executable(self, filename):
122124
and ("PKG-INFO: " not in output)
123125
and ("METADATA: " not in output)
124126
and ("pom.xml" not in output)
127+
and ("package-lock.json" not in output)
125128
):
126129
return False, None
127130
# otherwise use python implementation of file
@@ -169,8 +172,11 @@ def scan_file(self, filename):
169172

170173
# Check for Java package
171174
if output and "pom.xml" in output:
172-
java_lines = "\n".join(lines.splitlines())
173-
yield from self.run_java_checker(filename, java_lines)
175+
yield from self.run_java_checker(filename)
176+
177+
# Javascript checker
178+
if output and "package-lock.json" in output:
179+
yield from self.run_js_checker(filename)
174180

175181
# If python package then strip the lines to avoid detecting other product strings
176182
if output and ("PKG-INFO: " in output or "METADATA: " in output):
@@ -199,7 +205,7 @@ def find_java_vendor(self, product, version):
199205
return ProductInfo(vendor, product, version), file_path
200206
return None, None
201207

202-
def run_java_checker(self, filename, lines):
208+
def run_java_checker(self, filename: str) -> None:
203209
"""Process maven pom.xml file and extract product and dependency details"""
204210
tree = ET.parse(filename)
205211
# Find root element
@@ -247,6 +253,65 @@ def run_java_checker(self, filename, lines):
247253

248254
self.logger.debug(f"Done scanning file: {filename}")
249255

256+
def find_js_vendor(self, product: str, version: str) -> List[List[str]]:
257+
"""Find vendor for Javascript product"""
258+
if version == "*":
259+
return None
260+
vendor_package_pair = self.cve_db.get_vendor_product_pairs(product)
261+
vendorlist: List[List[str]] = []
262+
if vendor_package_pair != []:
263+
# To handle multiple vendors, return all combinations of product/vendor mappings
264+
for v in vendor_package_pair:
265+
vendor = v["vendor"]
266+
file_path = "".join(self.file_stack)
267+
# Tidy up version string
268+
if "^" in version:
269+
version = version[1:]
270+
self.logger.debug(f"{file_path} {product} {version} by {vendor}")
271+
vendorlist.append([ProductInfo(vendor, product, version), file_path])
272+
return vendorlist if len(vendorlist) > 0 else None
273+
return None
274+
275+
def run_js_checker(self, filename: str) -> None:
276+
"""Process package-lock.json file and extract product and dependency details"""
277+
fh = open(filename)
278+
data = json.load(fh)
279+
product = data["name"]
280+
version = data["version"]
281+
vendor = self.find_js_vendor(product, version)
282+
if vendor is not None:
283+
for v in vendor:
284+
yield v[0], v[1] # product_info, file_path
285+
# Now process dependencies
286+
for i in data["dependencies"]:
287+
# To handle @actions/<product>: lines, extract product name from line
288+
product = i.split("/")[1] if "/" in i else i
289+
# Handle different formats. Either <product> : <version> or
290+
# <product>: {
291+
# ...
292+
# "version" : <version>
293+
# ...
294+
# }
295+
try:
296+
version = data["dependencies"][i]["version"]
297+
except Exception:
298+
# Cater for case when version field not present
299+
version = data["dependencies"][i]
300+
vendor = self.find_js_vendor(product, version)
301+
if vendor is not None:
302+
for v in vendor:
303+
yield v[0], v[1] # product_info, file_path
304+
if "requires" in data["dependencies"][i]:
305+
for r in data["dependencies"][i]["requires"]:
306+
# To handle @actions/<product>: lines, extract product name from line
307+
product = r.split("/")[1] if "/" in r else r
308+
version = data["dependencies"][i]["requires"][r]
309+
vendor = self.find_js_vendor(product, version)
310+
if vendor is not None:
311+
for v in vendor:
312+
yield v[0], v[1] # product_info, file_path
313+
self.logger.debug(f"Done scanning file: {filename}")
314+
250315
def run_python_package_checkers(self, filename, lines):
251316
"""
252317
This generator runs only for python packages.
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
{
2+
"name": "setup-python",
3+
"version": "2.2.2",
4+
"lockfileVersion": 1,
5+
"requires": true,
6+
"dependencies": {
7+
"@actions/cache": {
8+
"version": "1.0.8",
9+
"resolved": "https://registry.npmjs.org/@actions/cache/-/cache-1.0.8.tgz",
10+
"integrity": "sha512-GWNNB67w93HGJRQXlsV56YqrdAuDoP3esK/mo5mzU8WoDCVjtQgJGsTdkYUX7brswtT7xnI30bWNo1WLKQ8FZQ==",
11+
"requires": {
12+
"@actions/core": "^1.2.6",
13+
"@actions/exec": "^1.0.1",
14+
"@actions/glob": "^0.1.0",
15+
"@actions/http-client": "^1.0.9",
16+
"@actions/io": "^1.0.1",
17+
"@azure/ms-rest-js": "^2.0.7",
18+
"@azure/storage-blob": "^12.1.2",
19+
"semver": "^6.1.0",
20+
"uuid": "^3.3.3"
21+
},
22+
"dependencies": {
23+
"@actions/glob": {
24+
"version": "0.1.2",
25+
"resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.1.2.tgz",
26+
"integrity": "sha512-SclLR7Ia5sEqjkJTPs7Sd86maMDw43p769YxBOxvPvEWuPEhpAnBsQfENOpXjFYMmhCqd127bmf+YdvJqVqR4A==",
27+
"requires": {
28+
"@actions/core": "^1.2.6",
29+
"minimatch": "^3.0.4"
30+
}
31+
},
32+
"@actions/http-client": {
33+
"version": "1.0.11",
34+
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
35+
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
36+
"requires": {
37+
"tunnel": "0.0.6"
38+
}
39+
},
40+
"semver": {
41+
"version": "6.3.0",
42+
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
43+
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
44+
}
45+
}
46+
},
47+
"@actions/core": {
48+
"version": "1.2.6",
49+
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
50+
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
51+
},
52+
"jest-snapshot": {
53+
"version": "27.2.5",
54+
"resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.2.5.tgz",
55+
"integrity": "sha512-2/Jkn+VN6Abwz0llBltZaiJMnL8b1j5Bp/gRIxe9YR3FCEh9qp0TXVV0dcpTGZ8AcJV1SZGQkczewkI9LP5yGw==",
56+
"dev": true,
57+
"requires": {
58+
"@babel/core": "^7.7.2",
59+
"@babel/generator": "^7.7.2",
60+
"@babel/parser": "^7.7.2",
61+
"@babel/plugin-syntax-typescript": "^7.7.2",
62+
"@babel/traverse": "^7.7.2",
63+
"@babel/types": "^7.0.0",
64+
"@jest/transform": "^27.2.5",
65+
"@jest/types": "^27.2.5",
66+
"@types/babel__traverse": "^7.0.4",
67+
"@types/prettier": "^2.1.5",
68+
"babel-preset-current-node-syntax": "^1.0.0",
69+
"chalk": "^4.0.0",
70+
"expect": "^27.2.5",
71+
"graceful-fs": "^4.2.4",
72+
"jest-diff": "^27.2.5",
73+
"jest-get-type": "^27.0.6",
74+
"jest-haste-map": "^27.2.5",
75+
"jest-matcher-utils": "^27.2.5",
76+
"jest-message-util": "^27.2.5",
77+
"jest-resolve": "^27.2.5",
78+
"jest-util": "^27.2.5",
79+
"natural-compare": "^1.4.0",
80+
"pretty-format": "^27.2.5",
81+
"semver": "^7.3.2"
82+
},
83+
"dependencies": {
84+
"semver": {
85+
"version": "7.3.5",
86+
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
87+
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
88+
"dev": true,
89+
"requires": {
90+
"lru-cache": "^6.0.0"
91+
}
92+
}
93+
}
94+
},
95+
"node-releases": {
96+
"version": "1.1.77",
97+
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.77.tgz",
98+
"integrity": "sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ==",
99+
"dev": true
100+
},
101+
"typescript": {
102+
"version": "3.8.3",
103+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
104+
"integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
105+
"dev": true
106+
},
107+
"yargs-parser": {
108+
"version": "20.2.9",
109+
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
110+
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
111+
"dev": true
112+
}
113+
}
114+
}

test/language_data/package.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "node-js-sample",
3+
"version": "0.2.0",
4+
"description": "A sample Node.js app using Express 4",
5+
"main": "index.js",
6+
"scripts": {
7+
"start": "node index.js"
8+
},
9+
"dependencies": {
10+
"express": "^4.13.3"
11+
},
12+
"engines": {
13+
"node": "4.0.0"
14+
},
15+
"repository": {
16+
"type": "git",
17+
"url": "https://github.com/heroku/node-js-sample"
18+
},
19+
"keywords": [
20+
"node",
21+
"heroku",
22+
"express"
23+
],
24+
"author": "Mark Pundsack",
25+
"contributors": [
26+
"Zeke Sikelianos <[email protected]> (http://zeke.sikelianos.com)"
27+
],
28+
"license": "MIT"
29+
}

0 commit comments

Comments
 (0)