summaryrefslogtreecommitdiff
path: root/pgweb/security/models.py
diff options
context:
space:
mode:
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)
+