Skip to content

Commit 14ad120

Browse files
committed
Use node js git security advisories instead of using npm registry
Signed-off-by: Shivam Sandbhor <shivam.sandbhor@gmail.com>
1 parent 94b5e74 commit 14ad120

4 files changed

Lines changed: 68 additions & 192 deletions

File tree

vulnerabilities/importers/npm.py

Lines changed: 46 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -36,103 +36,71 @@
3636
from packageurl import PackageURL
3737

3838
from vulnerabilities.data_source import Advisory
39-
from vulnerabilities.data_source import DataSource
39+
from vulnerabilities.data_source import GitDataSource
4040

4141
NPM_URL = 'https://registry.npmjs.org{}'
42-
PAGE = '/-/npm/v1/security/advisories?perPage=100&page=0'
4342

4443

45-
class NpmDataSource(DataSource):
44+
class NpmDataSource(GitDataSource):
4645

47-
def __init__(self, *args, **kwargs):
48-
super().__init__(*args, **kwargs)
49-
self._api_response = None
46+
def __enter__(self):
47+
super(NpmDataSource, self).__enter__()
5048
self._versions = VersionAPI()
51-
self._added_records, self._updated_records = [], []
52-
self._added_advisories, self._updated_advisories = [], []
49+
if not getattr(self, '_added_files', None):
50+
self._added_files, self._updated_files = self.file_changes(
51+
recursive=True, file_ext='json', subdir='./vuln/npm')
52+
5353

54-
def __enter__(self):
55-
self._api_response = self._fetch()
56-
self._categorize_records()
54+
55+
def updated_advisories(self) -> Set[Advisory]:
56+
files = self._updated_files.union(self._added_files)
57+
advisories = []
58+
for f in files:
59+
processed_data = self.process_file(f)
60+
if processed_data:
61+
advisories.extend(processed_data)
62+
print(advisories)
63+
return self.batch_advisories(advisories)
5764

5865
@property
5966
def versions(self): # quick hack to make it patchable
6067
return self._versions
6168

62-
def _fetch(self) -> Mapping[str, Any]:
63-
data = None
64-
nextpage = PAGE
65-
while nextpage:
66-
try:
67-
with urlopen(NPM_URL.format(nextpage)) as response:
68-
response = json.load(response)
69-
70-
if data is None:
71-
data = response
72-
else:
73-
data['objects'].extend(response.get('objects', []))
74-
75-
nextpage = response.get('urls', {}).get('next')
76-
77-
except HTTPError as error:
78-
if error.code == 404:
79-
return data
80-
else:
81-
raise
82-
83-
return data
84-
85-
def _categorize_records(self) -> None:
86-
for advisory in self._api_response['objects']:
87-
created = parse(advisory['created']).timestamp()
88-
updated = parse(advisory['updated']).timestamp()
89-
90-
if created > self.cutoff_timestamp:
91-
self._added_records.append(advisory)
92-
elif updated > self.cutoff_timestamp:
93-
self._updated_records.append(advisory)
94-
95-
def _parse(self, records: List[Mapping[str, Any]]) -> List[Advisory]:
96-
advisories = []
97-
98-
for record in records:
99-
package_name = record['module_name']
100-
all_versions = self.versions.get(package_name)
101-
aff_range = record.get('vulnerable_versions', '')
102-
fixed_range = record.get('patched_versions', '')
103-
104-
impacted_versions, resolved_versions = categorize_versions(
105-
all_versions,
106-
aff_range,
107-
fixed_range
108-
)
109-
110-
impacted_purls = _versions_to_purls(package_name, impacted_versions)
111-
resolved_purls = _versions_to_purls(package_name, resolved_versions)
112-
113-
for cve_id in record.get('cves') or ['']:
114-
advisories.append(Advisory(
115-
summary=record.get('overview', ''),
116-
cve_id=cve_id,
117-
impacted_package_urls=impacted_purls,
118-
resolved_package_urls=resolved_purls,
119-
reference_urls=[NPM_URL.format(f'/-/npm/v1/advisories/{record["id"]}')],
120-
))
69+
def process_file(self, file) -> List[Advisory]:
70+
print(file)
71+
with open(file) as f:
72+
record = json.load(f)
73+
print(record)
74+
advisories = []
75+
package_name = record['module_name']
76+
all_versions = self.versions.get(package_name)
77+
aff_range = record.get('vulnerable_versions', '')
78+
fixed_range = record.get('patched_versions', '')
79+
80+
impacted_versions, resolved_versions = categorize_versions(
81+
all_versions,
82+
aff_range,
83+
fixed_range
84+
)
85+
86+
impacted_purls = _versions_to_purls(package_name, impacted_versions)
87+
resolved_purls = _versions_to_purls(package_name, resolved_versions)
88+
89+
for cve_id in record.get('cves') or ['']:
90+
advisories.append(Advisory(
91+
summary=record.get('overview', ''),
92+
cve_id=cve_id,
93+
impacted_package_urls=impacted_purls,
94+
resolved_package_urls=resolved_purls,
95+
reference_urls=[NPM_URL.format(f'/-/npm/v1/advisories/{record["id"]}')],
96+
))
12197

12298
return advisories
12399

124-
def added_advisories(self) -> Set[Advisory]:
125-
return self.batch_advisories(self._parse(self._added_records))
126-
127-
def updated_advisories(self) -> Set[Advisory]:
128-
return self.batch_advisories(self._parse(self._updated_records))
129-
130-
131100
def _versions_to_purls(package_name, versions):
132101
purls = {f'pkg:npm/{quote(package_name)}@{v}' for v in versions}
133102
return {PackageURL.from_string(s) for s in purls}
134103

135-
136104
def categorize_versions(
137105
all_versions: Set[str],
138106
aff_version_range: str,
26.5 KB
Binary file not shown.

vulnerabilities/tests/test_data/npm_test.json

Lines changed: 0 additions & 104 deletions
This file was deleted.

vulnerabilities/tests/test_npm.py

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2323
import json
2424
import os
25+
import shutil
26+
import tempfile
2527
from unittest.mock import patch
28+
import zipfile
2629

2730
from django.test import TestCase
2831

@@ -42,34 +45,43 @@
4245
})
4346

4447

45-
@patch('vulnerabilities.importers.NpmDataSource.versions', new=MOCK_VERSION_API)
48+
@patch('vulnerabilities.importers.NpmDataSource._update_from_remote')
4649
class NpmImportTest(TestCase):
50+
51+
tempdir = None
52+
4753
@classmethod
4854
def setUpClass(cls) -> None:
49-
fixture_path = os.path.join(TEST_DATA, 'npm_test.json')
50-
with open(fixture_path) as f:
51-
cls.mock_response = json.load(f)
55+
cls.tempdir = tempfile.mkdtemp()
56+
zip_path = os.path.join(TEST_DATA, 'npm.zip')
57+
58+
with zipfile.ZipFile(zip_path, "r") as zip_ref:
59+
zip_ref.extractall(cls.tempdir)
5260

5361
cls.importer = models.Importer.objects.create(
5462
name='npm_unittests',
5563
license='',
5664
last_run=None,
5765
data_source='NpmDataSource',
58-
data_source_cfg={},
66+
data_source_cfg={
67+
'repository_url' : 'https://example.git',
68+
'working_directory': os.path.join(cls.tempdir, 'npm_test'),
69+
'create_working_directory': False,
70+
'remove_working_directory': False
71+
}
5972
)
6073

6174
@classmethod
6275
def tearDownClass(cls) -> None:
6376
# Make sure no requests for unexpected package names have been made during the tests.
77+
shutil.rmtree(cls.tempdir)
6478
assert len(MOCK_VERSION_API.cache) == 3, MOCK_VERSION_API.cache
6579

66-
def test_import(self):
80+
def test_import(self, _):
6781
runner = ImportRunner(self.importer, 5)
6882

69-
with patch(
70-
'vulnerabilities.importers.NpmDataSource._fetch',
71-
return_value=self.mock_response
72-
):
83+
84+
with patch('vulnerabilities.importers.NpmDataSource.versions', new=MOCK_VERSION_API):
7385
runner.run()
7486

7587
assert models.Vulnerability.objects.count() == 3

0 commit comments

Comments
 (0)