diff options
author | Magnus Hagander | 2018-01-25 20:59:13 +0000 |
---|---|---|
committer | Magnus Hagander | 2018-01-25 20:59:13 +0000 |
commit | 0cb56d93554926d087dfb696f608f9b51bfc7cfc (patch) | |
tree | 208b180b049bee4ad4b263faa8ebb677d1a4d9be /pgweb/security/models.py | |
parent | d0aa8ac11910e7352d83dfe281afebb57cfde554 (diff) |
Database:ify the list of security patches
This finally moves the patches into the db, which makes it a lot easier
to filter patches in the views.
It also adds the new way of categorising patches, which is assigning
them a CVSSv3 score.
For now, there are no public views to this, and the old static pages
remain. This is so we can backfill all existing security patches before
we make it public.
Diffstat (limited to 'pgweb/security/models.py')
-rw-r--r-- | pgweb/security/models.py | 111 |
1 files changed, 111 insertions, 0 deletions
diff --git a/pgweb/security/models.py b/pgweb/security/models.py new file mode 100644 index 00000000..e4ec6563 --- /dev/null +++ b/pgweb/security/models.py @@ -0,0 +1,111 @@ +from django.db import models +from django.core.validators import ValidationError + +import re + +from pgweb.core.models import Version +from pgweb.news.models import NewsArticle + +import cvss + +vector_choices = {k:list(v.items()) for k,v in cvss.constants3.METRICS_VALUE_NAMES.items()} + +component_choices = ( + ('core server', 'Core server product'), + ('client', 'Client library or application only'), + ('contrib module', 'Contrib module only'), + ('client contrib module', 'Client contrib module only'), + ('packaging', 'Packaging, e.g. installers or RPM'), + ('other', 'Other'), +) + +re_cve = re.compile('^(\d{4})-(\d{4,5})$') +def cve_validator(val): + if not re_cve.match(val): + raise ValidationError("Enter CVE in format 0000-0000 without the CVE text") + +def other_vectors_validator(val): + if val != val.upper(): + raise ValidationError("Vector must be uppercase") + + try: + for vector in val.split('/'): + k,v = vector.split(':') + if not cvss.constants3.METRICS_VALUES.has_key(k): + raise ValidationError("Metric {0} is unknown".format(k)) + if k in ('AV', 'AC', 'PR', 'UI', 'S', 'C', 'I', 'A'): + raise ValidationError("Metric {0} must be specified in the dropdowns".format(k)) + if not cvss.constants3.METRICS_VALUES[k].has_key(v): + raise ValidationError("Metric {0} has unknown value {1}. Valind ones are: {2}".format( + k,v, + ", ".join(cvss.constants3.METRICS_VALUES[k].keys()), + )) + except ValidationError, ve: + raise + except Exception, e: + raise ValidationError("Failed to parse vectors: %s" % e) + +class SecurityPatch(models.Model): + public = models.BooleanField(null=False, blank=False, default=False) + newspost = models.ForeignKey(NewsArticle, null=True, blank=True) + cve = models.CharField(max_length=32, null=False, blank=True, validators=[cve_validator,]) + cvenumber = models.IntegerField(null=False, blank=False, db_index=True) + detailslink = models.URLField(null=False, blank=True) + description = models.TextField(null=False, blank=False) + component = models.CharField(max_length=32, null=False, blank=False, help_text="If multiple components, choose the most critical one", choices=component_choices) + + versions = models.ManyToManyField(Version, through='SecurityPatchVersion') + + vector_av = models.CharField(max_length=1, null=False, blank=True, verbose_name="Attack Vector", choices=vector_choices['AV']) + vector_ac = models.CharField(max_length=1, null=False, blank=True, verbose_name="Attack Complexity", choices=vector_choices['AC']) + vector_pr = models.CharField(max_length=1, null=False, blank=True, verbose_name="Privileges Required", choices=vector_choices['PR']) + vector_ui = models.CharField(max_length=1, null=False, blank=True, verbose_name="User Interaction", choices=vector_choices['UI']) + vector_s = models.CharField(max_length=1, null=False, blank=True, verbose_name="Scope", choices=vector_choices['S']) + vector_c = models.CharField(max_length=1, null=False, blank=True, verbose_name="Confidentiality Impact", choices=vector_choices['C']) + vector_i = models.CharField(max_length=1, null=False, blank=True, verbose_name="Integrity Impact", choices=vector_choices['I']) + vector_a = models.CharField(max_length=1, null=False, blank=True, verbose_name="Availability Impact", choices=vector_choices['A']) + legacyscore = models.CharField(max_length=1, null=False, blank=True, verbose_name='Legacy score', choices=(('A', 'A'),('B','B'),('C','C'),('D','D'))) + + purge_urls = ('/support/security/', ) + + def save(self, force_insert=False, force_update=False): + # Calculate a number from the CVE, that we can use to sort by. We need to + # do this, because CVEs can have 4 or 5 digit second parts... + if self.cve == '': + self.cvenumber = 0 + else: + m = re_cve.match(self.cve) + if not m: + raise ValidationError("Invalid CVE, should not get here!") + self.cvenumber = 100000 * int(m.groups(0)[0]) + int(m.groups(0)[1]) + super(SecurityPatch, self).save(force_insert, force_update) + + def __unicode__(self): + return self.cve + + @property + def cvssvector(self): + if not self.vector_av: + return None + s = 'AV:{0}/AC:{1}/PR:{2}/UI:{3}/S:{4}/C:{5}/I:{6}/A:{7}'.format( + self.vector_av, self.vector_ac, self.vector_pr, self.vector_ui, + self.vector_s, self.vector_c, self.vector_i, self.vector_a) + return s + + @property + def cvssscore(self): + try: + c = cvss.CVSS3("CVSS:3.0/" + self.cvssvector) + return c.base_score + except Exception, e: + return -1 + + class Meta: + verbose_name_plural = 'Security patches' + ordering = ('-cvenumber',) + +class SecurityPatchVersion(models.Model): + patch = models.ForeignKey(SecurityPatch, null=False, blank=False) + version = models.ForeignKey(Version, null=False, blank=False) + fixed_minor = models.IntegerField(null=False, blank=False) + |