summaryrefslogtreecommitdiff
path: root/pgweb/security/models.py
diff options
context:
space:
mode:
authorMagnus Hagander2018-01-25 20:59:13 +0000
committerMagnus Hagander2018-01-25 20:59:13 +0000
commit0cb56d93554926d087dfb696f608f9b51bfc7cfc (patch)
tree208b180b049bee4ad4b263faa8ebb677d1a4d9be /pgweb/security/models.py
parentd0aa8ac11910e7352d83dfe281afebb57cfde554 (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.py111
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)
+