Merge branch '49-august-9-human-in-the-loop' into 'master'
Added expert review/edit document Closes #49 See merge request kbr4/riskletpy!54
This commit was merged in pull request #103.
This commit is contained in:
@@ -1,10 +1,13 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import EmailConfirmation
|
from .models import EmailConfirmation, ExpertAnalysisEmails
|
||||||
|
|
||||||
|
|
||||||
class EmailConfirmationAdmin(admin.ModelAdmin):
|
class EmailConfirmationAdmin(admin.ModelAdmin):
|
||||||
list_display= ['email', 'uuid', 'created_at']
|
list_display= ['email', 'uuid', 'created_at']
|
||||||
|
|
||||||
|
class ExpertAnalysisEmailsAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['email', 'created_at']
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(EmailConfirmation, EmailConfirmationAdmin)
|
admin.site.register(EmailConfirmation, EmailConfirmationAdmin)
|
||||||
|
admin.site.register(ExpertAnalysisEmails, ExpertAnalysisEmailsAdmin)
|
||||||
|
|||||||
21
backend/accounts/migrations/0003_expertanalysisemails.py
Normal file
21
backend/accounts/migrations/0003_expertanalysisemails.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Generated by Django 5.1.3 on 2025-08-17 15:39
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('accounts', '0002_emailconfirmation_confirmed'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ExpertAnalysisEmails',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('email', models.EmailField(max_length=254, unique=True)),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -11,3 +11,10 @@ class EmailConfirmation(models.Model):
|
|||||||
|
|
||||||
def is_expired(self):
|
def is_expired(self):
|
||||||
return now() > (self.created_at + timedelta(days=1))
|
return now() > (self.created_at + timedelta(days=1))
|
||||||
|
|
||||||
|
class ExpertAnalysisEmails(models.Model):
|
||||||
|
email = models.EmailField(unique=True)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.email
|
||||||
@@ -43,6 +43,26 @@ def send_payment_email(email):
|
|||||||
fail_silently=False,
|
fail_silently=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def send_documet_to_expert(email, document):
|
||||||
|
subject = "Document for Expert Analysis"
|
||||||
|
document_link = f"{site_domain}{reverse('core:document', args=[document.id])}"
|
||||||
|
edit_link = f"{site_domain}{reverse('admin:core_document_change', args=[document.id])}"
|
||||||
|
|
||||||
|
message = f"""
|
||||||
|
You have been assigned a document for expert analysis.
|
||||||
|
Document ID: {document.id}
|
||||||
|
Document Link: {document_link}
|
||||||
|
Edit Document: {edit_link}
|
||||||
|
"""
|
||||||
|
|
||||||
|
send_mail(
|
||||||
|
subject=subject,
|
||||||
|
message=message,
|
||||||
|
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||||
|
recipient_list=[email],
|
||||||
|
fail_silently=False,
|
||||||
|
)
|
||||||
|
|
||||||
def send_document_email(email, document_link, document):
|
def send_document_email(email, document_link, document):
|
||||||
image_io = generate_first_page_image(document)
|
image_io = generate_first_page_image(document)
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,266 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import Document, DocumentSegment, Organization, Risk, Control, DocumentTemplate, DocumentRiskControl, DemoCode
|
from .models import Document, DocumentSegment, Organization, Risk, Control, DocumentTemplate, DocumentRiskControl, DemoCode
|
||||||
from django.urls import reverse
|
from django.urls import reverse, path
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from .utils import generate_demo_code
|
from .utils import generate_demo_code, get_controls_for_risk, generate_key_findings, generate_recommendations
|
||||||
from django.urls import path
|
from .tables import get_risk_table
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from .forms import GenerateCodesForm
|
from .forms import GenerateCodesForm
|
||||||
|
from django.conf import settings
|
||||||
|
from backend.accounts.utils import send_document_email
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.admin.widgets import FilteredSelectMultiple
|
||||||
|
|
||||||
|
class DocumentRiskControlInline(admin.TabularInline):
|
||||||
|
model = DocumentRiskControl
|
||||||
|
extra = 2
|
||||||
|
max_num = 10
|
||||||
|
can_delete = False
|
||||||
|
fields = ('risk', 'control', 'weight', 'likelihood')
|
||||||
|
|
||||||
|
def get_formset(self, request, obj=None, **kwargs):
|
||||||
|
formset = super().get_formset(request, obj, **kwargs)
|
||||||
|
try:
|
||||||
|
if request.method == 'POST' and 'organization_risks' in request.POST:
|
||||||
|
risk_ids = request.POST.getlist('organization_risks')
|
||||||
|
formset.form.base_fields['risk'].queryset = Risk.objects.filter(pk__in=risk_ids)
|
||||||
|
elif obj:
|
||||||
|
formset.form.base_fields['risk'].queryset = obj.organization.risks.all()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return formset
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentAdminForm(forms.ModelForm):
|
||||||
|
organization_risks = forms.ModelMultipleChoiceField(
|
||||||
|
queryset=Risk.objects.all(),
|
||||||
|
required=False,
|
||||||
|
widget=FilteredSelectMultiple(verbose_name="Risks", is_stacked=False),
|
||||||
|
help_text="Edit the AI-selected risks for this organization."
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Document
|
||||||
|
fields = ['organization', 'status', 'key_findings', 'recomendations']
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = { 'all': ('admin/css/widgets.css',) }
|
||||||
|
js = (
|
||||||
|
'admin/js/SelectBox.js',
|
||||||
|
'admin/js/SelectFilter2.js',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
if self.instance and getattr(self.instance, 'organization_id', None):
|
||||||
|
self.fields['organization_risks'].initial = self.instance.organization.risks.all()
|
||||||
|
|
||||||
class DocumentSegmentInline(admin.StackedInline):
|
|
||||||
model = DocumentSegment
|
|
||||||
extra = 1
|
|
||||||
ordering = ['order']
|
|
||||||
fields = ('segment_type', 'content', 'order')
|
|
||||||
|
|
||||||
class DocumentAdmin(admin.ModelAdmin):
|
class DocumentAdmin(admin.ModelAdmin):
|
||||||
inlines = [DocumentSegmentInline]
|
change_form_template = "admin/core/document/change_form.html"
|
||||||
list_display = ('organization', 'created_at', 'modified_at')
|
|
||||||
search_fields = ['organization__name']
|
form = DocumentAdminForm
|
||||||
readonly_fields = ('created_at', 'modified_at')
|
inlines = [DocumentRiskControlInline]
|
||||||
|
list_display = ('organization', 'status', 'created_at', 'modified_at', 'review_link')
|
||||||
|
list_filter = ('status', 'created_at')
|
||||||
|
search_fields = ['organization__name', 'organization__email']
|
||||||
|
readonly_fields = (
|
||||||
|
'created_at', 'modified_at',
|
||||||
|
'regen_controls_action', 'regen_keyfindings_action', 'regen_recommendations_action',
|
||||||
|
)
|
||||||
|
|
||||||
|
fieldsets = (
|
||||||
|
('Organization & Risks', {
|
||||||
|
'fields': ('organization', 'organization_risks', 'regen_controls_action')
|
||||||
|
}),
|
||||||
|
('Key Findings', {
|
||||||
|
'fields': ('key_findings', 'regen_keyfindings_action')
|
||||||
|
}),
|
||||||
|
('Recommendations', {
|
||||||
|
'fields': ('recomendations', 'regen_recommendations_action')
|
||||||
|
}),
|
||||||
|
('Status', {
|
||||||
|
'fields': ('status',)
|
||||||
|
}),
|
||||||
|
('Timestamps', {
|
||||||
|
'fields': ('created_at', 'modified_at')
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
def regen_controls_action(self, obj):
|
||||||
|
return format_html('<button type="submit" name="_regen_controls" class="button">Regenerate Controls using AI</button>')
|
||||||
|
regen_controls_action.short_description = ''
|
||||||
|
|
||||||
|
def regen_keyfindings_action(self, obj):
|
||||||
|
return format_html('<button type="submit" name="_regen_key_findings" class="button">Regenerate Key Findings using AI</button>')
|
||||||
|
regen_keyfindings_action.short_description = ''
|
||||||
|
|
||||||
|
def regen_recommendations_action(self, obj):
|
||||||
|
return format_html('<button type="submit" name="_regen_recommendations" class="button">Regenerate Recommendations using AI</button>')
|
||||||
|
regen_recommendations_action.short_description = ''
|
||||||
|
|
||||||
|
def save_model(self, request, obj, form, change):
|
||||||
|
super().save_model(request, obj, form, change)
|
||||||
|
org_risks = form.cleaned_data.get('organization_risks')
|
||||||
|
if org_risks is not None and obj.organization_id:
|
||||||
|
obj.organization.risks.set(org_risks)
|
||||||
|
|
||||||
|
def _apply_post_org_risks(self, request, obj):
|
||||||
|
try:
|
||||||
|
if 'organization_risks' in request.POST and obj.organization_id:
|
||||||
|
risk_ids = [int(pk) for pk in request.POST.getlist('organization_risks') if pk]
|
||||||
|
obj.organization.risks.set(Risk.objects.filter(pk__in=risk_ids))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _regen_controls(self, obj):
|
||||||
|
obj.segments.filter(content__startswith="Identified Risks").delete()
|
||||||
|
obj.segments.filter(content__startswith="Mitigation Controls").delete()
|
||||||
|
obj.segments.filter(content__in=["Top 10 Risks Identified", "Regenerated Controls"]).delete()
|
||||||
|
obj.documentriskcontrol_set.all().delete()
|
||||||
|
top_risks = list(obj.organization.risks.all())
|
||||||
|
obj.add_segment('h1', "Top 10 Risks Identified")
|
||||||
|
risk_content = "\n\n".join([
|
||||||
|
f"Risk: {r.risk_id} - {r.risk_name} \n"
|
||||||
|
f"Category: {r.category}\n"
|
||||||
|
f"Primary Impact: {r.primary_impact} \n"
|
||||||
|
f"Secondary Impact: {r.secondary_impact}\n"
|
||||||
|
f"Tertiary Impact: {r.tretiary_impact} \n"
|
||||||
|
f"Detection Difficulty: {r.detection_difficulty} \n"
|
||||||
|
f"Recovery Complexity: {r.recovery_complexity} \n"
|
||||||
|
f"Business Impact Severity: {r.businnes_impact_severity}\n"
|
||||||
|
for r in top_risks
|
||||||
|
])
|
||||||
|
obj.add_segment('body', f"Identified Risks: \n\n{risk_content}")
|
||||||
|
controls_content = "Mitigation Controls:\n\n"
|
||||||
|
for risk in top_risks:
|
||||||
|
controls_content += f"Risk: {risk.risk_id} - {risk.risk_name}\n"
|
||||||
|
selected_controls = get_controls_for_risk(risk, organization=obj.organization)
|
||||||
|
for control_id, weight, likelihood in selected_controls:
|
||||||
|
control = Control.objects.filter(id=control_id).first()
|
||||||
|
if control:
|
||||||
|
DocumentRiskControl.objects.create(
|
||||||
|
document=obj,
|
||||||
|
risk=risk,
|
||||||
|
control=control,
|
||||||
|
weight=weight,
|
||||||
|
likelihood=likelihood
|
||||||
|
)
|
||||||
|
label = f"{control.subcategory} - {control.function or ''}".rstrip(" -")
|
||||||
|
controls_content += f" - Control: {label} (Impact Weight: {weight}/10) (Likelihood: {likelihood}/10)\n"
|
||||||
|
controls_content += "\n"
|
||||||
|
obj.add_segment('body', controls_content)
|
||||||
|
|
||||||
|
def _regen_key_findings(self, obj):
|
||||||
|
risks_top3 = get_risk_table(obj)[:3]
|
||||||
|
key_findings = generate_key_findings(obj, risks_top3)
|
||||||
|
if key_findings:
|
||||||
|
obj.key_findings = key_findings
|
||||||
|
obj.save(update_fields=['key_findings', 'modified_at'])
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _regen_recommendations(self, obj):
|
||||||
|
risks_top10 = get_risk_table(obj)[:10]
|
||||||
|
recommendations = generate_recommendations(risks_top10, obj.organization)
|
||||||
|
if recommendations:
|
||||||
|
obj.recomendations = recommendations
|
||||||
|
obj.save(update_fields=['recomendations', 'modified_at'])
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
|
||||||
|
if request.method == 'POST' and any(k in request.POST for k in ("_regen_controls", "_regen_key_findings", "_regen_recommendations")):
|
||||||
|
obj = self.get_object(request, object_id)
|
||||||
|
if obj is None:
|
||||||
|
return super().changeform_view(request, object_id, form_url, extra_context)
|
||||||
|
try:
|
||||||
|
self._apply_post_org_risks(request, obj)
|
||||||
|
if "_regen_controls" in request.POST:
|
||||||
|
self._regen_controls(obj)
|
||||||
|
self.message_user(request, "Risks and controls regenerated successfully.")
|
||||||
|
elif "_regen_key_findings" in request.POST:
|
||||||
|
if self._regen_key_findings(obj):
|
||||||
|
self.message_user(request, "Key Findings regenerated.")
|
||||||
|
else:
|
||||||
|
self.message_user(request, "Key Findings could not be generated.", level='warning')
|
||||||
|
elif "_regen_recommendations" in request.POST:
|
||||||
|
if self._regen_recommendations(obj):
|
||||||
|
self.message_user(request, "Recommendations regenerated.")
|
||||||
|
else:
|
||||||
|
self.message_user(request, "Recommendations could not be generated.", level='warning')
|
||||||
|
except Exception as e:
|
||||||
|
self.message_user(request, f"Action failed: {e}", level='error')
|
||||||
|
return redirect(reverse('admin:core_document_change', args=[obj.pk]))
|
||||||
|
return super().changeform_view(request, object_id, form_url, extra_context)
|
||||||
|
|
||||||
|
def review_link(self, obj):
|
||||||
|
url = reverse('admin:core_document_change', args=[obj.pk])
|
||||||
|
label = 'Review / Edit'
|
||||||
|
return format_html('<a class="button" href="{}">{}</a>', url, label)
|
||||||
|
review_link.short_description = 'Action'
|
||||||
|
|
||||||
|
def response_change(self, request, obj):
|
||||||
|
if "_save_send" in request.POST:
|
||||||
|
try:
|
||||||
|
url = f"{settings.SITE_DOMAIN}/pdf/{obj.id}/"
|
||||||
|
send_document_email(obj.organization.email, url, obj)
|
||||||
|
obj.status = Document.STATUS_DONE
|
||||||
|
obj.save(update_fields=['status', 'modified_at'])
|
||||||
|
self.message_user(request, "Document sent and marked as done.")
|
||||||
|
except Exception as e:
|
||||||
|
self.message_user(request, f"Failed to send document: {e}", level='error')
|
||||||
|
return redirect(reverse('admin:core_document_change', args=[obj.pk]))
|
||||||
|
return super().response_change(request, obj)
|
||||||
|
|
||||||
|
def _refresh_segments_from_current_mappings(self, obj):
|
||||||
|
|
||||||
|
obj.segments.filter(content__startswith="Identified Risks").delete()
|
||||||
|
obj.segments.filter(content__startswith="Mitigation Controls").delete()
|
||||||
|
obj.segments.filter(content__in=["Top 10 Risks Identified", "Regenerated Controls"]).delete()
|
||||||
|
|
||||||
|
top_risks = list(obj.organization.risks.all())
|
||||||
|
|
||||||
|
if top_risks:
|
||||||
|
obj.add_segment('h1', "Top 10 Risks Identified")
|
||||||
|
risk_content = "\n\n".join([
|
||||||
|
f"Risk: {r.risk_id} - {r.risk_name} \n"
|
||||||
|
f"Category: {r.category}\n"
|
||||||
|
f"Primary Impact: {r.primary_impact} \n"
|
||||||
|
f"Secondary Impact: {r.secondary_impact}\n"
|
||||||
|
f"Tertiary Impact: {r.tretiary_impact} \n"
|
||||||
|
f"Detection Difficulty: {r.detection_difficulty} \n"
|
||||||
|
f"Recovery Complexity: {r.recovery_complexity} \n"
|
||||||
|
f"Business Impact Severity: {r.businnes_impact_severity}\n"
|
||||||
|
for r in top_risks
|
||||||
|
])
|
||||||
|
obj.add_segment('body', f"Identified Risks: \n\n{risk_content}")
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
controls_by_risk = defaultdict(list)
|
||||||
|
for drc in obj.documentriskcontrol_set.select_related('risk', 'control').all():
|
||||||
|
controls_by_risk[drc.risk_id].append(drc)
|
||||||
|
|
||||||
|
controls_content = "Mitigation Controls:\n\n"
|
||||||
|
for risk in top_risks:
|
||||||
|
controls_content += f"Risk: {risk.risk_id} - {risk.risk_name}\n"
|
||||||
|
rows = controls_by_risk.get(risk.pk, [])
|
||||||
|
rows.sort(key=lambda x: (x.control.subcategory or '', x.control.function or ''))
|
||||||
|
for drc in rows:
|
||||||
|
control = drc.control
|
||||||
|
if control:
|
||||||
|
label = f"{control.subcategory} - {control.function or ''}".rstrip(" -")
|
||||||
|
controls_content += f" - Control: {label} (Impact Weight: {drc.weight}/10) (Likelihood: {drc.likelihood}/10)\n"
|
||||||
|
controls_content += "\n"
|
||||||
|
obj.add_segment('body', controls_content)
|
||||||
|
|
||||||
|
def save_related(self, request, form, formsets, change):
|
||||||
|
super().save_related(request, form, formsets, change)
|
||||||
|
obj = form.instance
|
||||||
|
try:
|
||||||
|
self._refresh_segments_from_current_mappings(obj)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
class DocumentTemplateAdmin(admin.ModelAdmin):
|
class DocumentTemplateAdmin(admin.ModelAdmin):
|
||||||
list_display = ['name', 'created_at', 'updated_at', 'preview_button']
|
list_display = ['name', 'created_at', 'updated_at', 'preview_button']
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class OrganizationForm(forms.ModelForm):
|
|||||||
'network_infrastructure', 'remote_workforce_percentage', 'third_party_vendor_access',
|
'network_infrastructure', 'remote_workforce_percentage', 'third_party_vendor_access',
|
||||||
'internal_software_development', 'geographic_scope', 'customer_base', 'customer_type',
|
'internal_software_development', 'geographic_scope', 'customer_base', 'customer_type',
|
||||||
'product_portfolio', 'supplier_base', 'it_infrastructure', 'intellectual_property',
|
'product_portfolio', 'supplier_base', 'it_infrastructure', 'intellectual_property',
|
||||||
'sensitive_data','sensitive_data_types', 'integration_level', 'ip_value', 'change_rate', 'threat_actors'
|
'sensitive_data','sensitive_data_types', 'integration_level', 'ip_value', 'change_rate', 'threat_actors', 'expert_analysis'
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'compliance_frameworks': forms.CheckboxSelectMultiple(),
|
'compliance_frameworks': forms.CheckboxSelectMultiple(),
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.1.3 on 2025-08-14 21:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0021_remove_control_subcategory_description'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='control',
|
||||||
|
name='subcategory_description',
|
||||||
|
field=models.TextField(blank=True, help_text='Subcategory Description', null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='organization',
|
||||||
|
name='expert_analysis',
|
||||||
|
field=models.BooleanField(blank=True, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.1.3 on 2025-08-14 21:03
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0022_control_subcategory_description_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='control',
|
||||||
|
name='subcategory_description',
|
||||||
|
),
|
||||||
|
]
|
||||||
18
backend/core/migrations/0024_document_status.py
Normal file
18
backend/core/migrations/0024_document_status.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.1.3 on 2025-08-14 21:51
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('core', '0023_remove_control_subcategory_description'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='document',
|
||||||
|
name='status',
|
||||||
|
field=models.CharField(choices=[('waiting', 'Waiting'), ('done', 'Done')], default='waiting', max_length=16),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -71,6 +71,7 @@ class Organization(models.Model):
|
|||||||
threat_actors = models.JSONField(null=True, blank=True, help_text="Which types of threat actors are most relevant to your organization (e.g., cybercriminals, insiders, nation-states)?")
|
threat_actors = models.JSONField(null=True, blank=True, help_text="Which types of threat actors are most relevant to your organization (e.g., cybercriminals, insiders, nation-states)?")
|
||||||
sensitive_data_types = models.JSONField(null=True, blank=True, help_text="What type of sensitive data does your organization handle?")
|
sensitive_data_types = models.JSONField(null=True, blank=True, help_text="What type of sensitive data does your organization handle?")
|
||||||
risks = models.ManyToManyField('Risk', related_name='organizations', blank=True)
|
risks = models.ManyToManyField('Risk', related_name='organizations', blank=True)
|
||||||
|
expert_analysis = models.BooleanField(null=True, blank=True)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
@@ -105,6 +106,15 @@ class Document(models.Model):
|
|||||||
organization = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name='documents')
|
organization = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name='documents')
|
||||||
created_at = models.DateTimeField(auto_now_add=True)
|
created_at = models.DateTimeField(auto_now_add=True)
|
||||||
modified_at = models.DateTimeField(auto_now=True)
|
modified_at = models.DateTimeField(auto_now=True)
|
||||||
|
|
||||||
|
STATUS_WAITING = 'waiting'
|
||||||
|
STATUS_DONE = 'done'
|
||||||
|
STATUS_CHOICES = (
|
||||||
|
(STATUS_WAITING, 'Waiting'),
|
||||||
|
(STATUS_DONE, 'Done'),
|
||||||
|
)
|
||||||
|
status = models.CharField(max_length=16, choices=STATUS_CHOICES, default=STATUS_WAITING)
|
||||||
|
|
||||||
key_findings = models.TextField(blank=True, null=True, help_text="Key findings")
|
key_findings = models.TextField(blank=True, null=True, help_text="Key findings")
|
||||||
recomendations = models.TextField(blank=True, null=True, help_text="Recommendations")
|
recomendations = models.TextField(blank=True, null=True, help_text="Recommendations")
|
||||||
|
|
||||||
|
|||||||
56
backend/core/templates/admin/core/document/change_form.html
Normal file
56
backend/core/templates/admin/core/document/change_form.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
{% extends "admin/change_form.html" %}
|
||||||
|
{% load i18n admin_urls static admin_list %}
|
||||||
|
|
||||||
|
{% block extrastyle %}
|
||||||
|
{{ block.super }}
|
||||||
|
<style>
|
||||||
|
/* Keep page within viewport and allow horizontal scroll for wide tables */
|
||||||
|
.change-form { max-width: 100%; overflow-x: hidden; }
|
||||||
|
.inline-group { overflow-x: auto; }
|
||||||
|
.inline-group table { table-layout: fixed; width: 100%; }
|
||||||
|
.inline-group table th, .inline-group table td { white-space: nowrap; }
|
||||||
|
/* Make textareas fill width */
|
||||||
|
textarea { width: 100% !important; min-height: 120px; }
|
||||||
|
/* Adjust numeric inputs smaller */
|
||||||
|
input[type=number] { width: 90px; }
|
||||||
|
/* Make select boxes reasonable width */
|
||||||
|
.related-widget-wrapper select, select { min-width: 260px; }
|
||||||
|
.inline-action { margin: 8px 0 16px; }
|
||||||
|
/* Green Save & Send button */
|
||||||
|
.btn-save-send { background-color: #28a745 !important; border-color: #28a745 !important; color: #fff !important; }
|
||||||
|
.btn-save-send:hover, .btn-save-send:focus { background-color: #218838 !important; border-color: #1e7e34 !important; color: #fff !important; }
|
||||||
|
/* Hide labels (and colons) for regen action rows */
|
||||||
|
.form-row.field-regen_controls_action label,
|
||||||
|
.form-row.field-regen_keyfindings_action label,
|
||||||
|
.form-row.field-regen_recommendations_action label { display: none; }
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2 style="margin-top:0">Review and Edit Document</h2>
|
||||||
|
<p>Use the Regenerate buttons inside each section to update content. When ready, Save and Send.</p>
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block submit_buttons_bottom %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block footer %}
|
||||||
|
{{ block.super }}
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
function addSaveSendButton() {
|
||||||
|
document.querySelectorAll('.submit-row').forEach(function(row){
|
||||||
|
var saveBtn = row.querySelector('input[name="_save"]');
|
||||||
|
if (saveBtn && !row.querySelector('button[name="_save_send"]')) {
|
||||||
|
saveBtn.insertAdjacentHTML('afterend', '\n<button type="submit" name="_save_send" class="button btn-save-send">Save and Send</button>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
addSaveSendButton();
|
||||||
|
document.addEventListener('formset:added', addSaveSendButton);
|
||||||
|
document.addEventListener('formset:removed', addSaveSendButton);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
19
backend/core/templates/payment_expert_analysis.html
Normal file
19
backend/core/templates/payment_expert_analysis.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block content %}
|
||||||
|
<section class="py-16 bg-secondary sm:py-24 p-body-full">
|
||||||
|
<div class="max-w-lg w-full mx-auto text-center shadow-lg border border-success rounded-xl p-8 bg-white">
|
||||||
|
<h2 class="text-3xl font-extrabold mb-4 text-success">Thank you.</h2>
|
||||||
|
<p class="mb-8 text-gray-700 text-lg">
|
||||||
|
Your report is awaiting expert analysis. We’ll deliver the final document to {{ email }} once the review is complete.
|
||||||
|
</p>
|
||||||
|
<a href="{% url 'core:index' %}" class="bg-transparent border-2 border-accent text-accent hover:bg-accent hover:text-primary font-semibold py-3 px-8 rounded-lg text-lg transition-all duration-300 ease-in-out">
|
||||||
|
Go back to Homepage
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% endblock content %}
|
||||||
|
|
||||||
|
{% block bottom %}
|
||||||
|
<script src="/static/js/formHandling.js"></script>
|
||||||
|
{% endblock bottom %}
|
||||||
@@ -1213,6 +1213,31 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Expert Analysis -->
|
||||||
|
<div class="mb-3 question basic-section" id="q25">
|
||||||
|
<label class="form-label mt-3">
|
||||||
|
Do u want to perform an expert analysis of your cybersecurity posture?
|
||||||
|
<br>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
This will help identify gaps and provide tailored recommendations.
|
||||||
|
</small>
|
||||||
|
</label>
|
||||||
|
<hr>
|
||||||
|
<div class="pb-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="expert_analysis" id="expert-analysis-yes" value="true">
|
||||||
|
<label class="form-check-label" for="expert-analysis-yes">
|
||||||
|
<i class="fa-solid fa-magnifying-glass"></i> Yes, I want an expert analysis
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="radio" name="expert_analysis" id="expert-analysis-no" value="false">
|
||||||
|
<label class="form-check-label" for="expert-analysis-no">
|
||||||
|
<i class="fa-solid fa-circle-xmark"></i> No, I don't need an expert analysis
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="d-flex justify-content-end mt-4">
|
<div class="d-flex justify-content-end mt-4">
|
||||||
<button type="button" class="btn btn-lg btn-outline-secondary me-3" id="back">Back</button>
|
<button type="button" class="btn btn-lg btn-outline-secondary me-3" id="back">Back</button>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import time
|
|||||||
|
|
||||||
from django.shortcuts import render, redirect , get_object_or_404
|
from django.shortcuts import render, redirect , get_object_or_404
|
||||||
from .forms import OrganizationForm
|
from .forms import OrganizationForm
|
||||||
from .models import Organization,Document, DocumentTemplate, DemoCode
|
from .models import Organization,Document, DocumentTemplate, DemoCode, DocumentRiskControl, Risk, Control
|
||||||
from backend.accounts.utils import send_confirmation_email, send_document_email
|
from backend.accounts.utils import send_confirmation_email, send_document_email, send_documet_to_expert
|
||||||
from django.contrib.admin.views.decorators import staff_member_required
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
from .utils import generate_pdf, generate_risk_graph, generate_residual_risk_graph
|
from .utils import generate_pdf, generate_risk_graph, generate_residual_risk_graph
|
||||||
from .tables import risk_matrix_table ,get_risk_table, get_safeguard_summary_table
|
from .tables import risk_matrix_table ,get_risk_table, get_safeguard_summary_table
|
||||||
@@ -20,6 +20,7 @@ from django.utils import timezone
|
|||||||
from weasyprint import HTML
|
from weasyprint import HTML
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
|
from backend.accounts.models import ExpertAnalysisEmails
|
||||||
|
|
||||||
|
|
||||||
# @login_required
|
# @login_required
|
||||||
@@ -134,7 +135,23 @@ def payment_page(request):
|
|||||||
payment_code.save()
|
payment_code.save()
|
||||||
document = Document.objects.get(organization = org)
|
document = Document.objects.get(organization = org)
|
||||||
url = f"{site_domain}/pdf/{document.id}/"
|
url = f"{site_domain}/pdf/{document.id}/"
|
||||||
|
if org.expert_analysis:
|
||||||
|
document.status = Document.STATUS_WAITING
|
||||||
|
document.save(update_fields=['status', 'modified_at'])
|
||||||
|
expert_emails_qs = ExpertAnalysisEmails.objects.values_list('email', flat=True).distinct()
|
||||||
|
expert_emails = [e for e in expert_emails_qs if e]
|
||||||
|
if expert_emails:
|
||||||
|
for email_addr in expert_emails:
|
||||||
|
try:
|
||||||
|
send_documet_to_expert(email_addr, document)
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Failed to send expert analysis email to %s", email_addr)
|
||||||
|
else:
|
||||||
|
logger.info("No expert emails configured; skipping expert notifications.")
|
||||||
|
return render(request, 'payment_expert_analysis.html', {'email': email, 'document': document})
|
||||||
send_document_email(email, url, document)
|
send_document_email(email, url, document)
|
||||||
|
document.status = Document.STATUS_DONE
|
||||||
|
document.save(update_fields=['status', 'modified_at'])
|
||||||
return redirect(url)
|
return redirect(url)
|
||||||
except DemoCode.DoesNotExist:
|
except DemoCode.DoesNotExist:
|
||||||
error = "❌ Invalid code"
|
error = "❌ Invalid code"
|
||||||
|
|||||||
Reference in New Issue
Block a user