Skip to content

Commit

Permalink
Requirements.in for automatic requirements.txt gen with hashed, CSP r…
Browse files Browse the repository at this point in the history
…eport-to feature WIP
  • Loading branch information
pparage committed Jan 24, 2025
1 parent fd90897 commit c5fc56c
Show file tree
Hide file tree
Showing 12 changed files with 1,795 additions and 999 deletions.
29 changes: 29 additions & 0 deletions requirements.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
django
django-bootstrap5
django-cors-headers
django-enumfields
django-extensions
django-icons
django-picklefield
django-widget-tweaks
djangorestframework
djangorestframework-simplejwt
drf-spectacular
drf-spectacular-sidecar
onekey-client
python-decouple
redis
weasyprint
pylookyloo
pillow
ipwhois
dnspython
pypandora
pyvulnerabilitylookup
defusedxml
matplotlib
beautifulsoup4
python3-nmap
pycrypto
cryptography
blake2signer
2,171 changes: 1,222 additions & 949 deletions requirements.txt

Large diffs are not rendered by default.

43 changes: 27 additions & 16 deletions testing/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -924,13 +924,22 @@ def analyze_csp(csp, result, header_name):
check_unsafe_directives(directives, result, header_name)
check_missing_directives(directives, result, header_name)
check_overly_permissive_directives(directives, result, header_name)
check_csp_syntax(csp, result, header_name)
# check_csp_syntax(csp, result, header_name)
check_report_uri(directives, result, header_name)


def parse_csp(csp):
return dict(
directive.split(None, 1) for directive in csp.split(';') if directive.strip())
directives = {}
for directive in csp.split(';'):
directive = directive.strip()
if not directive:
continue
parts = directive.split(None, 1)
if len(parts) == 2:
directives[parts[0]] = parts[1]
elif len(parts) == 1:
directives[parts[0]] = ""
return directives


def check_unsafe_directives(directives, result, header_name):
Expand All @@ -946,9 +955,9 @@ def check_unsafe_directives(directives, result, header_name):

def check_missing_directives(directives, result, header_name):
important_directives = ['default-src', 'script-src', 'style-src', 'img-src',
'connect-src', 'frame-src']
'connect-src'] # Removed frame-src as it's optional when default-src is set
for directive in important_directives:
if directive not in directives:
if directive not in directives and 'default-src' not in directives:
result['issues'].append(
f"{header_name}: Missing important directive '{directive}'.")
result['recommendations'].append(
Expand All @@ -957,24 +966,26 @@ def check_missing_directives(directives, result, header_name):

def check_overly_permissive_directives(directives, result, header_name):
for directive, value in directives.items():
if '*' in value:
result['issues'].append(
f"{header_name}: Overly permissive wildcard '*' found in '{directive}'.")
result['recommendations'].append(
f"Restrict the '{directive}' directive to specific sources instead of using '*'.")

values = value.split()
for val in values:
if val == '*': # Only flag standalone wildcards
result['issues'].append(
f"{header_name}: Overly permissive wildcard '*' found in '{directive}'.")
result['recommendations'].append(
f"Restrict the '{directive}' directive to specific sources instead of using '*'.")

def check_csp_syntax(csp, result, header_name):
if not re.match(r'^[a-zA-Z0-9\-]+\s+[^;]+(?:;\s*[a-zA-Z0-9\-]+\s+[^;]+)*$', csp):
result['issues'].append(f"{header_name}: CSP syntax appears to be invalid.")
result['recommendations'].append("Review and correct the CSP syntax.")
# False positives not reliable enough
#def check_csp_syntax(csp, result, header_name):
# if not re.match(r'^[a-zA-Z0-9\-]+\s+[^;]+(?:;\s*[a-zA-Z0-9\-]+\s+[^;]+)*$', csp):
# result['issues'].append(f"{header_name}: CSP syntax appears to be invalid.")
# result['recommendations'].append("Review and correct the CSP syntax.")


def check_report_uri(directives, result, header_name):
if 'report-uri' not in directives and 'report-to' not in directives:
result['issues'].append(f"{header_name}: No reporting directive found.")
result['recommendations'].append(
"Consider adding a 'report-uri' or 'report-to' directive for CSP violation reporting.")
"Consider adding a 'report-uri (deprecated)' or 'report-to' directive for CSP violation reporting.")


def check_cookies(domain: str) -> Dict[str, Any]:
Expand Down
30 changes: 30 additions & 0 deletions testing/migrations/0005_cspreport.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 5.1.5 on 2025-01-24 15:49

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


class Migration(migrations.Migration):

dependencies = [
('testing', '0004_testreport'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name='CSPReport',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('allowed_origin', models.URLField(help_text='The domain allowed to send reports to this endpoint.')),
('endpoint_uuid', models.CharField(editable=False, max_length=64, unique=True)),
('report_data', models.JSONField(default=dict)),
('timestamp', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
options={
'constraints': [models.UniqueConstraint(fields=('user', 'allowed_origin'), name='unique_user_domain')],
},
),
]
38 changes: 36 additions & 2 deletions testing/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from django.db import models

from django.contrib.auth import get_user_model
from authentication.models import User

import uuid
import hashlib

class Domain(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
Expand Down Expand Up @@ -103,3 +104,36 @@ class TestReport(models.Model):

def __str__(self):
return f"{self.test_ran}_{self.tested_site.replace('.', '-')}"


class CSPReport(models.Model):
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
allowed_origin = models.URLField(help_text="The domain allowed to send reports to this endpoint.")
endpoint_uuid = models.CharField(max_length=64, unique=True, editable=False)
report_data = models.JSONField(default=dict)
timestamp = models.DateTimeField(auto_now_add=True)

def save(self, *args, **kwargs):
if not self.endpoint_uuid:
# Create namespace using user's ID
namespace = uuid.uuid5(uuid.NAMESPACE_DNS, str(self.user.id))
# Generate UUID5 using namespace and domain
domain_uuid = uuid.uuid5(namespace, self.allowed_origin)
# Hash for additional security
self.endpoint_uuid = hashlib.blake2b(
str(domain_uuid).encode(),
digest_size=32
).hexdigest()
super().save(*args, **kwargs)

def __str__(self):
return f"{self.user.username} - {self.allowed_origin}"

class Meta:
constraints = [
models.UniqueConstraint(
fields=['user', 'allowed_origin'],
name='unique_user_domain'
)
]

67 changes: 67 additions & 0 deletions testing/templates/create_csp_endpoint.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{% extends "base.html" %}
{% block content %}
<div class="container py-4">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">
<h2 class="card-title mb-0">Create CSP Report Endpoint</h2>
</div>
<div class="card-body">
{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}

{% if endpoint_url %}
<div class="alert alert-success">
<h4 class="alert-heading">Endpoint Created Successfully!</h4>
<p>Your CSP report endpoint URL is:</p>
<div class="input-group mb-3">
<input type="text" class="form-control" value="{{ endpoint_url }}" id="endpoint-url" readonly>
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('endpoint-url')">
Copy
</button>
</div>

<hr>
<p>Add these headers to your website's configuration:</p>
<div class="bg-light p-3 rounded">
<code class="d-block">Content-Security-Policy-Report-Only: default-src 'self'; report-uri {{ endpoint_url }};</code>
<small class="text-muted">Use this header to test your CSP without enforcing it</small>

<code class="d-block mt-3">Content-Security-Policy: default-src 'self'; report-uri {{ endpoint_url }};</code>
<small class="text-muted">Use this header to enforce your CSP</small>
</div>
</div>
{% endif %}

<form method="POST" class="mt-4">
{% csrf_token %}
<div class="mb-3">
<label for="allowed_origin" class="form-label">Allowed Origin:</label>
<input type="url" class="form-control" id="allowed_origin" name="allowed_origin"
placeholder="https://example.com" required>
<div class="form-text">Enter the domain that will be sending CSP reports</div>
</div>
<button type="submit" class="btn btn-primary">Create Endpoint</button>
</form>
</div>
</div>
</div>
</div>
</div>

<script>
function copyToClipboard(elementId) {
const element = document.getElementById(elementId);
element.select();
document.execCommand('copy');

// Optional: Show feedback
const button = element.nextElementSibling;
const originalText = button.innerText;
button.innerText = 'Copied!';
setTimeout(() => button.innerText = originalText, 2000);
}
</script>
{% endblock %}
99 changes: 99 additions & 0 deletions testing/templates/csp_endpoints.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{# manage_csp_endpoints.html #}
{% extends "base.html" %}
{% block content %}
<div class="container">
<h2>Manage CSP Report Endpoints</h2>
<a href="{% url 'create_csp_endpoint' %}" class="btn btn-primary mb-3">Create New Endpoint</a>

{% if endpoints %}
<table class="table">
<thead>
<tr>
<th>Allowed Origin</th>
<th>Endpoint URL</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for endpoint in endpoints %}
<tr>
<td>{{ endpoint.allowed_origin }}</td>
<td>https://testing.nc3.lu/uri-report/{{ endpoint.endpoint_uuid }}/</td>
<td>{{ endpoint.timestamp|date:"Y-m-d H:i" }}</td>
<td>
<a href="{% url 'view_csp_reports' endpoint.endpoint_uuid %}" class="btn btn-sm btn-info">View Reports</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No CSP report endpoints configured yet.</p>
{% endif %}
</div>
{% endblock %}

{# create_csp_endpoint.html #}
{% extends "base.html" %}
{% block content %}
<div class="container">
<h2>Create CSP Report Endpoint</h2>

{% if error %}
<div class="alert alert-danger">{{ error }}</div>
{% endif %}

{% if endpoint_url %}
<div class="alert alert-success">
<h4>Endpoint Created Successfully!</h4>
<p>Your CSP report endpoint URL is:</p>
<code>{{ endpoint_url }}</code>
<p class="mt-3">Add this to your Content-Security-Policy header:</p>
<code>report-uri {{ endpoint_url }};</code>
</div>
{% endif %}

<form method="POST" class="mt-4">
{% csrf_token %}
<div class="form-group">
<label for="allowed_origin">Allowed Origin:</label>
<input type="url" class="form-control" id="allowed_origin" name="allowed_origin"
placeholder="https://example.com" required>
<small class="form-text text-muted">Enter the domain that will be sending CSP reports</small>
</div>
<button type="submit" class="btn btn-primary mt-3">Create Endpoint</button>
</form>
</div>
{% endblock %}

{# view_csp_reports.html #}
{% extends "base.html" %}
{% block content %}
<div class="container">
<h2>CSP Reports for {{ endpoint.allowed_origin }}</h2>

{% if reports %}
<table class="table">
<thead>
<tr>
<th>Timestamp</th>
<th>Violation Details</th>
</tr>
</thead>
<tbody>
{% for report in reports %}
<tr>
<td>{{ report.timestamp|date:"Y-m-d H:i:s" }}</td>
<td>
<pre><code>{{ report.report_data|json }}</code></pre>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p>No CSP violation reports received yet.</p>
{% endif %}
</div>
{% endblock %}
Loading

0 comments on commit c5fc56c

Please sign in to comment.