Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions surface/inventory/admin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from django.contrib import admin
from django.contrib.admin.decorators import register
from django.urls import reverse
from django.utils.html import format_html

from . import models


Expand All @@ -8,3 +11,29 @@ class ApplicationAdmin(admin.ModelAdmin):
"""
empty for now
"""


@admin.register(models.Finding)
class FindingAdmin(admin.ModelAdmin):
list_select_related = ['content_source']

def get_list_display(self, request):
l = super().get_list_display(request).copy()
l.insert(1, 'get_content_source')
return l

def get_readonly_fields(self, request, obj=None):
l = super().get_readonly_fields(request, obj=obj).copy()
l.insert(1, 'get_content_source')
return l

def get_content_source(self, obj):
meta = obj.content_source.model_class()._meta
return format_html(
'<a href="{}">{}</a>',
reverse(f'admin:{meta.app_label}_{meta.model_name}_change', args=(obj.pk,)),
f'{meta.app_label}: {meta.verbose_name}',
)

get_content_source.short_description = 'Content Source'
get_content_source.admin_order_field = 'content_source'
63 changes: 63 additions & 0 deletions surface/inventory/migrations/0002_finding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Generated by Django 3.2.18 on 2023-04-10 21:38

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('inventory', '0001_initial_20211102'),
]

operations = [
migrations.CreateModel(
name='Finding',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.TextField(blank=True)),
('summary', models.TextField(blank=True, null=True)),
(
'severity',
models.IntegerField(
blank=True,
choices=[(1, 'Informative'), (2, 'Low'), (3, 'Medium'), (4, 'High'), (5, 'Critical')],
db_index=True,
null=True,
),
),
(
'state',
models.IntegerField(
choices=[(1, 'New'), (2, 'Open'), (3, 'Closed'), (4, 'Resolved')], db_index=True, default=1
),
),
('first_seen', models.DateTimeField(auto_now_add=True)),
('last_seen_date', models.DateTimeField(blank=True, null=True)),
(
'application',
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to='inventory.application',
verbose_name='Application',
),
),
(
'content_source',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype'),
),
(
'related_to',
models.ManyToManyField(
blank=True,
help_text='Other findings related to this one',
related_name='_inventory_finding_related_to_+',
to='inventory.Finding',
),
),
],
),
]
70 changes: 70 additions & 0 deletions surface/inventory/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from django.db import models
from django.contrib.contenttypes import models as ct_models


class Person(models.Model):
Expand All @@ -12,3 +13,72 @@ class Application(models.Model):
director = models.ForeignKey('Person', blank=True, null=True, on_delete=models.SET_NULL, related_name='+')
director_direct = models.ForeignKey('Person', blank=True, null=True, on_delete=models.SET_NULL, related_name='+')
dev_lead = models.ForeignKey('Person', blank=True, null=True, on_delete=models.SET_NULL, related_name='+')


class FindingInheritanceQS(models.QuerySet):
Comment thread
gsilvapt marked this conversation as resolved.
def get_children(self) -> list:
return [
getattr(m, m.content_source.model)
for m in self.prefetch_related("content_source__model__finding_ptr").select_related('content_source')
]


class Finding(models.Model):
Comment thread
bogdanoniga marked this conversation as resolved.
class Severity(models.IntegerChoices):
INFORMATIVE = 1
LOW = 2
MEDIUM = 3
HIGH = 4
CRITICAL = 5

class State(models.IntegerChoices):
"""
States represent a point in the workflow.
States are not Status.
Do not add a state if the transitions for that state are the same as an existing one.
"""

# to be reviewed by Security Testing: NEW -> OPEN/CLOSED
NEW = 1
# viewed by the teams, included in score: OPEN -> CLOSED
OPEN = 2
# no score, nothing to do. Final state.
CLOSED = 3
# resolved/mitigated, can be re-open: RESOLVED -> NEW/OPEN
RESOLVED = 4

content_source = models.ForeignKey(ct_models.ContentType, on_delete=models.CASCADE)

title = models.TextField(blank=True)
summary = models.TextField(null=True, blank=True)
severity = models.IntegerField(null=True, blank=True, choices=Severity.choices, db_index=True)
state = models.IntegerField(choices=State.choices, default=State.NEW, db_index=True)

first_seen = models.DateTimeField(auto_now_add=True)
last_seen_date = models.DateTimeField(blank=True, null=True)

application = models.ForeignKey(
'inventory.Application', blank=True, null=True, on_delete=models.SET_NULL, verbose_name="Application"
)

related_to = models.ManyToManyField('self', blank=True, help_text='Other findings related to this one')

objects = FindingInheritanceQS.as_manager()

def __init__(self, *args, **kwargs):
if 'content_source' not in kwargs:
kwargs['content_source'] = self.content_type()
super().__init__(*args, **kwargs)

@classmethod
def content_type(cls):
return ct_models.ContentType.objects.get_for_model(cls)

@property
def cached_content_source(self):
if self.content_source_id is not None and not Finding.content_source.is_cached(self):
self.content_source = ct_models.ContentType.objects.get_for_id(self.content_source_id)
return self.content_source

def __str__(self):
return f'{self.pk} [{self.cached_content_source.app_label}] - {self.title}'