Skip to content

Commit 80e2aaf

Browse files
committed
Add NPM live importer #1936
* Add NPM live pipeline importer to filter advisories affecting a single PURL * Add tests for NPM live importer Signed-off-by: Michael Ehab Mikhail <michael.ehab@hotmail.com>
1 parent a00b677 commit 80e2aaf

File tree

5 files changed

+242
-236
lines changed

5 files changed

+242
-236
lines changed

vulnerabilities/pipelines/npm_importer.py

Lines changed: 6 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,14 @@
99

1010
# Author: Navonil Das (@NavonilDas)
1111

12-
import json
13-
import os
14-
import tempfile
1512
from pathlib import Path
1613
from typing import Iterable
1714

1815
import pytz
19-
import requests
2016
from dateutil.parser import parse
2117
from fetchcode.vcs import fetch_via_vcs
2218
from packageurl import PackageURL
2319
from univers.version_range import NpmVersionRange
24-
from univers.versions import SemverVersion
2520

2621
from vulnerabilities.importer import AdvisoryData
2722
from vulnerabilities.importer import AffectedPackage
@@ -44,24 +39,14 @@ class NpmImporterPipeline(VulnerableCodeBaseImporterPipeline):
4439
repo_url = "git+https://github.com/nodejs/security-wg"
4540
importer_name = "Npm Importer"
4641

47-
is_batch_run = True
48-
49-
def __init__(self, *args, purl=None, **kwargs):
50-
super().__init__(*args, **kwargs)
51-
self.purl = purl
52-
if self.purl:
53-
NpmImporterPipeline.is_batch_run = False
54-
if self.purl.type != "npm":
55-
print(f"Warning: This importer handles NPM packages. Current PURL: {self.purl!s}")
56-
5742
@classmethod
5843
def steps(cls):
59-
return [
44+
return (
6045
cls.clone,
6146
cls.collect_and_store_advisories,
6247
cls.import_new_advisories,
6348
cls.clean_downloads,
64-
]
49+
)
6550

6651
def clone(self):
6752
self.log(f"Cloning `{self.repo_url}`")
@@ -73,26 +58,9 @@ def advisories_count(self):
7358

7459
def collect_advisories(self) -> Iterable[AdvisoryData]:
7560
vuln_directory = Path(self.vcs_response.dest_dir) / "vuln" / "npm"
76-
advisory_files = list(vuln_directory.glob("*.json"))
77-
78-
if not self.is_batch_run:
79-
package_name = self.purl.name
80-
filtered_files = []
81-
for advisory_file in advisory_files:
82-
try:
83-
data = load_json(advisory_file)
84-
if data.get("module_name") == package_name:
85-
affected_package = self.get_affected_package(data, package_name)
86-
if not self.purl.version or self._version_is_affected(affected_package):
87-
filtered_files.append(advisory_file)
88-
except Exception as e:
89-
self.log(f"Error processing advisory file {advisory_file}: {str(e)}")
90-
advisory_files = filtered_files
91-
92-
for advisory in list(advisory_files):
93-
for result in self.to_advisory_data(advisory):
94-
if result:
95-
yield result
61+
62+
for advisory in vuln_directory.glob("*.json"):
63+
yield from self.to_advisory_data(advisory)
9664

9765
def to_advisory_data(self, file: Path) -> Iterable[AdvisoryData]:
9866
data = load_json(file)
@@ -144,11 +112,6 @@ def to_advisory_data(self, file: Path) -> Iterable[AdvisoryData]:
144112
affected_packages.append(self.get_affected_package(data, package_name))
145113
advsisory_aliases = data.get("cves") or []
146114

147-
if self.purl and self.purl.version:
148-
affected_package = affected_packages[0] if affected_packages else None
149-
if affected_package and not self._version_is_affected(affected_package):
150-
return
151-
152115
for alias in advsisory_aliases:
153116
yield AdvisoryData(
154117
summary=build_description(summary=summary, description=description),
@@ -159,13 +122,6 @@ def to_advisory_data(self, file: Path) -> Iterable[AdvisoryData]:
159122
url=f"https://github.com/nodejs/security-wg/blob/main/vuln/npm/{id}.json",
160123
)
161124

162-
def _version_is_affected(self, affected_package):
163-
if not self.purl.version or not affected_package.affected_version_range:
164-
return True
165-
166-
purl_version = SemverVersion(self.purl.version)
167-
return purl_version in affected_package.affected_version_range
168-
169125
def get_affected_package(self, data, package_name):
170126
affected_version_range = None
171127
unaffected_version_range = None
@@ -209,4 +165,4 @@ def clean_downloads(self):
209165
self.vcs_response.delete()
210166

211167
def on_failure(self):
212-
self.clean_downloads()
168+
self.clean_downloads()

vulnerabilities/pipelines/v2_importers/npm_importer.py

Lines changed: 8 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,14 @@
1010
# Author: Navonil Das (@NavonilDas)
1111

1212
import json
13-
import os
14-
import tempfile
1513
from pathlib import Path
1614
from typing import Iterable
1715

1816
import pytz
19-
import requests
2017
from dateutil.parser import parse
2118
from fetchcode.vcs import fetch_via_vcs
2219
from packageurl import PackageURL
2320
from univers.version_range import NpmVersionRange
24-
from univers.versions import SemverVersion
2521

2622
from vulnerabilities.importer import AdvisoryData
2723
from vulnerabilities.importer import AffectedPackage
@@ -47,16 +43,6 @@ class NpmImporterPipeline(VulnerableCodeBaseImporterPipelineV2):
4743
repo_url = "git+https://github.com/nodejs/security-wg"
4844
unfurl_version_ranges = True
4945

50-
is_batch_run = True
51-
52-
def __init__(self, *args, purl=None, **kwargs):
53-
super().__init__(*args, **kwargs)
54-
self.purl = purl
55-
if self.purl:
56-
NpmImporterPipeline.is_batch_run = False
57-
if self.purl.type != "npm":
58-
print(f"Warning: This importer handles NPM packages. Current PURL: {self.purl!s}")
59-
6046
@classmethod
6147
def steps(cls):
6248
return (
@@ -75,32 +61,18 @@ def advisories_count(self):
7561

7662
def collect_advisories(self) -> Iterable[AdvisoryData]:
7763
vuln_directory = Path(self.vcs_response.dest_dir) / "vuln" / "npm"
78-
advisory_files = list(vuln_directory.glob("*.json"))
79-
80-
if not self.is_batch_run:
81-
package_name = self.purl.name
82-
filtered_files = []
83-
for advisory_file in advisory_files:
84-
try:
85-
data = load_json(advisory_file)
86-
if data.get("module_name") == package_name:
87-
affected_package = self.get_affected_package(data, package_name)
88-
if not self.purl.version or self._version_is_affected(affected_package):
89-
filtered_files.append(advisory_file)
90-
except Exception as e:
91-
self.log(f"Error processing advisory file {advisory_file}: {str(e)}")
92-
advisory_files = filtered_files
93-
94-
for advisory in list(advisory_files):
95-
result = self.to_advisory_data(advisory)
96-
if result:
97-
yield result
64+
65+
for advisory in vuln_directory.glob("*.json"):
66+
yield self.to_advisory_data(advisory)
9867

9968
def to_advisory_data(self, file: Path) -> Iterable[AdvisoryData]:
10069
if file.name == "index.json":
10170
self.log(f"Skipping {file.name} file")
10271
return
10372
data = load_json(file)
73+
advisory_text = None
74+
with open(file) as f:
75+
advisory_text = f.read()
10476
id = data.get("id")
10577
description = data.get("overview") or ""
10678
summary = data.get("title") or ""
@@ -153,11 +125,6 @@ def to_advisory_data(self, file: Path) -> Iterable[AdvisoryData]:
153125
affected_packages.append(self.get_affected_package(data, package_name))
154126
advsisory_aliases = data.get("cves") or []
155127

156-
if self.purl and self.purl.version:
157-
affected_package = affected_packages[0] if affected_packages else None
158-
if affected_package and not self._version_is_affected(affected_package):
159-
return
160-
161128
return AdvisoryData(
162129
advisory_id=f"npm-{id}",
163130
aliases=advsisory_aliases,
@@ -167,15 +134,9 @@ def to_advisory_data(self, file: Path) -> Iterable[AdvisoryData]:
167134
references_v2=references,
168135
severities=severities,
169136
url=f"https://github.com/nodejs/security-wg/blob/main/vuln/npm/{id}.json",
137+
original_advisory_text=advisory_text or json.dumps(data, indent=2, ensure_ascii=False),
170138
)
171139

172-
def _version_is_affected(self, affected_package):
173-
if not self.purl.version or not affected_package.affected_version_range:
174-
return True
175-
176-
purl_version = SemverVersion(self.purl.version)
177-
return purl_version in affected_package.affected_version_range
178-
179140
def get_affected_package(self, data, package_name):
180141
affected_version_range = None
181142
unaffected_version_range = None
@@ -218,11 +179,5 @@ def clean_downloads(self):
218179
self.log(f"Removing cloned repository")
219180
self.vcs_response.delete()
220181

221-
if hasattr(self, "temp_dir") and os.path.exists(self.temp_dir):
222-
import shutil
223-
224-
self.log(f"Removing temporary directory")
225-
shutil.rmtree(self.temp_dir)
226-
227182
def on_failure(self):
228-
self.clean_downloads()
183+
self.clean_downloads()
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#
2+
# Copyright (c) nexB Inc. and others. All rights reserved.
3+
# VulnerableCode is a trademark of nexB Inc.
4+
# SPDX-License-Identifier: Apache-2.0
5+
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
6+
# See https://github.com/aboutcode-org/vulnerablecode for support or download.
7+
# See https://aboutcode.org for more information about nexB OSS projects.
8+
#
9+
10+
from pathlib import Path
11+
from typing import Iterable
12+
13+
from packageurl import PackageURL
14+
from univers.versions import SemverVersion
15+
16+
from vulnerabilities.importer import AdvisoryData
17+
from vulnerabilities.pipelines.v2_importers.npm_importer import NpmImporterPipeline
18+
from vulnerabilities.utils import load_json
19+
20+
21+
class NpmLiveImporterPipeline(NpmImporterPipeline):
22+
"""
23+
Node.js Security Working Group importer pipeline
24+
25+
Import advisories from nodejs security working group including node proper advisories and npm advisories for a single PURL.
26+
"""
27+
28+
pipeline_id = "nodejs_security_wg_live_importer"
29+
supported_types = ["npm"]
30+
spdx_license_expression = "MIT"
31+
license_url = "https://github.com/nodejs/security-wg/blob/main/LICENSE.md"
32+
repo_url = "git+https://github.com/nodejs/security-wg"
33+
unfurl_version_ranges = True
34+
35+
@classmethod
36+
def steps(cls):
37+
return (
38+
cls.get_purl_inputs,
39+
cls.clone,
40+
cls.collect_and_store_advisories,
41+
cls.clean_downloads,
42+
)
43+
44+
def get_purl_inputs(self):
45+
purl = self.inputs["purl"]
46+
if not purl:
47+
raise ValueError("PURL is required for NpmLiveImporterPipeline")
48+
49+
if isinstance(purl, str):
50+
purl = PackageURL.from_string(purl)
51+
52+
if not isinstance(purl, PackageURL):
53+
raise ValueError(f"Object of type {type(purl)} {purl!r} is not a PackageURL instance")
54+
55+
if purl.type not in self.supported_types:
56+
raise ValueError(
57+
f"PURL: {purl!s} is not among the supported package types {self.supported_types!r}"
58+
)
59+
60+
if not purl.version:
61+
raise ValueError(f"PURL: {purl!s} is expected to have a version")
62+
63+
self.purl = purl
64+
65+
def collect_advisories(self) -> Iterable[AdvisoryData]:
66+
vuln_directory = Path(self.vcs_response.dest_dir) / "vuln" / "npm"
67+
advisory_files = list(vuln_directory.glob("*.json"))
68+
69+
package_name = self.purl.name
70+
filtered_files = []
71+
for advisory_file in advisory_files:
72+
try:
73+
data = load_json(advisory_file)
74+
if data.get("module_name") == package_name:
75+
affected_package = self.get_affected_package(data, package_name)
76+
if not self.purl.version or self._version_is_affected(affected_package):
77+
filtered_files.append(advisory_file)
78+
except Exception as e:
79+
self.log(f"Error processing advisory file {advisory_file}: {str(e)}")
80+
advisory_files = filtered_files
81+
82+
for advisory in list(advisory_files):
83+
result = self.to_advisory_data(advisory)
84+
if result:
85+
yield result
86+
87+
def _version_is_affected(self, affected_package):
88+
if not self.purl.version or not affected_package.affected_version_range:
89+
return True
90+
91+
purl_version = SemverVersion(self.purl.version)
92+
return purl_version in affected_package.affected_version_range

0 commit comments

Comments
 (0)