Promene u dizajnu dokumenata, controla, residual graph, residual tabele...

This commit is contained in:
2025-06-13 17:45:22 +02:00
parent fa734e5be9
commit aff66589e9
14 changed files with 711 additions and 2132 deletions

View File

@@ -36,7 +36,7 @@ class RiskAdmin(admin.ModelAdmin):
list_display = ['risk_id','risk_name','category']
class ControlAdmin(admin.ModelAdmin):
list_display = ('id', 'name')
list_display = ('id','safeguard_id','name', 'description')
class DocumentRiskControlAdmin(admin.ModelAdmin):
list_display = ('document', 'risk', 'control', 'weight','likelihood')

View File

@@ -15,10 +15,13 @@ class Command(BaseCommand):
reader = csv.DictReader(csv_file)
for row in reader:
safeguard = row["CIS v8.1 Safeguards (Sub-Controls)"].strip()
safeguard_id = row["Safeguard ID"].strip()
safeguard = row["Name"].strip()
description = row["Description"].strip()
Control.objects.update_or_create(
name=safeguard,
name=safeguard,
safeguard_id = safeguard_id,
description=description,
defaults={"name": safeguard},
)

View File

@@ -0,0 +1,23 @@
# Generated by Django 5.1.3 on 2025-06-13 14:01
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0014_alter_organization_customer_base'),
]
operations = [
migrations.AddField(
model_name='control',
name='description',
field=models.TextField(default=' ', help_text='Description of the control'),
),
migrations.AddField(
model_name='control',
name='safeguard_id',
field=models.CharField(help_text="Unique identifier for the safeguard (e.g. '1.1', '4.6')", max_length=10, null=True, unique=True),
),
]

View File

@@ -157,8 +157,9 @@ class Risk(models.Model):
class Control(models.Model):
id = models.AutoField(primary_key=True)
safeguard_id = models.CharField(max_length=10, unique=True, null=True, help_text="Unique identifier for the safeguard (e.g. '1.1', '4.6')")
name = models.CharField(max_length=255)
description = models.TextField(default=" ", help_text="Description of the control")
def __str__(self):
return f"{self.id} ({self.name})"

View File

@@ -18,6 +18,50 @@ def render_universal_segment(segment, context_data):
rendered = []
context = Context(context_data)
if segment_type == 'organization':
rendered.append(
f'<div class="front-page">'
f'<img src="path/to/your/logo.png" alt="Risklet Logo" class="logo">'
f'<h1>Cyber Risk Assessment Report</h1>'
f'<p>Comprehensive Evaluation and Strategic Recommendations for Enhanced Cybersecurity Posture</p>'
f'<div class="prepared-by">'
f'<p>Prepared for</p>')
for item in content:
name = Template(item.get('name', '')).render(context)
date = Template(item.get('date', '')).render(context)
if name:
rendered.append(f'<p><strong>{name}</strong></p>')
rendered.append(f'<p>Prepared by</p>'
f'<p><strong>Risklet</strong></p>'
f'</div>')
if date:
rendered.append(f'<p style="margin-top: 40px;">{date}</p>')
rendered.append(f'</div>')
return '\n'.join(rendered)
elif segment_type == 'disclaimer':
rendered.append(
f'<div class="disclaimer-page">'
f'<img src="path/to/your/logo.png" alt="Risklet Logo" class="logo">'
)
for item in content:
subtitle = Template(item.get('subtitle', '')).render(context)
description = Template(item.get('description', '')).render(context)
if subtitle:
rendered.append(f'<h3>{subtitle}</h3>')
if description:
processed_desc = []
for line in description.split('\n'):
line = line.strip()
if line:
processed_desc.append(f'<p>{line}</p>')
rendered.append('\n'.join(processed_desc))
rendered.append(f'</div>')
return '\n'.join(rendered)
else:
rendered.append(f'<div class="section">')
for item in content:
if not isinstance(item, dict):
continue
@@ -27,10 +71,10 @@ def render_universal_segment(segment, context_data):
description = Template(item.get('description', '')).render(context)
if title:
rendered.append(f'<h2 style="color: #2c3e50; margin-top: 30px;">{title}</h2>')
rendered.append(f'<h2>{title}</h2>')
if subtitle:
rendered.append(f'<h3 style="color: #34495e; margin-top: 20px;">{subtitle}</h3>')
rendered.append(f'<h3>{subtitle}</h3>')
if description:
processed_desc = []
@@ -47,7 +91,7 @@ def render_universal_segment(segment, context_data):
processed_desc.append('</ul>')
in_list = False
if line:
processed_desc.append(f'<p style="margin: 10px 0; line-height: 1.6;">{line}</p>')
processed_desc.append(f'<p>{line}</p>')
if in_list:
processed_desc.append('</ul>')
rendered.append('\n'.join(processed_desc))
@@ -68,20 +112,53 @@ def render_universal_segment(segment, context_data):
table_html.append('</tbody></table>')
rendered.append('\n'.join(table_html))
if 'html' in item:
html_template = Template(item['html'])
rendered_html = html_template.render(context)
rendered.append(rendered_html)
if 'image' in item:
image_url = Template(item['image']).render(context)
rendered.append(f'<img src="{image_url}" alt="{title}" style="max-width: 100%; height: auto; margin: 20px 0;">')
if 'warning' in item:
warning_text = Template(item['warning']).render(context)
rendered.append(f'<p style="color: #e74c3c; font-weight: bold;">{warning_text}</p>')
if 'note' in item:
note_text = Template(item['note']).render(context)
rendered.append(f'<p><em>{note_text}</p></em>')
if 'html' in segment:
html_template = Template(segment['html'])
rendered_html = html_template.render(context)
rendered.append(rendered_html)
rendered.append('</div>')
return '\n'.join(rendered)
def render_template(template_segments, context_data):
final_output = []
container_segments = []
disclaimer_segment = None
for segment in template_segments:
segment_html = render_universal_segment(segment, context_data)
final_output.append(f'<div class="segment {segment.get("segment_type", "")}">{segment_html}</div>')
segment_type = segment.get('segment_type', 'unknown')
if segment_type == 'organization':
final_output.append(render_universal_segment(segment, context_data))
elif segment_type == 'disclaimer':
disclaimer_segment = segment
else:
container_segments.append(segment)
if container_segments:
container_html = ['<div class="container">']
for segment in container_segments:
container_html.append(render_universal_segment(segment, context_data))
container_html.append('</div>')
final_output.append('\n'.join(container_html))
if disclaimer_segment:
final_output.append(render_universal_segment(disclaimer_segment, context_data))
return '\n'.join(final_output)

View File

@@ -3,14 +3,6 @@ from backend.core.utils import calculate_aggregate_likelihood, calculate_aggrega
def risk_matrix_table():
likelihood_labels = [
"Almost Certain (90-100%) (5)",
"Probable (51-89%) (4)",
"Possible (25-50%) (3)",
"Unlikely (11-24%) (2)",
"Rare (0-10%) (1)"
]
impact_labels = [
"Insignificant (1)",
"Significant (2)",
@@ -18,39 +10,28 @@ def risk_matrix_table():
"Material (4)",
"Major (5)"
]
header = ["Likelihood ↓ / Impact →"] + impact_labels
color_mapping = {
"Very Low": "lightgreen",
"Low": "green",
"Medium": "yellow",
"High": "orange",
"Critical": "red"
}
matrix = [
["Almost Certain (5)",
(5, "bg-medium"), (10, "bg-high"), (15, "bg-critical"), (20, "bg-critical"), (25, "bg-critical")
],
["Likely (4)",
(4, "bg-low"), (8, "bg-medium"), (12, "bg-high"), (16, "bg-high"), (20, "bg-critical")
],
["Probable (3)",
(3, "bg-low"), (6, "bg-low"), (9, "bg-medium"), (12, "bg-high"), (15, "bg-high")
],
["Unlikely (2)",
(2, "bg-very-low"), (4, "bg-low"), (6, "bg-medium"), (8, "bg-medium"), (10, "bg-medium")
],
["Rare (1)",
(1, "bg-very-low"), (2, "bg-very-low"), (3, "bg-low"), (4, "bg-low"), (5, "bg-medium")
],
]
def get_label(score):
if score <= 2:
return "Very Low"
elif score <= 4:
return "Low"
elif score <= 10:
return "Medium"
elif score <= 16:
return "High"
else:
return "Critical"
table_matrix_risk = [["Likelihood ↓ / Impact →"] + impact_labels]
for likelihood in range(5, 0, -1):
row = [likelihood_labels[5 - likelihood]]
for impact in range(1, 6):
score = likelihood * impact
label = get_label(score)
color_class = color_mapping[label]
row.append((score, label, color_class))
table_matrix_risk.append(row)
return table_matrix_risk
table = [header] + matrix
return table
def get_risk_table(document):
risks = (
@@ -98,3 +79,34 @@ def get_risk_table(document):
risks_with_controls.sort(key=lambda x: x['risk_score'], reverse=True)
return risks_with_controls
def get_safeguard_summary_table(risks_with_controls):
from collections import Counter
from backend.core.models import Control
safeguard_counter = Counter()
safeguard_names = {}
for risk in risks_with_controls:
for control in risk.get('controls', []):
control_id = control.get('control')
control_name = control.get('control__name')
if control_id:
safeguard_counter[control_id] += 1
safeguard_names[control_id] = control_name
summary = []
controls = Control.objects.filter(id__in=safeguard_counter.keys())
controls_map = {c.id: c for c in controls}
for control_id, count in safeguard_counter.items():
control = controls_map.get(control_id)
summary.append({
'id': control_id,
'safeguard_id': control.safeguard_id if control else '',
'name': safeguard_names.get(control_id, ''),
'description': control.description if control else '',
'count': count,
})
summary.sort(key=lambda x: x['count'], reverse=True)
return summary

File diff suppressed because it is too large Load Diff

View File

@@ -3,157 +3,14 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document PDF</title>
<style>
@page {
size: A4;
margin: 2cm;
}
* {
box-sizing: border-box;
}
@media print {
table {
page-break-inside: avoid; /* Prevent table from breaking across pages */
}
tr {
page-break-inside: avoid; /* Prevent table rows from breaking across pages */
page-break-after: auto;
}
thead {
display: table-header-group; /* Ensure table headers repeat on each page */
}
tfoot {
display: table-footer-group; /* Ensure table footers repeat on each page */
}
.page-break {
page-break-before: always; /* Force a page break before this element */
}
}
body {
margin: 0;
padding: 0;
font-family: sans-serif;
font-size: 12pt;
overflow-wrap: break-word;
}
.document-container {
width: 100%;
max-width: 100%;
padding: 0;
margin: 0;
}
.document-header {
margin-bottom: 2rem;
}
.document-meta {
color: #666;
font-size: 0.9rem;
}
.document-content {
line-height: 1.6;
}
.document-title {
font-size: 2.5rem;
margin-bottom: 1.5rem;
}
.document-subtitle {
font-size: 2rem;
margin-bottom: 1.25rem;
}
.document-h1 {
font-size: 1.75rem;
margin-bottom: 1rem;
}
.document-h2 {
font-size: 1.5rem;
margin-bottom: 0.75rem;
}
.document-h3 {
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.document-quote {
border-left: 4px solid #ccc;
margin: 1.5rem 0;
padding-left: 1rem;
font-style: italic;
}
.document-body {
margin-bottom: 1rem;
}
.green { background-color: green; color: white; }
.lightgreen { background-color: lightgreen; }
.yellow { background-color: yellow; }
.orange { background-color: orange; }
.red { background-color: red; color: white; }
table {
width: 100%;
table-layout: fixed;
word-wrap: break-word;
border-collapse: collapse;
}
th, td {
padding: 4px 6px;
font-size: 10pt;
text-align: center;
border: 1px solid #ddd;
overflow-wrap: break-word;
word-break: break-word;
}
th {
background-color: #f2f2f2;
font-weight: bold;
}
caption {
font-weight: bold;
margin-bottom: 10px;
}
.cmmi thead th {
border-bottom: 1px solid black;
}
img {
max-width: 100%;
height: auto;
}
.page-break {
page-break-after: always;
}
</style>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cyber Risk Assessment Report - {{document.organization.name}}</title>
<link rel="stylesheet" href="{% static 'css/document.css' %}">
</head>
<body>
<div class="document-container">
{% if error %}
<p style="color: red;">{{ error }}</p>
{% endif %}
<div>
{{ rendered_html|safe }}
</div>
</div>
{{ rendered_html|safe }}
</body>
</html>

View File

@@ -32,14 +32,14 @@ class TestProcessors(unittest.TestCase):
def test_render_universal_segment(self):
segment = self.template_segments[0]
result = render_universal_segment(segment, self.context_data)
self.assertIn("<h2 style=", result)
self.assertIn("<h3 style=", result)
self.assertIn("<h2>", result)
self.assertIn("<h3>", result)
self.assertIn("<ul style=", result)
self.assertIn("<div><p>Custom HTML content with Example Corp</p></div>", result)
def test_render_template(self):
result = render_template(self.template_segments, self.context_data)
self.assertIn('<div class="segment example_segment">', result)
self.assertIn('<div class="section">', result)
self.assertIn("Main Title", result)
self.assertIn("Subtitle 1", result)
self.assertIn("Custom HTML content with Example Corp", result)
@@ -47,7 +47,7 @@ class TestProcessors(unittest.TestCase):
def test_empty_segment(self):
segment = {"segment_type": "empty_segment", "content": []}
result = render_universal_segment(segment, self.context_data)
self.assertEqual(result, "")
self.assertEqual(result, '<div class="section">\n</div>')
def test_missing_html(self):
segment = {

View File

@@ -128,4 +128,45 @@ class UtilsTests(TestCase):
graph_data = generate_risk_graph(risks_with_controls)
self.assertIsInstance(graph_data, str)
self.assertTrue(len(graph_data) > 1000)
self.assertTrue(len(graph_data) > 1000)
def test_generate_residual_risk_graph_base64(self):
risks_with_controls = [
{
'risk': {'id': 1, 'name': 'Risk 1'},
'residual_impact': 3,
'residual_likelihood': 4,
},
{
'risk': {'id': 2, 'name': 'Risk 2'},
'residual_impact': 2,
'residual_likelihood': 2,
}
]
graph_data = generate_residual_risk_graph(risks_with_controls)
self.assertIsInstance(graph_data, str)
self.assertTrue(len(graph_data) > 1000)
def test_get_safeguard_summary_table_basic(self):
from backend.core.tables import get_safeguard_summary_table
risks_with_controls = [
{
'risk': {'id': 1, 'name': 'Risk 1'},
'controls': [
{'control': 101, 'control__name': 'Control A'},
{'control': 102, 'control__name': 'Control B'},
]
},
{
'risk': {'id': 2, 'name': 'Risk 2'},
'controls': [
{'control': 101, 'control__name': 'Control A'},
]
}
]
summary = get_safeguard_summary_table(risks_with_controls)
self.assertEqual(summary, [
{'id': 101, 'name': 'Control A', 'count': 2},
{'id': 102, 'name': 'Control B', 'count': 1},
])

View File

@@ -294,4 +294,51 @@ def generate_risk_graph(risks_with_controls):
buffer.close()
plt.close()
return base64.b64encode(image_png).decode("utf-8")
return base64.b64encode(image_png).decode("utf-8")
def generate_residual_risk_graph(risks_with_controls):
impacts = [risk.get('residual_impact', 0) for risk in risks_with_controls]
likelihoods = [risk.get('residual_likelihood', 0) for risk in risks_with_controls]
risk_ids = [risk['risk']['id'] for risk in risks_with_controls]
bg_img_path = find('img/graph_matrix.png')
bg_img = mpimg.imread(bg_img_path)
fig, ax = plt.subplots(figsize=(10, 8))
ax.imshow(bg_img, extent=[0.0, 5.4, 0.0, 5.4], aspect='auto')
scatter = ax.scatter(
likelihoods, impacts,
c="blue", edgecolors="white", s=500, alpha=0.9
)
for i, risk_id in enumerate(risk_ids):
ax.annotate(
str(risk_id),
(likelihoods[i], impacts[i]),
color="white",
fontsize=12,
ha="center",
va="center",
weight="bold",
)
ax.set_xticks([])
ax.set_yticks([])
ax.set_xticklabels([])
ax.set_yticklabels([])
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
ax.spines['left'].set_visible(False)
ax.spines['bottom'].set_visible(False)
buffer = io.BytesIO()
plt.savefig(buffer, format="png", transparent=True, bbox_inches='tight', pad_inches=0)
buffer.seek(0)
image_png = buffer.getvalue()
buffer.close()
plt.close()
return base64.b64encode(image_png).decode("utf-8")

View File

@@ -6,8 +6,8 @@ from .forms import OrganizationForm
from .models import Organization,Document, DocumentTemplate
from backend.accounts.utils import send_confirmation_email, send_document_email
from django.contrib.admin.views.decorators import staff_member_required
from .utils import generate_pdf, generate_risk_graph
from .tables import risk_matrix_table ,get_risk_table
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 django.conf import settings
site_domain = settings.SITE_DOMAIN
from .processors import render_template
@@ -74,8 +74,9 @@ def document(request, document_id):
document = get_object_or_404(Document, id=document_id)
risks_with_controls = get_risk_table(document)
table_risk_matrix = risk_matrix_table()
safeguard_summary_table = get_safeguard_summary_table(risks_with_controls)
graph_base64 = generate_risk_graph(risks_with_controls)
residual_graph_base64 = generate_residual_risk_graph(risks_with_controls)
template_obj = get_object_or_404(DocumentTemplate, name="Default Template")
template_content = template_obj.content
@@ -88,9 +89,15 @@ def document(request, document_id):
'risks_with_controls': risks_with_controls,
'graph': graph_base64,
'table_risk_matrix': table_risk_matrix,
'residual_graph': residual_graph_base64,
'safeguard_summary_table': safeguard_summary_table,
'table_risk_matrix_header' : table_risk_matrix[0],
'table_risk_matrix_rows': table_risk_matrix[1:],
}
rendered_content = render_template(template_segments, context)
return render(request, 'document.html', {'rendered_html': rendered_content})
return render(request, 'document.html',
{'rendered_html': rendered_content,
'document': document,})
@staff_member_required