Skip to content

Commit 7606fee

Browse files
committed
Add tests
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 2a212fd commit 7606fee

6 files changed

Lines changed: 116 additions & 134 deletions

File tree

vulnerabilities/importers/npm.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from pathlib import Path
1313
from typing import Iterable
14+
from typing import List
1415

1516
import pytz
1617
from dateutil.parser import parse
@@ -48,7 +49,7 @@ def advisory_data(self) -> Iterable[AdvisoryData]:
4849
if self.vcs_response:
4950
self.vcs_response.delete()
5051

51-
def to_advisory_data(self, file: Path) -> AdvisoryData:
52+
def to_advisory_data(self, file: Path) -> List[AdvisoryData]:
5253
data = load_json(file)
5354
id = data.get("id")
5455
description = data.get("overview") or ""
@@ -95,14 +96,29 @@ def to_advisory_data(self, file: Path) -> AdvisoryData:
9596
if package_name:
9697
affected_packages.append(self.get_affected_package(data, package_name))
9798
advsisory_aliases = data.get("cves") or []
98-
advsisory_aliases.append(f"NPM-{id}")
99-
for alias in advsisory_aliases:
99+
100+
# If an adviosry has more than one CVE, then we should yield more than one advisory
101+
if len(advsisory_aliases) > 1:
102+
print(f"Found {len(advsisory_aliases)} CVEs for {id}")
103+
for alias in advsisory_aliases:
104+
yield AdvisoryData(
105+
summary=build_description(summary=summary, description=description),
106+
references=references,
107+
date_published=date_published,
108+
affected_packages=affected_packages,
109+
aliases=[alias],
110+
)
111+
112+
# If an advisory has only one CVE, then we should yield only one advisory
113+
# with the NPM ID also as an alias
114+
else:
115+
advsisory_aliases.append(f"NPM-{id}")
100116
yield AdvisoryData(
101117
summary=build_description(summary=summary, description=description),
102118
references=references,
103119
date_published=date_published,
104120
affected_packages=affected_packages,
105-
aliases=[alias],
121+
aliases=advsisory_aliases,
106122
)
107123

108124
def get_affected_package(self, data, package_name):

vulnerabilities/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,10 @@ def url(self):
452452
if alias.startswith("GHSA"):
453453
return f"https://github.com/advisories/{alias}"
454454

455+
if alias.startswith("NPM-"):
456+
id = alias.lstrip("NPM-")
457+
return f"https://github.com/nodejs/security-wg/blob/main/vuln/npm/{id}.json"
458+
455459

456460
class Advisory(models.Model):
457461
"""

vulnerabilities/tests/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ def no_rmtree(monkeypatch):
3636
"test_models.py",
3737
"test_mozilla.py",
3838
"test_msr2019.py",
39-
"test_npm.py",
4039
"test_package_managers.py",
4140
"test_postgresql.py",
4241
"test_retiredotnet.py",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"id": 152,
3+
"created_at": "2016-10-27",
4+
"updated_at": "2017-03-23",
5+
"title": "Predictable temp filenames allow overwrite of arbitrary files",
6+
"author": {
7+
"name": "Jérémy Lal",
8+
"website": null,
9+
"username": null
10+
},
11+
"module_name": "npm",
12+
"publish_date": "2017-03-23",
13+
"cves": [
14+
"CVE-2013-4116"
15+
],
16+
"vulnerable_versions": "<1.3.3",
17+
"patched_versions": ">=1.3.3",
18+
"overview": "npm versions before 1.3.3 allows local users to overwrite arbitrary files via a symlink attack on temporary files with predictable names that are created when unpacking archives.",
19+
"recommendation": "Upgrade to the latest version of npm",
20+
"references": [
21+
"https://github.com/npm/npm/issues/3635",
22+
"http://www.openwall.com/lists/oss-security/2013/07/10/17"
23+
],
24+
"cvss_vector": "CVSS:3.0/AV:L/AC:L/PR:L/UI:R/S:C/C:N/I:L/A:N",
25+
"cvss_score": 3.2,
26+
"coordinating_vendor": "^Lift Security"
27+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
[
2+
{
3+
"aliases": [
4+
[
5+
"CVE-2013-4116",
6+
"NPM-152"
7+
]
8+
],
9+
"summary": "Predictable temp filenames allow overwrite of arbitrary files\nnpm versions before 1.3.3 allows local users to overwrite arbitrary files via a symlink attack on temporary files with predictable names that are created when unpacking archives.",
10+
"affected_packages": [
11+
{
12+
"package": {
13+
"type": "npm",
14+
"namespace": null,
15+
"name": "npm",
16+
"version": null,
17+
"qualifiers": null,
18+
"subpath": null
19+
},
20+
"affected_version_range": "vers:npm/<1.3.3",
21+
"fixed_version": "1.3.3"
22+
}
23+
],
24+
"references": [
25+
{
26+
"reference_id": "",
27+
"url": "https://github.com/npm/npm/issues/3635",
28+
"severities": [
29+
{
30+
"system": "cvssv3",
31+
"value": 3.2
32+
}
33+
]
34+
},
35+
{
36+
"reference_id": "",
37+
"url": "http://www.openwall.com/lists/oss-security/2013/07/10/17",
38+
"severities": [
39+
{
40+
"system": "cvssv3",
41+
"value": 3.2
42+
}
43+
]
44+
},
45+
{
46+
"reference_id": 152,
47+
"url": "https://github.com/nodejs/security-wg/blob/main/vuln/npm/152.json",
48+
"severities": [
49+
{
50+
"system": "cvssv3",
51+
"value": 3.2
52+
}
53+
]
54+
}
55+
],
56+
"date_published": "2016-10-27T00:00:00+00:00"
57+
}
58+
]

vulnerabilities/tests/test_npm.py

Lines changed: 7 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -9,138 +9,16 @@
99
#
1010

1111
import os
12-
import shutil
13-
import tempfile
14-
import zipfile
15-
from unittest.mock import patch
1612

17-
from django.test import TestCase
18-
19-
from vulnerabilities import models
20-
from vulnerabilities.import_runner import ImportRunner
21-
from vulnerabilities.importers.npm import categorize_versions
22-
from vulnerabilities.package_managers import NpmVersionAPI
23-
from vulnerabilities.package_managers import Version
13+
from vulnerabilities.importers.npm import NpmImporter
14+
from vulnerabilities.tests import util_tests
2415

2516
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
2617
TEST_DATA = os.path.join(BASE_DIR, "test_data/")
2718

2819

29-
MOCK_VERSION_API = NpmVersionAPI(
30-
cache={
31-
"jquery": {Version("3.4.0"), Version("3.8.0")},
32-
"kerberos": {Version("0.5.8"), Version("1.2.0")},
33-
"@hapi/subtext": {
34-
Version("3.7.0"),
35-
Version("4.1.1"),
36-
Version("6.1.3"),
37-
Version("7.0.0"),
38-
Version("7.0.5"),
39-
},
40-
}
41-
)
42-
43-
44-
@patch("vulnerabilities.importers.NpmImporter._update_from_remote")
45-
class NpmImportTest(TestCase):
46-
47-
tempdir = None
48-
49-
@classmethod
50-
def setUpClass(cls) -> None:
51-
cls.tempdir = tempfile.mkdtemp()
52-
zip_path = os.path.join(TEST_DATA, "npm.zip")
53-
54-
with zipfile.ZipFile(zip_path, "r") as zip_ref:
55-
zip_ref.extractall(cls.tempdir)
56-
57-
cls.importer = models.Importer.objects.create(
58-
name="npm_unittests",
59-
license="",
60-
last_run=None,
61-
data_source="NpmImporter",
62-
data_source_cfg={
63-
"repository_url": "https://example.git",
64-
"working_directory": os.path.join(cls.tempdir, "npm/npm_test"),
65-
"create_working_directory": False,
66-
"remove_working_directory": False,
67-
},
68-
)
69-
70-
@classmethod
71-
def tearDownClass(cls) -> None:
72-
# Make sure no requests for unexpected package names have been made during the tests.
73-
shutil.rmtree(cls.tempdir)
74-
assert len(MOCK_VERSION_API.cache) == 3, MOCK_VERSION_API.cache
75-
76-
def test_import(self, _):
77-
runner = ImportRunner(self.importer, 5)
78-
79-
with patch("vulnerabilities.importers.NpmImporter.versions", new=MOCK_VERSION_API):
80-
with patch("vulnerabilities.importers.NpmImporter.set_api"):
81-
runner.run()
82-
83-
assert models.Vulnerability.objects.count() == 3
84-
assert models.VulnerabilityReference.objects.count() == 3
85-
assert models.PackageRelatedVulnerability.objects.all().count() == 4
86-
87-
assert models.Package.objects.count() == 8
88-
89-
self.assert_for_package(
90-
"jquery", {"3.4.0"}, {"3.8.0"}, "1518", vulnerability_id="CVE-2020-11022"
91-
) # nopep8
92-
self.assert_for_package("kerberos", {"0.5.8"}, {"1.2.0"}, "1514")
93-
self.assert_for_package("subtext", {"4.1.1", "7.0.0"}, {"6.1.3", "7.0.5"}, "1476")
94-
95-
def assert_for_package(
96-
self,
97-
package_name,
98-
impacted_versions,
99-
resolved_versions,
100-
vuln_id,
101-
vulnerability_id=None,
102-
):
103-
vuln = None
104-
105-
for version in impacted_versions:
106-
pkg = models.Package.objects.get(name=package_name, version=version)
107-
108-
assert pkg.vulnerabilities.count() == 1
109-
vuln = pkg.vulnerabilities.first()
110-
if vulnerability_id:
111-
assert vuln.vulnerability_id == vulnerability_id
112-
113-
ref_url = f"https://registry.npmjs.org/-/npm/v1/advisories/{vuln_id}"
114-
assert models.VulnerabilityReference.objects.get(url=ref_url, vulnerability=vuln)
115-
116-
for version in resolved_versions:
117-
pkg = models.Package.objects.get(name=package_name, version=version)
118-
assert models.PackageRelatedVulnerability.objects.filter(
119-
patched_package=pkg, vulnerability=vuln
120-
)
121-
122-
123-
def test_categorize_versions_simple_ranges():
124-
all_versions = {"3.4.0", "3.8.0"}
125-
impacted_ranges = "<3.5.0"
126-
resolved_ranges = ">=3.5.0"
127-
128-
impacted_versions, resolved_versions = categorize_versions(
129-
all_versions, impacted_ranges, resolved_ranges
130-
)
131-
132-
assert impacted_versions == {"3.4.0"}
133-
assert resolved_versions == {"3.8.0"}
134-
135-
136-
def test_categorize_versions_complex_ranges():
137-
all_versions = {"3.7.0", "4.1.1", "6.1.3", "7.0.0", "7.0.5"}
138-
impacted_ranges = ">=4.1.0 <6.1.3 || >= 7.0.0 <7.0.3"
139-
resolved_ranges = ">=6.1.3 <7.0.0 || >=7.0.3"
140-
141-
impacted_versions, resolved_versions = categorize_versions(
142-
all_versions, impacted_ranges, resolved_ranges
143-
)
144-
145-
assert impacted_versions == {"4.1.1", "7.0.0"}
146-
assert resolved_versions == {"3.7.0", "6.1.3", "7.0.5"}
20+
def test_npm_importer():
21+
file = os.path.join(TEST_DATA, "npm_sample.json")
22+
result = [adv.to_dict() for adv in NpmImporter().to_advisory_data(file=file)]
23+
expected_file = os.path.join(TEST_DATA, f"parse-advisory-npm-expected.json")
24+
util_tests.check_results_against_json(result, expected_file)

0 commit comments

Comments
 (0)