diff --git a/README.md b/README.md index 19d9f5760..f03920100 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ python -m pytest -v vulnerabilities/tests/test_scrapers.py vulnerabilities/tests For Django based tests ``` -DJANGO_DEV=1 python manage.py test vulnerabilities/tests +DJANGO_DEV=1 python manage.py test vulnerabilities.tests ``` ## Data import diff --git a/vulnerabilities/data_dump.py b/vulnerabilities/data_dump.py index 72c206d8d..797237690 100644 --- a/vulnerabilities/data_dump.py +++ b/vulnerabilities/data_dump.py @@ -34,15 +34,11 @@ def debian_dump(extract_data, base_release='jessie'): Save data scraped from Debian' security tracker. """ for data in extract_data: - vulnerability = Vulnerability.objects.create( - summary=data.get('description', ''), - ) - VulnerabilityReference.objects.create( - vulnerability=vulnerability, - reference_id=data.get('vulnerability_id', ''), + vulnerability, _ = Vulnerability.objects.get_or_create( + cve_id=data['cve_id'], ) - pkg_name = data.get('package_name', '') + pkg_name = data['package_name'] package = Package.objects.create( name=pkg_name, type='deb', @@ -83,15 +79,11 @@ def ubuntu_dump(html): Dump data scraped from Ubuntu's security tracker. """ for data in html: - vulnerability = Vulnerability.objects.create( - summary='', - ) - VulnerabilityReference.objects.create( - vulnerability=vulnerability, - reference_id=data.get('cve_id'), + vulnerability, _ = Vulnerability.objects.get_or_create( + cve_id=data['cve_id'], ) package = Package.objects.create( - name=data.get('package_name'), + name=data['package_name'], type='deb', namespace='ubuntu' ) @@ -105,90 +97,96 @@ def archlinux_dump(extract_data): """ Save data scraped from archlinux' security tracker. """ - for item in extract_data: - cves = item['issues'] - group = item['name'] - - advisories = set(item['advisories']) - vulnerabilities = cves + list(advisories) - vulnerabilities.append(group) - packages_name = item['packages'] - - affected_version = item['affected'] - fixed_version = item['fixed'] - if not fixed_version: - fixed_version = 'None' - - vulnerability = Vulnerability.objects.create( - summary=item['type'], - ) + base_url = 'https://security.archlinux.org' - for vulnerability_id in vulnerabilities: - VulnerabilityReference.objects.create( - vulnerability=vulnerability, - reference_id=vulnerability_id, - url=f'https://security.archlinux.org/{vulnerability_id}', - ) + for avg in extract_data: + affected_packages = [] + fixed_packages = [] - for package_name in packages_name: - package_affected = Package.objects.create( + for package_name in avg['packages']: + ap, _ = Package.objects.get_or_create( name=package_name, type='pacman', namespace='archlinux', - version=affected_version - ) - ImpactedPackage.objects.create( - vulnerability=vulnerability, - package=package_affected - ) - PackageReference.objects.create( - package=package_affected, - repository=f'https://security.archlinux.org/package/{package_name}', + version=avg['affected'], ) - package_fixed = Package.objects.create( + affected_packages.append(ap) + + fp, _ = Package.objects.get_or_create( name=package_name, type='pacman', namespace='archlinux', - version=fixed_version + version=avg['fixed'], ) - ResolvedPackage.objects.create( + fixed_packages.append(fp) + + for cve_id in avg['issues']: + vulnerability, _ = Vulnerability.objects.get_or_create( + cve_id=cve_id, + ) + VulnerabilityReference.objects.create( vulnerability=vulnerability, - package=package_fixed + url=f'{base_url}/{cve_id}', ) - PackageReference.objects.create( - package=package_fixed, - repository=f'https://security.archlinux.org/package/{package_name}', + avg_name = avg['name'] + VulnerabilityReference.objects.create( + vulnerability=vulnerability, + reference_id=avg_name, + url=f'{base_url}/{avg_name}', ) + for asa in avg['advisories']: + VulnerabilityReference.objects.create( + vulnerability=vulnerability, + reference_id=asa, + url=f'{base_url}/{asa}', + ) + + for ap in affected_packages: + ImpactedPackage.objects.get_or_create( + vulnerability=vulnerability, + package=ap, + ) + + for fp in fixed_packages: + ResolvedPackage.objects.get_or_create( + vulnerability=vulnerability, + package=fp, + ) + def npm_dump(extract_data): for data in extract_data: - vulnerability = Vulnerability.objects.create( - summary=data.get('summary'), - ) - VulnerabilityReference.objects.create( - vulnerability=vulnerability, - reference_id=data.get('vulnerability_id'), - ) + package_name = data['package_name'] + advisory = data['advisory'] - affected_versions = data.get('affected_version', []) - for version in affected_versions: - package_affected = Package.objects.create( - name=data.get('package_name'), - version=version, - ) - ImpactedPackage.objects.create( - vulnerability=vulnerability, - package=package_affected + for cve_id in data['cve_ids']: + vulnerability, _ = Vulnerability.objects.get_or_create( + cve_id=cve_id, ) - fixed_versions = data.get('fixed_version', []) - for version in fixed_versions: - package_fixed = Package.objects.create( - name=data.get('package_name'), - version=version - ) - ResolvedPackage.objects.create( - vulnerability=vulnerability, - package=package_fixed - ) + if advisory: + VulnerabilityReference.objects.create( + vulnerability=vulnerability, + url=advisory, + ) + + for version in data['affected_versions']: + package_affected = Package.objects.create( + name=package_name, + version=version, + ) + ImpactedPackage.objects.create( + vulnerability=vulnerability, + package=package_affected + ) + + for version in data['fixed_versions']: + package_fixed = Package.objects.create( + name=package_name, + version=version + ) + ResolvedPackage.objects.create( + vulnerability=vulnerability, + package=package_fixed + ) diff --git a/vulnerabilities/migrations/0001_initial.py b/vulnerabilities/migrations/0001_initial.py index 43b2635cf..c8fe22d6c 100644 --- a/vulnerabilities/migrations/0001_initial.py +++ b/vulnerabilities/migrations/0001_initial.py @@ -1,6 +1,4 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.4 on 2017-09-20 22:34 -from __future__ import unicode_literals +# Generated by Django 2.2.4 on 2019-11-04 18:01 from django.db import migrations, models import django.db.models.deletion @@ -24,20 +22,24 @@ class Migration(migrations.Migration): name='Package', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('platform', models.CharField(blank=True, help_text='Package platform eg:maven', max_length=50)), - ('name', models.CharField(blank=True, help_text='Package name', max_length=50)), - ('version', models.CharField(blank=True, help_text='Package version', max_length=50)), + ('type', models.CharField(blank=True, help_text='A short code to identify the type of this package. For example: gem for a Rubygem, docker for a container, pypi for a Python Wheel or Egg, maven for a Maven Jar, deb for a Debian package, etc.', max_length=16, null=True)), + ('namespace', models.CharField(blank=True, help_text='Package name prefix, such as Maven groupid, Docker image owner, GitHub user or organization, etc.', max_length=255, null=True)), + ('name', models.CharField(blank=True, help_text='Name of the package.', max_length=100, null=True)), + ('version', models.CharField(blank=True, help_text='Version of the package.', max_length=50, null=True)), + ('qualifiers', models.CharField(blank=True, help_text='Extra qualifying data for a package such as the name of an OS, architecture, distro, etc.', max_length=1024, null=True)), + ('subpath', models.CharField(blank=True, help_text='Extra subpath within a package, relative to the package root.', max_length=200, null=True)), ], + options={ + 'abstract': False, + }, ), migrations.CreateModel( - name='PackageReference', + name='Vulnerability', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('repository', models.CharField(blank=True, help_text='Repository URL eg:http://central.maven.org', max_length=50)), - ('platform', models.CharField(blank=True, help_text='Platform eg:maven', max_length=50)), - ('name', models.CharField(blank=True, help_text='Package reference name eg:org.apache.commons.io', max_length=50)), - ('version', models.CharField(blank=True, help_text='Reference version', max_length=50)), - ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='vulnerabilities.Package')), + ('cve_id', models.CharField(help_text='CVE ID', max_length=50, null=True, unique=True)), + ('summary', models.TextField(blank=True, help_text='Summary of the vulnerability')), + ('cvss', models.FloatField(help_text='CVSS Score', max_length=100, null=True)), ], ), migrations.CreateModel( @@ -45,30 +47,24 @@ class Migration(migrations.Migration): fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='vulnerabilities.Package')), + ('vulnerability', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='vulnerabilities.Vulnerability')), ], ), migrations.CreateModel( - name='Vulnerability', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('summary', models.CharField(blank=True, help_text='Summary of the vulnerability', max_length=50)), - ('cvss', models.FloatField(help_text='CVSS Score', max_length=50, null=True)), - ], - ), - migrations.CreateModel( - name='VulnerabilityReference', + name='PackageReference', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('source', models.CharField(blank=True, help_text='Source(s) name eg:NVD', max_length=50)), - ('reference_id', models.CharField(blank=True, help_text='Reference ID, eg:CVE-ID', max_length=50)), - ('url', models.URLField(blank=True, help_text='URL of Vulnerability data', max_length=1024)), - ('vulnerability', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='vulnerabilities.Vulnerability')), + ('repository', models.CharField(blank=True, help_text='Repository URL eg:http://central.maven.org', max_length=100)), + ('platform', models.CharField(blank=True, help_text='Platform eg:maven', max_length=50)), + ('name', models.CharField(blank=True, help_text='Package reference name eg:org.apache.commons.io', max_length=50)), + ('version', models.CharField(blank=True, help_text='Reference version', max_length=50)), + ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='vulnerabilities.Package')), ], ), migrations.AddField( - model_name='resolvedpackage', - name='vulnerability', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='vulnerabilities.Vulnerability'), + model_name='package', + name='vulnerabilities', + field=models.ManyToManyField(through='vulnerabilities.ImpactedPackage', to='vulnerabilities.Vulnerability'), ), migrations.AddField( model_name='impactedpackage', @@ -80,12 +76,21 @@ class Migration(migrations.Migration): name='vulnerability', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='vulnerabilities.Vulnerability'), ), - migrations.AlterUniqueTogether( - name='vulnerabilityreference', - unique_together=set([('vulnerability', 'source', 'reference_id', 'url')]), + migrations.CreateModel( + name='VulnerabilityReference', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('source', models.CharField(blank=True, help_text='Source(s) name eg:NVD', max_length=50)), + ('reference_id', models.CharField(blank=True, help_text='Reference ID, eg:DSA-4465-1', max_length=50)), + ('url', models.URLField(blank=True, help_text='URL of Vulnerability data', max_length=1024)), + ('vulnerability', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='vulnerabilities.Vulnerability')), + ], + options={ + 'unique_together': {('vulnerability', 'source', 'reference_id', 'url')}, + }, ), migrations.AlterUniqueTogether( name='impactedpackage', - unique_together=set([('vulnerability', 'package')]), + unique_together={('vulnerability', 'package')}, ), ] diff --git a/vulnerabilities/migrations/0002_package_vulnerabilities.py b/vulnerabilities/migrations/0002_package_vulnerabilities.py deleted file mode 100644 index 595b8063e..000000000 --- a/vulnerabilities/migrations/0002_package_vulnerabilities.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.5 on 2017-09-22 19:18 -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('vulnerabilities', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='package', - name='vulnerabilities', - field=models.ManyToManyField(through='vulnerabilities.ImpactedPackage', to='vulnerabilities.Vulnerability'), - ), - ] diff --git a/vulnerabilities/migrations/0003_auto_20190406_0950.py b/vulnerabilities/migrations/0003_auto_20190406_0950.py deleted file mode 100644 index 8cb2c8455..000000000 --- a/vulnerabilities/migrations/0003_auto_20190406_0950.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 2.2 on 2019-04-06 09:50 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('vulnerabilities', '0002_package_vulnerabilities'), - ] - - operations = [ - migrations.AlterField( - model_name='vulnerability', - name='cvss', - field=models.FloatField(help_text='CVSS Score', max_length=100, null=True), - ), - migrations.AlterField( - model_name='vulnerability', - name='summary', - field=models.CharField(blank=True, help_text='Summary of the vulnerability', max_length=100), - ), - ] diff --git a/vulnerabilities/migrations/0004_auto_20190407_1838.py b/vulnerabilities/migrations/0004_auto_20190407_1838.py deleted file mode 100644 index 65716c651..000000000 --- a/vulnerabilities/migrations/0004_auto_20190407_1838.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2 on 2019-04-07 18:38 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('vulnerabilities', '0003_auto_20190406_0950'), - ] - - operations = [ - migrations.AlterField( - model_name='vulnerability', - name='summary', - field=models.TextField(blank=True, help_text='Summary of the vulnerability'), - ), - ] diff --git a/vulnerabilities/migrations/0005_auto_20190411_1644.py b/vulnerabilities/migrations/0005_auto_20190411_1644.py deleted file mode 100644 index 1fb58a022..000000000 --- a/vulnerabilities/migrations/0005_auto_20190411_1644.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2 on 2019-04-11 16:44 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('vulnerabilities', '0004_auto_20190407_1838'), - ] - - operations = [ - migrations.AlterField( - model_name='packagereference', - name='repository', - field=models.CharField(blank=True, help_text='Repository URL eg:http://central.maven.org', max_length=100), - ), - ] diff --git a/vulnerabilities/migrations/0006_auto_20190927_1438.py b/vulnerabilities/migrations/0006_auto_20190927_1438.py deleted file mode 100644 index 5d5d6b0a3..000000000 --- a/vulnerabilities/migrations/0006_auto_20190927_1438.py +++ /dev/null @@ -1,47 +0,0 @@ -# Generated by Django 2.2.4 on 2019-09-27 14:38 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('vulnerabilities', '0005_auto_20190411_1644'), - ] - - operations = [ - migrations.RemoveField( - model_name='package', - name='platform', - ), - migrations.AddField( - model_name='package', - name='namespace', - field=models.CharField(blank=True, help_text='Package name prefix, such as Maven groupid, Docker image owner, GitHub user or organization, etc.', max_length=255, null=True), - ), - migrations.AddField( - model_name='package', - name='qualifiers', - field=models.CharField(blank=True, help_text='Extra qualifying data for a package such as the name of an OS, architecture, distro, etc.', max_length=1024, null=True), - ), - migrations.AddField( - model_name='package', - name='subpath', - field=models.CharField(blank=True, help_text='Extra subpath within a package, relative to the package root.', max_length=200, null=True), - ), - migrations.AddField( - model_name='package', - name='type', - field=models.CharField(blank=True, help_text='A short code to identify the type of this package. For example: gem for a Rubygem, docker for a container, pypi for a Python Wheel or Egg, maven for a Maven Jar, deb for a Debian package, etc.', max_length=16, null=True), - ), - migrations.AlterField( - model_name='package', - name='name', - field=models.CharField(blank=True, help_text='Name of the package.', max_length=100, null=True), - ), - migrations.AlterField( - model_name='package', - name='version', - field=models.CharField(blank=True, help_text='Version of the package.', max_length=50, null=True), - ), - ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index aaec56f9b..5bebdd21d 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -29,8 +29,9 @@ class Vulnerability(models.Model): """ A software vulnerability with minimal information. - Identifiers are stored as VulnerabilityReference. + Identifiers other than CVE ID are stored as VulnerabilityReference. """ + cve_id = models.CharField(max_length=50, help_text='CVE ID', unique=True, null=True) summary = models.TextField(help_text='Summary of the vulnerability', blank=True) cvss = models.FloatField(max_length=100, help_text='CVSS Score', null=True) @@ -40,16 +41,15 @@ def __str__(self): class VulnerabilityReference(models.Model): """ - One or more remote web site references about a software - vulnerability data on such as a CVE ID and its web page - at the NVD, a bug id and similar references. + A reference to a vulnerability such as a security advisory from + a Linux distribution or language package manager. """ vulnerability = models.ForeignKey( Vulnerability, on_delete=models.CASCADE) source = models.CharField( max_length=50, help_text='Source(s) name eg:NVD', blank=True) reference_id = models.CharField( - max_length=50, help_text='Reference ID, eg:CVE-ID', blank=True) + max_length=50, help_text='Reference ID, eg:DSA-4465-1', blank=True) url = models.URLField( max_length=1024, help_text='URL of Vulnerability data', blank=True) diff --git a/vulnerabilities/scraper/debian.py b/vulnerabilities/scraper/debian.py index fe84ffc4f..80edec456 100644 --- a/vulnerabilities/scraper/debian.py +++ b/vulnerabilities/scraper/debian.py @@ -39,7 +39,7 @@ def extract_vulnerabilities(debian_data, base_release='jessie'): if not vulnerabilities or not package_name: continue - for vulnerability, details in vulnerabilities.items(): + for cve_id, details in vulnerabilities.items(): releases = details.get('releases') if not releases: continue @@ -57,7 +57,7 @@ def extract_vulnerabilities(debian_data, base_release='jessie'): package_vulnerabilities.append({ 'package_name': package_name, - 'vulnerability_id': vulnerability, + 'cve_id': cve_id, 'description': details.get('description', ''), 'status': status, 'urgency': release.get('urgency', ''), diff --git a/vulnerabilities/scraper/npm.py b/vulnerabilities/scraper/npm.py index a46a10b72..4ee02539f 100644 --- a/vulnerabilities/scraper/npm.py +++ b/vulnerabilities/scraper/npm.py @@ -47,30 +47,27 @@ def remove_spaces(x): return x -def get_all_version(package_name): +def get_all_versions(package_name): """ - Returns all available for a module + Returns all versions available for a module """ - package_url = NPM_URL.format('/'+package_name) - response = urlopen(package_url).read() - data = json.loads(response) - versions = data.get('versions', {}) - all_version = [obj for obj in versions] - return all_version + package_url = NPM_URL.format(f'/{package_name}') + data = json.load(urlopen(package_url)) + return [v for v in data.get('versions', {})] -def extract_version(package_name, aff_version_range, fixed_version_range): +def extract_versions(package_name, aff_version_range, fixed_version_range): """ - Seperate list of Affected version and fixed version from all version - using the range specified + Seperate list of affected versions and fixed versions from all version + using the ranges specified """ - + # FIXME This skips unfixed vulnerabilities if aff_version_range == '' or fixed_version_range == '': return ([], []) aff_spec = semantic_version.NpmSpec(remove_spaces(aff_version_range)) fix_spec = semantic_version.NpmSpec(remove_spaces(fixed_version_range)) - all_ver = get_all_version(package_name) + all_ver = get_all_versions(package_name) aff_ver = [] fix_ver = [] for ver in all_ver: @@ -86,23 +83,17 @@ def extract_version(package_name, aff_version_range, fixed_version_range): def extract_data(JSON): """ - Extract module name, summary, vulnerability id,severity + Extract package name, summary, CVE IDs, severity and + fixed & affected versions """ package_vulnerabilities = [] for obj in JSON.get('objects', []): if 'module_name' not in obj: continue - package_name = obj['module_name'] - summary = obj.get('overview', '') - severity = obj.get('severity', '') - vulnerability_id = obj.get('cves', []) - if len(vulnerability_id) > 0: - vulnerability_id = vulnerability_id[0] - else: - vulnerability_id = '' + package_name = obj['module_name'] - affected_version, fixed_version = extract_version( + affected_versions, fixed_versions = extract_versions( package_name, obj.get('vulnerable_versions', ''), obj.get('patched_versions', '') @@ -110,11 +101,12 @@ def extract_data(JSON): package_vulnerabilities.append({ 'package_name': package_name, - 'summary': summary, - 'vulnerability_id': vulnerability_id, - 'fixed_version': fixed_version, - 'affected_version': affected_version, - 'severity': severity + 'summary': obj.get('overview', ''), + 'cve_ids': obj.get('cves', []), + 'fixed_versions': fixed_versions, + 'affected_versions': affected_versions, + 'severity': obj.get('severity', ''), + 'advisory': obj.get('url', ''), }) return package_vulnerabilities @@ -123,16 +115,13 @@ def scrape_vulnerabilities(): """ Extract JSON From NPM registry """ - cururl = NPM_URL.format(PAGE) - response = urlopen(cururl).read() + nextpage = PAGE package_vulnerabilities = [] - while True: - data = json.loads(response) - package_vulnerabilities = package_vulnerabilities + extract_data(data) - next_page = data.get('urls', {}).get('next', False) - if next_page: - cururl = NPM_URL.format(next_page) - response = urlopen(cururl).read() - else: - break + + while nextpage: + cururl = NPM_URL.format(nextpage) + response = json.load(urlopen(cururl)) + package_vulnerabilities = package_vulnerabilities.extend(extract_data(data)) + next_page = data.get('urls', {}).get('next') + return package_vulnerabilities diff --git a/vulnerabilities/tests/__init__.py b/vulnerabilities/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/vulnerabilities/tests/test_api.py b/vulnerabilities/tests/test_api.py index b6cc8b7de..293dea767 100644 --- a/vulnerabilities/tests/test_api.py +++ b/vulnerabilities/tests/test_api.py @@ -121,8 +121,7 @@ def test_ubuntu_response(self): self.assertEqual(1, len(result['vulnerabilities'])) vuln = result['vulnerabilities'][0] - self.assertEqual(1, len(vuln['references'])) - self.assertEqual('CVE-2012-3386', vuln['references'][0]['reference_id']) + self.assertEqual(0, len(vuln['references'])) class TestSerializers(TestCase): diff --git a/vulnerabilities/tests/test_data_dump.py b/vulnerabilities/tests/test_data_dump.py index b1734ccd2..3f46b517b 100644 --- a/vulnerabilities/tests/test_data_dump.py +++ b/vulnerabilities/tests/test_data_dump.py @@ -59,24 +59,15 @@ def test_Vulnerability(self): """ self.assertEqual(3, Vulnerability.objects.count()) - self.assertTrue(Vulnerability.objects.get( - summary='Multiple stack-based buffer overflows in mimetex.cgi in mimeTeX')) - - self.assertTrue(Vulnerability.objects.get( - summary='Multiple unspecified vulnerabilities in mimeTeX')) - - self.assertTrue(Vulnerability.objects.get( - summary='librsync before 1.0.0 uses a truncated MD4 checksum \ -to match blocks')) + self.assertTrue(Vulnerability.objects.filter(cve_id='CVE-2009-1382')) + self.assertTrue(Vulnerability.objects.filter(cve_id='CVE-2009-2459')) + self.assertTrue(Vulnerability.objects.filter(cve_id='CVE-2014-8242')) def test_VulnerabilityReference(self): """ - Check that all vulnerability references from the test data are stored in the database + Check that no vulnerability references were found in the test data """ - self.assertEqual(3, VulnerabilityReference.objects.count()) - self.assertTrue(VulnerabilityReference.objects.get(reference_id='CVE-2009-1382')) - self.assertTrue(VulnerabilityReference.objects.get(reference_id='CVE-2009-2459')) - self.assertTrue(VulnerabilityReference.objects.get(reference_id='CVE-2014-8242')) + self.assertEqual(0, VulnerabilityReference.objects.count()) def test_Package(self): """ @@ -99,22 +90,22 @@ def test_ImpactedPackage(self): Check that all impacted packages from the test data are stored in the database """ impacted_pkgs = ImpactedPackage.objects.all() - impacted_pkg = impacted_pkgs[0] - self.assertEqual(1, len(impacted_pkgs)) - self.assertEqual('librsync', impacted_pkg.package.name) - self.assertEqual('0.9.7-10', impacted_pkg.package.version) + self.assertEqual(1, impacted_pkgs.count()) + + ip = impacted_pkgs[0] + self.assertEqual('librsync', ip.package.name) + self.assertEqual('0.9.7-10', ip.package.version) def test_ResolvedPackage(self): """ Check that all resolved packages from the test data are stored in the database """ resolved_pkgs = ResolvedPackage.objects.all() - resolved_pkg = resolved_pkgs[0] versions = [rp.package.version for rp in resolved_pkgs] - self.assertEqual(4, len(resolved_pkgs)) - self.assertEqual('mimetex', resolved_pkg.package.name) + self.assertEqual(4, resolved_pkgs.count()) + self.assertEqual('mimetex', resolved_pkgs[0].package.name) self.assertIn('1.50-1.1', versions) self.assertIn('1.74-1', versions) @@ -132,8 +123,7 @@ def test_data_dump(self): """ Check basic data import """ - reference = VulnerabilityReference.objects.filter(reference_id='CVE-2002-2439')[0] - self.assertEqual(reference.reference_id, 'CVE-2002-2439') + self.assertTrue(Vulnerability.objects.filter(cve_id='CVE-2002-2439')) pkgs = Package.objects.filter(name='gcc-4.6') self.assertTrue(pkgs) @@ -143,6 +133,11 @@ def test_data_dump(self): class TestArchLinuxDataDump(TestCase): + + CVE_IDS = ('CVE-2018-11362', 'CVE-2018-11361', 'CVE-2018-11360', + 'CVE-2018-11359', 'CVE-2018-11358', 'CVE-2018-11357', + 'CVE-2018-11356', 'CVE-2018-11355', 'CVE-2018-11354') + @classmethod def setUpTestData(cls): with open(os.path.join(TEST_DATA, 'archlinux.json')) as f: @@ -154,52 +149,81 @@ def test_Vulnerability(self): """ Check that all vulnerabilities from the test data are stored in the database """ - self.assertEqual(1, Vulnerability.objects.count()) - self.assertTrue(Vulnerability.objects.get(summary='multiple issues')) + self.assertEqual(len(self.CVE_IDS), Vulnerability.objects.count()) + + for cve_id in self.CVE_IDS: + self.assertTrue(Vulnerability.objects.filter(cve_id=cve_id)) def test_VulnerabilityReference(self): """ Check that all vulnerability references from the test data are stored in the database """ - self.assertEqual(14, VulnerabilityReference.objects.count()) - self.assertTrue(VulnerabilityReference.objects.get(reference_id='CVE-2018-11360')) - self.assertTrue(VulnerabilityReference.objects.get(reference_id='ASA-201805-24')) - self.assertTrue(VulnerabilityReference.objects.get(reference_id='AVG-708')) + for ref in ('ASA-201805-22', 'ASA-201805-23', 'ASA-201805-24', 'ASA-201805-25', 'AVG-708'): + self.assertEqual( + len(self.CVE_IDS), + VulnerabilityReference.objects.filter(reference_id=ref).count() + ) + + for ref in self.CVE_IDS: + url = f'https://security.archlinux.org/{ref}' + self.assertEqual(1, VulnerabilityReference.objects.filter(url=url).count()) def test_Package(self): """ Check that all packages from the test data are stored in the database """ self.assertEqual(8, Package.objects.count()) - pkgs = Package.objects.filter(name='wireshark-cli') - self.assertTrue(pkgs) - for pkg in pkgs: + for pkg in ('wireshark-common', 'wireshark-gtk', 'wireshark-cli', 'wireshark-qt'): + for ver in ('2.6.0-1', '2.6.1-1'): + self.assertTrue(Package.objects.filter(name=pkg, version=ver)) + + for pkg in Package.objects.filter(name='wireshark-cli'): self.assertEqual('pacman', pkg.type) self.assertEqual('archlinux', pkg.namespace) def test_PackageReference(self): """ - Check that all package references from the test data are stored in the database + Check that no package references were found in the test data """ - self.assertEqual(8, PackageReference.objects.count()) + self.assertEqual(0, PackageReference.objects.count()) def test_ImpactedPackage(self): """ - Check that all impacted packages from the test data are stored in the database + Check there is one ImpactedPackage for the number of packages + with the affected version number, times the number of vulnerabilities """ - impacted_pkgs = ImpactedPackage.objects.all() - impacted_pkg = impacted_pkgs[0] + packages = Package.objects.filter(version='2.6.0-1') + vulnerabilities = Vulnerability.objects.all() + + impacted_pkgs_count = ImpactedPackage.objects.count() + expected_count = packages.count() * vulnerabilities.count() + + self.assertEqual(expected_count, impacted_pkgs_count) - self.assertEqual(4, len(impacted_pkgs)) - self.assertEqual('2.6.0-1', impacted_pkg.package.version) + for pkg in packages: + for vuln in vulnerabilities: + self.assertTrue(ImpactedPackage.objects.filter( + package=pkg, + vulnerability=vuln, + )) def test_ResolvedPackage(self): """ - Check that all resolved packages from the test data are stored in the database + Check there is one ResolvedPackage for the number of packages + with the fixed version number, times the number of vulnerabilities """ - resolved_pkgs = ResolvedPackage.objects.all() - resolved_pkg = resolved_pkgs[0] + packages = Package.objects.filter(version='2.6.1-1') + vulnerabilities = Vulnerability.objects.all() + + resolved_pkgs_count = ResolvedPackage.objects.count() + expected_count = packages.count() * vulnerabilities.count() + + self.assertEqual(expected_count, resolved_pkgs_count) - self.assertEqual(4, len(resolved_pkgs)) - self.assertEqual('2.6.1-1', resolved_pkg.package.version) + for pkg in packages: + for vuln in vulnerabilities: + self.assertTrue(ResolvedPackage.objects.filter( + package=pkg, + vulnerability=vuln, + )) diff --git a/vulnerabilities/tests/test_npm.py b/vulnerabilities/tests/test_npm.py index 4ef932a97..f5cc355d0 100644 --- a/vulnerabilities/tests/test_npm.py +++ b/vulnerabilities/tests/test_npm.py @@ -23,7 +23,7 @@ from django.test import TestCase -from vulnerabilities.scraper.npm import remove_spaces, get_all_version, extract_data +from vulnerabilities.scraper.npm import remove_spaces, get_all_versions, extract_data import os import json @@ -39,20 +39,20 @@ def test_remove_space(self): res = remove_spaces(">= v1.2.1 || <= V2.1.1") self.assertEqual(res, '>=1.2.1 || <=2.1.1') - def test_get_all_version(self): - x = get_all_version('electron') + def test_get_all_versions(self): + x = get_all_versions('electron') expected = ['0.1.2', '2.0.0', '3.0.0', '4.0.0', '5.0.0', '6.0.0', '7.0.0'] self.assertTrue(set(expected) <= set(x)) def test_extract_data(self): with open(os.path.join(TEST_DATA, 'npm_test.json')) as f: - test_data = json.loads(f.read()) + test_data = json.load(f) expected = { 'package_name': 'hapi', - 'vulnerability_id': 'CVE-2014-4671', - 'fixed_version': [ + 'cve_ids': ['CVE-2014-4671'], + 'fixed_versions': [ '6.1.0', '6.2.0', '6.2.1', '6.2.2', '6.3.0', '6.4.0', '6.5.0', '6.5.1', '6.6.0', '6.7.0', '6.7.1', '6.8.0', '6.8.1', '6.9.0', '6.10.0', '6.11.0', '6.11.1', '7.0.0', @@ -78,7 +78,7 @@ def test_extract_data(self): '17.6.1', '17.6.2', '17.6.3', '16.6.4', '17.6.4', '16.6.5', '17.7.0', '16.7.0', '17.8.0', '17.8.1', '18.0.0', '17.8.2', '17.8.3', '18.0.1', '17.8.4', '18.1.0', '17.8.5'], - 'affected_version': [ + 'affected_versions': [ '0.0.1', '0.0.2', '0.0.3', '0.0.4', '0.0.5', '0.0.6', '0.1.0', '0.1.1', '0.1.2', '0.1.3', '0.2.0', '0.2.1', '0.3.0', '0.4.0', '0.4.1', '0.4.2', '0.4.3', '0.4.4', '0.5.0', '0.5.1', '0.6.0', @@ -103,11 +103,11 @@ def test_extract_data(self): } got = extract_data(test_data)[0] # Check if expected affected version and fixed version is subset of what we get from online - self.assertTrue(set(expected['fixed_version']) - <= set(got['fixed_version'])) - self.assertTrue(set(expected['affected_version']) <= set( - got['affected_version'])) + self.assertTrue(set(expected['fixed_versions']) + <= set(got['fixed_versions'])) + self.assertTrue(set(expected['affected_versions']) <= set( + got['affected_versions'])) self.assertEqual(expected['package_name'], got['package_name']) self.assertEqual(expected['severity'], got['severity']) - self.assertEqual(expected['vulnerability_id'], got['vulnerability_id']) + self.assertEqual(expected['cve_ids'], got['cve_ids']) diff --git a/vulnerabilities/tests/test_scrapers.py b/vulnerabilities/tests/test_scrapers.py index bb32f57b9..fea53bc2c 100644 --- a/vulnerabilities/tests/test_scrapers.py +++ b/vulnerabilities/tests/test_scrapers.py @@ -68,7 +68,7 @@ def test_debian_extract_vulnerabilities(): expected = [ { 'package_name': 'librsync', - 'vulnerability_id': 'CVE-2014-8242', + 'cve_id': 'CVE-2014-8242', 'description': 'librsync before 1.0.0 uses a truncated MD4 checksum to match blocks', 'status': 'open', 'urgency': 'low', @@ -77,7 +77,7 @@ def test_debian_extract_vulnerabilities(): }, { 'package_name': 'mimetex', - 'vulnerability_id': 'CVE-2009-1382', + 'cve_id': 'CVE-2009-1382', 'description': 'Multiple stack-based buffer overflows in mimetex.cgi in mimeTeX', 'status': 'resolved', 'urgency': 'medium', @@ -86,7 +86,7 @@ def test_debian_extract_vulnerabilities(): }, { 'package_name': 'mimetex', - 'vulnerability_id': 'CVE-2009-2459', + 'cve_id': 'CVE-2009-2459', 'description': 'Multiple unspecified vulnerabilities in mimeTeX', 'status': 'resolved', 'urgency': 'medium',