diff --git a/vulnerabilities/admin.py b/vulnerabilities/admin.py index 3936d2896..01cf0b8d1 100644 --- a/vulnerabilities/admin.py +++ b/vulnerabilities/admin.py @@ -37,7 +37,7 @@ class VulnerabilityAdmin(admin.ModelAdmin): @admin.register(VulnerabilityReference) class VulnerabilityReferenceAdmin(admin.ModelAdmin): - search_fields = ["vulnerability__vulnerability_id", "reference_id", "url"] + search_fields = ["vulnerabilityrelatedreference__vulnerability__id", "reference_id", "url"] @admin.register(Package) diff --git a/vulnerabilities/api.py b/vulnerabilities/api.py index a9c6e31df..1b339070e 100644 --- a/vulnerabilities/api.py +++ b/vulnerabilities/api.py @@ -30,6 +30,7 @@ from rest_framework.decorators import action from rest_framework.response import Response +from vulnerabilities.models import Alias from vulnerabilities.models import Package from vulnerabilities.models import Vulnerability from vulnerabilities.models import VulnerabilityReference @@ -44,10 +45,11 @@ class Meta: class VulnerabilityReferenceSerializer(serializers.ModelSerializer): scores = VulnerabilitySeveritySerializer(many=True, source="vulnerabilityseverity_set") + reference_url = serializers.CharField(source="url") class Meta: model = VulnerabilityReference - fields = ["reference_id", "url", "scores"] + fields = ["reference_url", "reference_id", "scores"] class MinimalPackageSerializer(serializers.HyperlinkedModelSerializer): @@ -71,36 +73,64 @@ class MinimalVulnerabilitySerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Vulnerability - fields = ["url", "vulnerability_id", "references", "summary"] + fields = ["url", "vulnerability_id", "summary", "references"] + + +class AliasSerializer(serializers.HyperlinkedModelSerializer): + """ + Used for nesting inside package focused APIs. + """ + + class Meta: + model = Alias + fields = ["alias"] class VulnerabilitySerializer(serializers.HyperlinkedModelSerializer): - resolved_packages = MinimalPackageSerializer(many=True, source="resolved_to", read_only=True) - unresolved_packages = MinimalPackageSerializer( - many=True, source="vulnerable_to", read_only=True - ) + fixed_packages = MinimalPackageSerializer(many=True, source="resolved_to", read_only=True) + affected_packages = MinimalPackageSerializer(many=True, source="vulnerable_to", read_only=True) references = VulnerabilityReferenceSerializer(many=True, source="vulnerabilityreference_set") + aliases = AliasSerializer(many=True, source="alias") class Meta: model = Vulnerability - fields = "__all__" + fields = [ + "url", + "vulnerability_id", + "summary", + "aliases", + "fixed_packages", + "affected_packages", + "references", + ] class PackageSerializer(serializers.HyperlinkedModelSerializer): - unresolved_vulnerabilities = MinimalVulnerabilitySerializer( + purl = serializers.CharField(source="package_url") + affected_by_vulnerabilities = MinimalVulnerabilitySerializer( many=True, source="vulnerable_to", read_only=True ) - resolved_vulnerabilities = MinimalVulnerabilitySerializer( + fixing_vulnerabilities = MinimalVulnerabilitySerializer( many=True, source="resolved_to", read_only=True ) - purl = serializers.CharField(source="package_url") class Meta: model = Package - exclude = ["vulnerabilities"] + fields = [ + "url", + "purl", + "type", + "namespace", + "name", + "version", + "qualifiers", + "subpath", + "affected_by_vulnerabilities", + "fixing_vulnerabilities", + ] class PackageFilterSet(filters.FilterSet): diff --git a/vulnerabilities/importers/alpine_linux.py b/vulnerabilities/importers/alpine_linux.py index 069392d1a..66d87599d 100644 --- a/vulnerabilities/importers/alpine_linux.py +++ b/vulnerabilities/importers/alpine_linux.py @@ -54,7 +54,6 @@ class AlpineImporter(Importer): license_url = "https://secdb.alpinelinux.org/license.txt" def advisory_data(self) -> Iterable[AdvisoryData]: - advisories = [] page_response_content = fetch_response(BASE_URL).content advisory_directory_links = fetch_advisory_directory_links(page_response_content) advisory_links = [] @@ -68,8 +67,7 @@ def advisory_data(self) -> Iterable[AdvisoryData]: if not record["packages"]: LOGGER.error(f'"packages" not found in {link!r}') continue - advisories.extend(process_record(record)) - return advisories + yield from process_record(record) def fetch_response(url): @@ -127,7 +125,7 @@ def check_for_attributes(record) -> bool: return True -def process_record(record: dict) -> List[AdvisoryData]: +def process_record(record: dict) -> Iterable[AdvisoryData]: """ Return a list of AdvisoryData objects by processing data present in that `record` @@ -136,22 +134,18 @@ def process_record(record: dict) -> List[AdvisoryData]: LOGGER.error(f'"packages" not found in this record {record!r}') return [] - advisories: List[AdvisoryData] = [] - for package in record["packages"]: if not package["pkg"]: LOGGER.error(f'"pkg" not found in this package {package!r}') continue if not check_for_attributes(record): continue - loaded_advisories = load_advisories( + yield from load_advisories( package["pkg"], record["distroversion"], record["reponame"], record["archs"], ) - advisories.extend(loaded_advisories) - return advisories def load_advisories( diff --git a/vulnerabilities/migrations/0011_vulnerability_packages_alter_package_vulnerabilities_and_more.py b/vulnerabilities/migrations/0011_vulnerability_packages_alter_package_vulnerabilities_and_more.py new file mode 100644 index 000000000..a33e5b599 --- /dev/null +++ b/vulnerabilities/migrations/0011_vulnerability_packages_alter_package_vulnerabilities_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.0.3 on 2022-04-26 08:42 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('vulnerabilities', '0010_vulnerabilityrelatedreference_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='vulnerability', + name='packages', + field=models.ManyToManyField(through='vulnerabilities.PackageRelatedVulnerability', to='vulnerabilities.package'), + ), + migrations.AlterField( + model_name='package', + name='vulnerabilities', + field=models.ManyToManyField(through='vulnerabilities.PackageRelatedVulnerability', to='vulnerabilities.vulnerability'), + ), + migrations.AlterField( + model_name='packagerelatedvulnerability', + name='package', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='vulnerabilities.package'), + ), + ] diff --git a/vulnerabilities/migrations/0012_alter_vulnerability_vulnerability_id.py b/vulnerabilities/migrations/0012_alter_vulnerability_vulnerability_id.py new file mode 100644 index 000000000..244fc6ac8 --- /dev/null +++ b/vulnerabilities/migrations/0012_alter_vulnerability_vulnerability_id.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.4 on 2022-05-03 09:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('vulnerabilities', '0011_vulnerability_packages_alter_package_vulnerabilities_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='vulnerability', + name='vulnerability_id', + field=models.CharField(blank=True, help_text='Unique identifier for a vulnerability in the external representation. It is prefixed with VULCOID-', max_length=20, unique=True), + ), + ] diff --git a/vulnerabilities/migrations/0013_auto_20220503_0941.py b/vulnerabilities/migrations/0013_auto_20220503_0941.py new file mode 100644 index 000000000..8077a1561 --- /dev/null +++ b/vulnerabilities/migrations/0013_auto_20220503_0941.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.4 on 2022-05-03 09:41 + +from django.db import migrations + +from django.utils.http import int_to_base36 + +class Migration(migrations.Migration): + + dependencies = [ + ('vulnerabilities', '0012_alter_vulnerability_vulnerability_id'), + ] + + def save_vulnerability_id(apps, schema_editor): + Vulnerabilities = apps.get_model("vulnerabilities", "Vulnerability") + for vulnerability in Vulnerabilities.objects.all(): + if not vulnerability.vulnerability_id: + vulnerability.vulnerability_id = f"VULCOID-{int_to_base36(vulnerability.id).upper()}" + vulnerability.save() + + operations = [ + migrations.RunPython(save_vulnerability_id) + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 170bb7edc..6d8982763 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -29,6 +29,7 @@ from django.core.validators import MaxValueValidator from django.core.validators import MinValueValidator from django.db import models +from django.utils.http import int_to_base36 from packageurl import PackageURL from packageurl.contrib.django.models import PackageURLMixin @@ -47,12 +48,12 @@ class Vulnerability(models.Model): stored as ``Alias``. """ - vulnerability_id = models.UUIDField( - default=uuid.uuid4, - editable=False, + vulnerability_id = models.CharField( unique=True, - help_text="Unique identifier for a vulnerability in this database, assigned automatically. " - "In the external representation it is prefixed with VULCOID-", + blank=True, + max_length=20, + help_text="Unique identifier for a vulnerability in the external representation. " + "It is prefixed with VULCOID-", ) summary = models.TextField( @@ -63,17 +64,23 @@ class Vulnerability(models.Model): references = models.ManyToManyField( to="VulnerabilityReference", through="VulnerabilityRelatedReference" ) + packages = models.ManyToManyField( + to="Package", + through="PackageRelatedVulnerability", + ) - @property - def vulcoid(self): - return f"VULCOID-{self.vulnerability_id}" + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + if not self.vulnerability_id: + self.vulnerability_id = f"VULCOID-{int_to_base36(self.id).upper()}" + super().save(update_fields=["vulnerability_id"]) @property def vulnerable_to(self): """ Return packages that are vulnerable to this vulnerability. """ - return self.packages.filter(vulnerabilities__packagerelatedvulnerability__fix=False) + return self.packages.filter(packagerelatedvulnerability__fix=False) @property def resolved_to(self): @@ -81,10 +88,18 @@ def resolved_to(self): Returns packages that first received patch against this vulnerability in their particular version history. """ - return self.packages.filter(vulnerabilities__packagerelatedvulnerability__fix=True) + return self.packages.filter(packagerelatedvulnerability__fix=True) + + @property + def alias(self): + """ + Returns packages that first received patch against this vulnerability + in their particular version history. + """ + return self.aliases.all() def __str__(self): - return self.vulcoid + return self.vulnerability_id class Meta: verbose_name_plural = "Vulnerabilities" @@ -150,10 +165,7 @@ class Package(PackageURLMixin): """ vulnerabilities = models.ManyToManyField( - to="Vulnerability", - through="PackageRelatedVulnerability", - through_fields=("package", "vulnerability"), - related_name="packages", + to="Vulnerability", through="PackageRelatedVulnerability" ) # Remove the `qualifers` and `set_package_url` overrides after @@ -218,8 +230,14 @@ def __str__(self): class PackageRelatedVulnerability(models.Model): # TODO: Fix related_name - package = models.ForeignKey(Package, on_delete=models.CASCADE, related_name="package") - vulnerability = models.ForeignKey(Vulnerability, on_delete=models.CASCADE) + package = models.ForeignKey( + Package, + on_delete=models.CASCADE, + ) + vulnerability = models.ForeignKey( + Vulnerability, + on_delete=models.CASCADE, + ) created_by = models.CharField( max_length=100, blank=True, diff --git a/vulnerabilities/templates/package_update.html b/vulnerabilities/templates/package_update.html index 881b3ef84..b45afec74 100644 --- a/vulnerabilities/templates/package_update.html +++ b/vulnerabilities/templates/package_update.html @@ -30,7 +30,7 @@
Vulnerable To
+Affected By