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
61 changes: 61 additions & 0 deletions app/access.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import secrets


GUEST_SCAN_CAPABILITIES_SESSION_KEY = 'guest_scan_capabilities'
GUEST_APP_CAPABILITIES_SESSION_KEY = 'guest_app_capabilities'


def grant_guest_scan_access(request, scan):
"""Grant this browser an unguessable, session-bound capability for a scan."""
capabilities = request.session.get(GUEST_SCAN_CAPABILITIES_SESSION_KEY, {})
capabilities[str(scan.pk)] = secrets.token_urlsafe(32)
request.session[GUEST_SCAN_CAPABILITIES_SESSION_KEY] = capabilities
request.session.modified = True


def grant_guest_app_access(request, app):
"""Grant this browser an unguessable, session-bound capability for an app."""
capabilities = request.session.get(GUEST_APP_CAPABILITIES_SESSION_KEY, {})
capabilities[str(app.pk)] = secrets.token_urlsafe(32)
request.session[GUEST_APP_CAPABILITIES_SESSION_KEY] = capabilities
request.session.modified = True


def guest_scan_ids(request):
capabilities = request.session.get(GUEST_SCAN_CAPABILITIES_SESSION_KEY, {})
if not isinstance(capabilities, dict):
return []

return [int(scan_id) for scan_id in capabilities if scan_id.isdigit()]


def guest_app_ids(request):
capabilities = request.session.get(GUEST_APP_CAPABILITIES_SESSION_KEY, {})
if not isinstance(capabilities, dict):
return []

return [int(app_id) for app_id in capabilities if app_id.isdigit()]


def can_access_app(request, app):
if app.user_id is not None:
return request.user.is_authenticated and app.user_id == request.user.id

capabilities = request.session.get(GUEST_APP_CAPABILITIES_SESSION_KEY, {})
if not isinstance(capabilities, dict):
return False

capability = capabilities.get(str(app.pk))
return isinstance(capability, str) and bool(capability)


def can_access_scan(request, scan):
if scan.user_id is not None:
return request.user.is_authenticated and scan.user_id == request.user.id

capabilities = request.session.get(GUEST_SCAN_CAPABILITIES_SESSION_KEY, {})
if not isinstance(capabilities, dict):
return False

capability = capabilities.get(str(scan.pk))
return isinstance(capability, str) and bool(capability)
85 changes: 76 additions & 9 deletions app/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,73 @@
from app.worker.tasks import task_create_scan
from rest_framework import viewsets, mixins, status
from django.db.models import Q
from django.shortcuts import get_object_or_404
from django_filters import rest_framework as filters
from app.access import (
can_access_app,
grant_guest_app_access,
grant_guest_scan_access,
guest_app_ids,
guest_scan_ids,
)
from rest_framework.exceptions import PermissionDenied


class IsAuthenticatedOrGuestCreate(permissions.BasePermission):
"""Allow public creation while leaving unsafe object changes owner-only."""

def has_permission(self, request, view):
return request.method in permissions.SAFE_METHODS or request.method == 'POST' or request.user.is_authenticated

class IsUserOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in ['PUT', 'PATCH']:
return obj.user == request.user
return True
if request.method in permissions.SAFE_METHODS:
return True
return request.user.is_authenticated and obj.user == request.user

class ApplicationViewSet(viewsets.ModelViewSet):
serializer_class = ApplicationSerializer
queryset = Application.objects.all()
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsUserOrReadOnly)
permission_classes = (IsAuthenticatedOrGuestCreate, IsUserOrReadOnly)

def get_queryset(self):
if self.request.user.is_authenticated:
return Application.objects.filter(
Q(user=self.request.user) | Q(pk__in=guest_app_ids(self.request))
)
return Application.objects.filter(pk__in=guest_app_ids(self.request))

def perform_create(self, serializer):
obj = serializer.save(user=self.request.user)
if self.request.user.is_authenticated:
serializer.save(user=self.request.user)
else:
app = serializer.save()
grant_guest_app_access(self.request, app)

class ScanViewSet(viewsets.ModelViewSet):
serializer_class = ScanSerializer
queryset = Scan.objects.all()
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsUserOrReadOnly)
permission_classes = (IsAuthenticatedOrGuestCreate, IsUserOrReadOnly)
parser_classes = (MultiPartParser, FormParser)

def get_queryset(self):
if self.request.user.is_authenticated:
return Scan.objects.filter(
Q(user=self.request.user) | Q(pk__in=guest_scan_ids(self.request))
)
return Scan.objects.filter(pk__in=guest_scan_ids(self.request))

def perform_create(self, serializer):
scan = serializer.save(user=self.request.user, status='In progress', progress=1)
app = serializer.validated_data.get('app')
if app is not None and not can_access_app(self.request, app):
raise PermissionDenied('You do not have access to this application.')

save_kwargs = {'status': 'In progress', 'progress': 1}
if self.request.user.is_authenticated and (app is None or app.user_id is not None):
save_kwargs['user'] = self.request.user
scan = serializer.save(**save_kwargs)
if scan.user_id is None:
grant_guest_scan_access(self.request, scan)
task_id = task_create_scan.delay(scan.id)
scan.task = task_id.id
scan.save()
Expand All @@ -40,14 +83,26 @@ class FindingViewSet(viewsets.ModelViewSet):
queryset = Finding.objects.all()
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsUserOrReadOnly)

def get_queryset(self):
if self.request.user.is_authenticated:
return Finding.objects.filter(
Q(scan__user=self.request.user) | Q(scan_id__in=guest_scan_ids(self.request))
)
return Finding.objects.filter(scan_id__in=guest_scan_ids(self.request))

def perform_create(self, serializer):
obj = serializer.save(user=self.request.user)


@action(detail=True, methods=['GET'], name='Get findings for scan')
def scan(self, request, pk=None):
if (pk != None):
scan = Scan.objects.get(pk=pk)
scan_queryset = Scan.objects.filter(pk__in=guest_scan_ids(request))
if request.user.is_authenticated:
scan_queryset = Scan.objects.filter(
Q(user=request.user) | Q(pk__in=guest_scan_ids(request))
)
scan = get_object_or_404(scan_queryset, pk=pk)
queryset = Finding.objects.filter(scan=scan).order_by('id')
else:
queryset = Finding.objects.all().order_by('id')
Expand All @@ -64,14 +119,26 @@ class PermissionViewSet(viewsets.ModelViewSet):
queryset = Permission.objects.all()
permission_classes = (permissions.IsAuthenticatedOrReadOnly, IsUserOrReadOnly)

def get_queryset(self):
if self.request.user.is_authenticated:
return Permission.objects.filter(
Q(scan__user=self.request.user) | Q(scan_id__in=guest_scan_ids(self.request))
)
return Permission.objects.filter(scan_id__in=guest_scan_ids(self.request))

def perform_create(self, serializer):
obj = serializer.save(user=self.request.user)


@action(detail=True, methods=['GET'], name='Get findings for scan')
def scan(self, request, pk=None):
if (pk != None):
scan = Scan.objects.get(pk=pk)
scan_queryset = Scan.objects.filter(pk__in=guest_scan_ids(request))
if request.user.is_authenticated:
scan_queryset = Scan.objects.filter(
Q(user=request.user) | Q(pk__in=guest_scan_ids(request))
)
scan = get_object_or_404(scan_queryset, pk=pk)
queryset = Permission.objects.filter(scan=scan).order_by('id')
else:
queryset = Permission.objects.all().order_by('id')
Expand Down
5 changes: 2 additions & 3 deletions app/config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from rest_framework.authtoken.views import obtain_auth_token
from drf_yasg.views import get_schema_view
from drf_yasg import openapi
from app.worker.tasks import scan_state

schema_view = get_schema_view(
openapi.Info(
Expand Down Expand Up @@ -55,9 +54,9 @@
path('accounts/profile/', views.user_profile, name='profile'),
path('api/v1/auth-token/', obtain_auth_token, name='api_token_auth'),
path('api/v1/', include(router.urls)),
path('scan_state/<int:id>', scan_state, name="scan_state"),
path('scan_state/<int:id>', views.scan_state, name="scan_state"),
re_path(r'^swagger(?P<format>\.json|\.yaml)$', schema_view.without_ui(cache_timeout=0), name='schema-json'),
re_path(r'^swagger/$', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'),
re_path(r'^redoc/$', schema_view.with_ui('redoc', cache_timeout=0), name='schema-redoc'),

]
]
8 changes: 7 additions & 1 deletion app/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ class ScanForm(forms.ModelForm):
class Meta:
model = Scan
fields = ('description', 'apk', 'app', 'defectdojo_id')


class GuestScanForm(forms.ModelForm):
class Meta:
model = Scan
fields = ('description', 'apk')

class ApplicationForm(forms.ModelForm):
class Meta:
Expand All @@ -55,4 +61,4 @@ class FindingForm(forms.ModelForm):
risk = RiskModelChoiceField(queryset=Risk.objects.all())
class Meta:
model = Finding
fields = ('scan', 'name', 'description', 'severity', 'status', 'path', 'line_number', 'line', 'snippet', 'cwe', 'risk', 'mitigation', 'defectdojo_id')
fields = ('scan', 'name', 'description', 'severity', 'status', 'path', 'line_number', 'line', 'snippet', 'cwe', 'risk', 'mitigation', 'defectdojo_id')
34 changes: 34 additions & 0 deletions app/migrations/0004_allow_guest_scans.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 3.2 on 2026-07-28

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


class Migration(migrations.Migration):

dependencies = [
('app', '0003_auto_20210509_1408'),
]

operations = [
migrations.AlterField(
model_name='scan',
name='app',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.CASCADE,
to='app.application',
),
),
migrations.AlterField(
model_name='scan',
name='user',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to='auth.user',
),
),
]
24 changes: 24 additions & 0 deletions app/migrations/0005_allow_guest_apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 3.2 on 2026-08-26

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


class Migration(migrations.Migration):

dependencies = [
('app', '0004_allow_guest_scans'),
]

operations = [
migrations.AlterField(
model_name='application',
name='user',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
to='auth.user',
),
),
]
8 changes: 4 additions & 4 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,18 @@ class Application(models.Model):
id = models.AutoField(primary_key=True)
name = models.CharField(max_length=255, blank=False)
description = models.CharField(max_length=255, blank=False)
user = models.ForeignKey(User, on_delete=models.PROTECT)
user = models.ForeignKey(User, on_delete=models.PROTECT, blank=True, null=True)
created_on = models.DateTimeField(auto_now_add=True, null=True)
updated_on = models.DateTimeField(auto_now=True, null=True)

class Scan(models.Model):
id = models.AutoField(primary_key=True)
app = models.ForeignKey(Application, on_delete=models.CASCADE)
app = models.ForeignKey(Application, on_delete=models.CASCADE, blank=True, null=True)
name = models.CharField(max_length=255, blank=True)
apk = models.FileField(upload_to='apk/', blank=False, validators=[validate_file_extension])
description = models.CharField(max_length=255, blank=False)
defectdojo_id = models.IntegerField(blank=True, default=0)
user = models.ForeignKey(User, on_delete=models.PROTECT)
user = models.ForeignKey(User, on_delete=models.PROTECT, blank=True, null=True)
status = models.CharField(max_length=50,blank=True, null=True)
progress = models.IntegerField(blank=True, null=True)
findings = models.IntegerField(blank=True, null=True, default=0)
Expand Down Expand Up @@ -293,4 +293,4 @@ class Antivirus(models.Model):
method = models.CharField(max_length=255, null=True)
update = models.CharField(max_length=255, null=True)
created_on = models.DateTimeField(auto_now_add=True, null=True)
updated_on = models.DateTimeField(auto_now=True, null=True)
updated_on = models.DateTimeField(auto_now=True, null=True)
2 changes: 1 addition & 1 deletion app/templates/app.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ <h4 class="card-title">App</h4>
</tr>
<tr>
<th>Created by</th>
<td>{{ app.user }}</td>
<td>{% if app.user %}{{ app.user }}{% else %}Guest{% endif %}</td>
</tr>
</table>
</div>
Expand Down
2 changes: 1 addition & 1 deletion app/templates/create_app.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@
});
</script>
{% endif %}
{% endblock %}
{% endblock %}
2 changes: 1 addition & 1 deletion app/templates/create_scan.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@
fade_alerts();
});
</script>
{% endblock %}
{% endblock %}
10 changes: 5 additions & 5 deletions app/templates/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ <h5>Apps</h5>
<p id="alert-model" class="alert alert-danger" style="display: none;">To create apps, you must first create the model</p>
<a onclick="$('#alert-model').show()" class="btn btn-outline-primary">New App</a> {% else %}
<a href="{% url 'create_app' %}" class="btn btn-outline-primary">New App</a> {% endif %} {% else %}
<p id="alert-account" class="alert alert-danger" style="display: none;">To create apps, you must have and account</p>
<a onclick="$('#alert-account').show()" class="btn btn-outline-primary">New App</a> {% endif %}
<a href="{% url 'create_app' %}" class="btn btn-outline-primary">New App</a>
{% endif %}
<br> {% if apps %}
<ul class="messages">
{% for message in messages %}
Expand All @@ -31,7 +31,7 @@ <h5>Apps</h5>
<tr>
<td><a class="btn btn-outline-primary btn-round" href="{% url 'app' id=app.id %}">{{ app.id }}</a></td>
<td>{{ app.name }} </td>
<td>{{ app.user.username }} </td>
<td>{% if app.user %}{{ app.user.username }}{% else %}Guest{% endif %}</td>
<td>{{ app.description }} </td>
<td>
<table class="table table-striped table-borderless">
Expand Down Expand Up @@ -101,7 +101,7 @@ <h5>Apps</h5>
{% endif %} {% endfor %}</tbody>
</table>
</td>
{% if user.is_authenticated %}
{% if user.is_authenticated or not app.user %}
<td><a href="{% url 'create_scan' app_id=app.id %}" class="btn btn-outline-success">New Scan</a></td>
{% endif %}
</tr>
Expand Down Expand Up @@ -131,4 +131,4 @@ <h5>Apps</h5>
});
});
</script>
{% endif %} {% endblock %}
{% endif %} {% endblock %}
Loading
Loading