Skip to content

Commit

Permalink
feat: Add support for Javascript package scanning (Fixes #1453) (#1548)
Browse files Browse the repository at this point in the history
  • Loading branch information
anthonyharrison committed Feb 16, 2022
1 parent 49b4b24 commit 19ebb0d
Show file tree
Hide file tree
Showing 7 changed files with 1,303 additions and 3 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,12 @@ The scanner examines the `pom.xml` file within a Java package archive to identif

JAR, WAR and EAR archives are supported.

### Javascript

The scanner examines the `package-lock.json` file within a javascript application
to identify components. The package names and versions are used to search the database for vulnerabilities.


### Python

The scanner examines the `PKG-INFO` and `METADATA` files for an installed Python package to extract the component name and version which
Expand Down
71 changes: 68 additions & 3 deletions cve_bin_tool/version_scanner.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# Copyright (C) 2021 Intel Corporation
# SPDX-License-Identifier: GPL-3.0-or-later

import json
import os
import subprocess
import sys
from re import MULTILINE, compile, search
from typing import List

import defusedxml.ElementTree as ET

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

# Check for Java package
if output and "pom.xml" in output:
java_lines = "\n".join(lines.splitlines())
yield from self.run_java_checker(filename, java_lines)
yield from self.run_java_checker(filename)

# Javascript checker
if output and "package-lock.json" in output:
yield from self.run_js_checker(filename)

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

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

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

def find_js_vendor(self, product: str, version: str) -> List[List[str]]:
"""Find vendor for Javascript product"""
if version == "*":
return None
vendor_package_pair = self.cve_db.get_vendor_product_pairs(product)
vendorlist: List[List[str]] = []
if vendor_package_pair != []:
# To handle multiple vendors, return all combinations of product/vendor mappings
for v in vendor_package_pair:
vendor = v["vendor"]
file_path = "".join(self.file_stack)
# Tidy up version string
if "^" in version:
version = version[1:]
self.logger.debug(f"{file_path} {product} {version} by {vendor}")
vendorlist.append([ProductInfo(vendor, product, version), file_path])
return vendorlist if len(vendorlist) > 0 else None
return None

def run_js_checker(self, filename: str) -> None:
"""Process package-lock.json file and extract product and dependency details"""
fh = open(filename)
data = json.load(fh)
product = data["name"]
version = data["version"]
vendor = self.find_js_vendor(product, version)
if vendor is not None:
for v in vendor:
yield v[0], v[1] # product_info, file_path
# Now process dependencies
for i in data["dependencies"]:
# To handle @actions/<product>: lines, extract product name from line
product = i.split("/")[1] if "/" in i else i
# Handle different formats. Either <product> : <version> or
# <product>: {
# ...
# "version" : <version>
# ...
# }
try:
version = data["dependencies"][i]["version"]
except Exception:
# Cater for case when version field not present
version = data["dependencies"][i]
vendor = self.find_js_vendor(product, version)
if vendor is not None:
for v in vendor:
yield v[0], v[1] # product_info, file_path
if "requires" in data["dependencies"][i]:
for r in data["dependencies"][i]["requires"]:
# To handle @actions/<product>: lines, extract product name from line
product = r.split("/")[1] if "/" in r else r
version = data["dependencies"][i]["requires"][r]
vendor = self.find_js_vendor(product, version)
if vendor is not None:
for v in vendor:
yield v[0], v[1] # product_info, file_path
self.logger.debug(f"Done scanning file: {filename}")

def run_python_package_checkers(self, filename, lines):
"""
This generator runs only for python packages.
Expand Down
114 changes: 114 additions & 0 deletions test/language_data/package-lock1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
{
"name": "setup-python",
"version": "2.2.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@actions/cache": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@actions/cache/-/cache-1.0.8.tgz",
"integrity": "sha512-GWNNB67w93HGJRQXlsV56YqrdAuDoP3esK/mo5mzU8WoDCVjtQgJGsTdkYUX7brswtT7xnI30bWNo1WLKQ8FZQ==",
"requires": {
"@actions/core": "^1.2.6",
"@actions/exec": "^1.0.1",
"@actions/glob": "^0.1.0",
"@actions/http-client": "^1.0.9",
"@actions/io": "^1.0.1",
"@azure/ms-rest-js": "^2.0.7",
"@azure/storage-blob": "^12.1.2",
"semver": "^6.1.0",
"uuid": "^3.3.3"
},
"dependencies": {
"@actions/glob": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.1.2.tgz",
"integrity": "sha512-SclLR7Ia5sEqjkJTPs7Sd86maMDw43p769YxBOxvPvEWuPEhpAnBsQfENOpXjFYMmhCqd127bmf+YdvJqVqR4A==",
"requires": {
"@actions/core": "^1.2.6",
"minimatch": "^3.0.4"
}
},
"@actions/http-client": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.11.tgz",
"integrity": "sha512-VRYHGQV1rqnROJqdMvGUbY/Kn8vriQe/F9HR2AlYHzmKuM/p3kjNuXhmdBfcVgsvRWTz5C5XW5xvndZrVBuAYg==",
"requires": {
"tunnel": "0.0.6"
}
},
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
}
}
},
"@actions/core": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
},
"jest-snapshot": {
"version": "27.2.5",
"resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.2.5.tgz",
"integrity": "sha512-2/Jkn+VN6Abwz0llBltZaiJMnL8b1j5Bp/gRIxe9YR3FCEh9qp0TXVV0dcpTGZ8AcJV1SZGQkczewkI9LP5yGw==",
"dev": true,
"requires": {
"@babel/core": "^7.7.2",
"@babel/generator": "^7.7.2",
"@babel/parser": "^7.7.2",
"@babel/plugin-syntax-typescript": "^7.7.2",
"@babel/traverse": "^7.7.2",
"@babel/types": "^7.0.0",
"@jest/transform": "^27.2.5",
"@jest/types": "^27.2.5",
"@types/babel__traverse": "^7.0.4",
"@types/prettier": "^2.1.5",
"babel-preset-current-node-syntax": "^1.0.0",
"chalk": "^4.0.0",
"expect": "^27.2.5",
"graceful-fs": "^4.2.4",
"jest-diff": "^27.2.5",
"jest-get-type": "^27.0.6",
"jest-haste-map": "^27.2.5",
"jest-matcher-utils": "^27.2.5",
"jest-message-util": "^27.2.5",
"jest-resolve": "^27.2.5",
"jest-util": "^27.2.5",
"natural-compare": "^1.4.0",
"pretty-format": "^27.2.5",
"semver": "^7.3.2"
},
"dependencies": {
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
"integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==",
"dev": true,
"requires": {
"lru-cache": "^6.0.0"
}
}
}
},
"node-releases": {
"version": "1.1.77",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.77.tgz",
"integrity": "sha512-rB1DUFUNAN4Gn9keO2K1efO35IDK7yKHCdCaIMvFO7yUYmmZYeDjnGKle26G4rwj+LKRQpjyUUvMkPglwGCYNQ==",
"dev": true
},
"typescript": {
"version": "3.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz",
"integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==",
"dev": true
},
"yargs-parser": {
"version": "20.2.9",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
"dev": true
}
}
}
29 changes: 29 additions & 0 deletions test/language_data/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "node-js-sample",
"version": "0.2.0",
"description": "A sample Node.js app using Express 4",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "^4.13.3"
},
"engines": {
"node": "4.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/heroku/node-js-sample"
},
"keywords": [
"node",
"heroku",
"express"
],
"author": "Mark Pundsack",
"contributors": [
"Zeke Sikelianos <zeke@sikelianos.com> (http://zeke.sikelianos.com)"
],
"license": "MIT"
}

0 comments on commit 19ebb0d

Please sign in to comment.