Skip to content

Commit c5fc56c

Browse files
committed
Requirements.in for automatic requirements.txt gen with hashed, CSP report-to feature WIP
1 parent fd90897 commit c5fc56c

File tree

12 files changed

+1795
-999
lines changed

12 files changed

+1795
-999
lines changed

requirements.in

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
django
2+
django-bootstrap5
3+
django-cors-headers
4+
django-enumfields
5+
django-extensions
6+
django-icons
7+
django-picklefield
8+
django-widget-tweaks
9+
djangorestframework
10+
djangorestframework-simplejwt
11+
drf-spectacular
12+
drf-spectacular-sidecar
13+
onekey-client
14+
python-decouple
15+
redis
16+
weasyprint
17+
pylookyloo
18+
pillow
19+
ipwhois
20+
dnspython
21+
pypandora
22+
pyvulnerabilitylookup
23+
defusedxml
24+
matplotlib
25+
beautifulsoup4
26+
python3-nmap
27+
pycrypto
28+
cryptography
29+
blake2signer

requirements.txt

Lines changed: 1222 additions & 949 deletions
Large diffs are not rendered by default.

testing/helpers.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -924,13 +924,22 @@ def analyze_csp(csp, result, header_name):
924924
check_unsafe_directives(directives, result, header_name)
925925
check_missing_directives(directives, result, header_name)
926926
check_overly_permissive_directives(directives, result, header_name)
927-
check_csp_syntax(csp, result, header_name)
927+
# check_csp_syntax(csp, result, header_name)
928928
check_report_uri(directives, result, header_name)
929929

930930

931931
def parse_csp(csp):
932-
return dict(
933-
directive.split(None, 1) for directive in csp.split(';') if directive.strip())
932+
directives = {}
933+
for directive in csp.split(';'):
934+
directive = directive.strip()
935+
if not directive:
936+
continue
937+
parts = directive.split(None, 1)
938+
if len(parts) == 2:
939+
directives[parts[0]] = parts[1]
940+
elif len(parts) == 1:
941+
directives[parts[0]] = ""
942+
return directives
934943

935944

936945
def check_unsafe_directives(directives, result, header_name):
@@ -946,9 +955,9 @@ def check_unsafe_directives(directives, result, header_name):
946955

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

958967
def check_overly_permissive_directives(directives, result, header_name):
959968
for directive, value in directives.items():
960-
if '*' in value:
961-
result['issues'].append(
962-
f"{header_name}: Overly permissive wildcard '*' found in '{directive}'.")
963-
result['recommendations'].append(
964-
f"Restrict the '{directive}' directive to specific sources instead of using '*'.")
965-
969+
values = value.split()
970+
for val in values:
971+
if val == '*': # Only flag standalone wildcards
972+
result['issues'].append(
973+
f"{header_name}: Overly permissive wildcard '*' found in '{directive}'.")
974+
result['recommendations'].append(
975+
f"Restrict the '{directive}' directive to specific sources instead of using '*'.")
966976

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

972983

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

979990

980991
def check_cookies(domain: str) -> Dict[str, Any]:
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Generated by Django 5.1.5 on 2025-01-24 15:49
2+
3+
import django.db.models.deletion
4+
from django.conf import settings
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('testing', '0004_testreport'),
12+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='CSPReport',
18+
fields=[
19+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20+
('allowed_origin', models.URLField(help_text='The domain allowed to send reports to this endpoint.')),
21+
('endpoint_uuid', models.CharField(editable=False, max_length=64, unique=True)),
22+
('report_data', models.JSONField(default=dict)),
23+
('timestamp', models.DateTimeField(auto_now_add=True)),
24+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
25+
],
26+
options={
27+
'constraints': [models.UniqueConstraint(fields=('user', 'allowed_origin'), name='unique_user_domain')],
28+
},
29+
),
30+
]

testing/models.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from django.db import models
2-
2+
from django.contrib.auth import get_user_model
33
from authentication.models import User
4-
4+
import uuid
5+
import hashlib
56

67
class Domain(models.Model):
78
user = models.ForeignKey(User, on_delete=models.CASCADE)
@@ -103,3 +104,36 @@ class TestReport(models.Model):
103104

104105
def __str__(self):
105106
return f"{self.test_ran}_{self.tested_site.replace('.', '-')}"
107+
108+
109+
class CSPReport(models.Model):
110+
user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE)
111+
allowed_origin = models.URLField(help_text="The domain allowed to send reports to this endpoint.")
112+
endpoint_uuid = models.CharField(max_length=64, unique=True, editable=False)
113+
report_data = models.JSONField(default=dict)
114+
timestamp = models.DateTimeField(auto_now_add=True)
115+
116+
def save(self, *args, **kwargs):
117+
if not self.endpoint_uuid:
118+
# Create namespace using user's ID
119+
namespace = uuid.uuid5(uuid.NAMESPACE_DNS, str(self.user.id))
120+
# Generate UUID5 using namespace and domain
121+
domain_uuid = uuid.uuid5(namespace, self.allowed_origin)
122+
# Hash for additional security
123+
self.endpoint_uuid = hashlib.blake2b(
124+
str(domain_uuid).encode(),
125+
digest_size=32
126+
).hexdigest()
127+
super().save(*args, **kwargs)
128+
129+
def __str__(self):
130+
return f"{self.user.username} - {self.allowed_origin}"
131+
132+
class Meta:
133+
constraints = [
134+
models.UniqueConstraint(
135+
fields=['user', 'allowed_origin'],
136+
name='unique_user_domain'
137+
)
138+
]
139+
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
{% extends "base.html" %}
2+
{% block content %}
3+
<div class="container py-4">
4+
<div class="row justify-content-center">
5+
<div class="col-md-8">
6+
<div class="card">
7+
<div class="card-header">
8+
<h2 class="card-title mb-0">Create CSP Report Endpoint</h2>
9+
</div>
10+
<div class="card-body">
11+
{% if error %}
12+
<div class="alert alert-danger">{{ error }}</div>
13+
{% endif %}
14+
15+
{% if endpoint_url %}
16+
<div class="alert alert-success">
17+
<h4 class="alert-heading">Endpoint Created Successfully!</h4>
18+
<p>Your CSP report endpoint URL is:</p>
19+
<div class="input-group mb-3">
20+
<input type="text" class="form-control" value="{{ endpoint_url }}" id="endpoint-url" readonly>
21+
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('endpoint-url')">
22+
Copy
23+
</button>
24+
</div>
25+
26+
<hr>
27+
<p>Add these headers to your website's configuration:</p>
28+
<div class="bg-light p-3 rounded">
29+
<code class="d-block">Content-Security-Policy-Report-Only: default-src 'self'; report-uri {{ endpoint_url }};</code>
30+
<small class="text-muted">Use this header to test your CSP without enforcing it</small>
31+
32+
<code class="d-block mt-3">Content-Security-Policy: default-src 'self'; report-uri {{ endpoint_url }};</code>
33+
<small class="text-muted">Use this header to enforce your CSP</small>
34+
</div>
35+
</div>
36+
{% endif %}
37+
38+
<form method="POST" class="mt-4">
39+
{% csrf_token %}
40+
<div class="mb-3">
41+
<label for="allowed_origin" class="form-label">Allowed Origin:</label>
42+
<input type="url" class="form-control" id="allowed_origin" name="allowed_origin"
43+
placeholder="https://example.com" required>
44+
<div class="form-text">Enter the domain that will be sending CSP reports</div>
45+
</div>
46+
<button type="submit" class="btn btn-primary">Create Endpoint</button>
47+
</form>
48+
</div>
49+
</div>
50+
</div>
51+
</div>
52+
</div>
53+
54+
<script>
55+
function copyToClipboard(elementId) {
56+
const element = document.getElementById(elementId);
57+
element.select();
58+
document.execCommand('copy');
59+
60+
// Optional: Show feedback
61+
const button = element.nextElementSibling;
62+
const originalText = button.innerText;
63+
button.innerText = 'Copied!';
64+
setTimeout(() => button.innerText = originalText, 2000);
65+
}
66+
</script>
67+
{% endblock %}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
{# manage_csp_endpoints.html #}
2+
{% extends "base.html" %}
3+
{% block content %}
4+
<div class="container">
5+
<h2>Manage CSP Report Endpoints</h2>
6+
<a href="{% url 'create_csp_endpoint' %}" class="btn btn-primary mb-3">Create New Endpoint</a>
7+
8+
{% if endpoints %}
9+
<table class="table">
10+
<thead>
11+
<tr>
12+
<th>Allowed Origin</th>
13+
<th>Endpoint URL</th>
14+
<th>Created</th>
15+
<th>Actions</th>
16+
</tr>
17+
</thead>
18+
<tbody>
19+
{% for endpoint in endpoints %}
20+
<tr>
21+
<td>{{ endpoint.allowed_origin }}</td>
22+
<td>https://testing.nc3.lu/uri-report/{{ endpoint.endpoint_uuid }}/</td>
23+
<td>{{ endpoint.timestamp|date:"Y-m-d H:i" }}</td>
24+
<td>
25+
<a href="{% url 'view_csp_reports' endpoint.endpoint_uuid %}" class="btn btn-sm btn-info">View Reports</a>
26+
</td>
27+
</tr>
28+
{% endfor %}
29+
</tbody>
30+
</table>
31+
{% else %}
32+
<p>No CSP report endpoints configured yet.</p>
33+
{% endif %}
34+
</div>
35+
{% endblock %}
36+
37+
{# create_csp_endpoint.html #}
38+
{% extends "base.html" %}
39+
{% block content %}
40+
<div class="container">
41+
<h2>Create CSP Report Endpoint</h2>
42+
43+
{% if error %}
44+
<div class="alert alert-danger">{{ error }}</div>
45+
{% endif %}
46+
47+
{% if endpoint_url %}
48+
<div class="alert alert-success">
49+
<h4>Endpoint Created Successfully!</h4>
50+
<p>Your CSP report endpoint URL is:</p>
51+
<code>{{ endpoint_url }}</code>
52+
<p class="mt-3">Add this to your Content-Security-Policy header:</p>
53+
<code>report-uri {{ endpoint_url }};</code>
54+
</div>
55+
{% endif %}
56+
57+
<form method="POST" class="mt-4">
58+
{% csrf_token %}
59+
<div class="form-group">
60+
<label for="allowed_origin">Allowed Origin:</label>
61+
<input type="url" class="form-control" id="allowed_origin" name="allowed_origin"
62+
placeholder="https://example.com" required>
63+
<small class="form-text text-muted">Enter the domain that will be sending CSP reports</small>
64+
</div>
65+
<button type="submit" class="btn btn-primary mt-3">Create Endpoint</button>
66+
</form>
67+
</div>
68+
{% endblock %}
69+
70+
{# view_csp_reports.html #}
71+
{% extends "base.html" %}
72+
{% block content %}
73+
<div class="container">
74+
<h2>CSP Reports for {{ endpoint.allowed_origin }}</h2>
75+
76+
{% if reports %}
77+
<table class="table">
78+
<thead>
79+
<tr>
80+
<th>Timestamp</th>
81+
<th>Violation Details</th>
82+
</tr>
83+
</thead>
84+
<tbody>
85+
{% for report in reports %}
86+
<tr>
87+
<td>{{ report.timestamp|date:"Y-m-d H:i:s" }}</td>
88+
<td>
89+
<pre><code>{{ report.report_data|json }}</code></pre>
90+
</td>
91+
</tr>
92+
{% endfor %}
93+
</tbody>
94+
</table>
95+
{% else %}
96+
<p>No CSP violation reports received yet.</p>
97+
{% endif %}
98+
</div>
99+
{% endblock %}

0 commit comments

Comments
 (0)