Scan files with clamdscan #77
@ -5,6 +5,12 @@
|
|||||||
{% block page_title %}{{ extension.name }}{% endblock page_title %}
|
{% block page_title %}{{ extension.name }}{% endblock page_title %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% if extension.latest_version %}
|
||||||
|
{% with latest=extension.latest_version %}
|
||||||
|
{% include "files/components/scan_details.html" with file=latest.file %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% has_maintainer extension as is_maintainer %}
|
{% has_maintainer extension as is_maintainer %}
|
||||||
{% with latest=extension.latest_version %}
|
{% with latest=extension.latest_version %}
|
||||||
|
|
||||||
|
@ -12,9 +12,17 @@ def scan_selected_files(self, request, queryset):
|
|||||||
files.signals._initiate_scan(instance)
|
files.signals._initiate_scan(instance)
|
||||||
|
|
||||||
|
|
||||||
class FileValidationInlineAdmin(admin.TabularInline):
|
class FileValidationInlineAdmin(admin.StackedInline):
|
||||||
model = FileValidation
|
model = FileValidation
|
||||||
readonly_fields = ('date_created', 'date_modified', 'is_valid', 'results')
|
readonly_fields = ('date_created', 'date_modified', 'is_ok', 'results')
|
||||||
|
extra = 0
|
||||||
|
|
||||||
|
def _nope(self, request, obj):
|
||||||
|
return False
|
||||||
|
|
||||||
|
has_add_permission = _nope
|
||||||
|
has_change_permission = _nope
|
||||||
|
has_delete_permission = _nope
|
||||||
|
|
||||||
|
|
||||||
@admin.register(File)
|
@admin.register(File)
|
||||||
@ -23,13 +31,14 @@ class FileAdmin(admin.ModelAdmin):
|
|||||||
save_on_top = True
|
save_on_top = True
|
||||||
|
|
||||||
list_filter = (
|
list_filter = (
|
||||||
|
'validation__is_ok',
|
||||||
'type',
|
'type',
|
||||||
'status',
|
'status',
|
||||||
'date_status_changed',
|
'date_status_changed',
|
||||||
'date_approved',
|
'date_approved',
|
||||||
'date_deleted',
|
'date_deleted',
|
||||||
)
|
)
|
||||||
list_display = ('original_name', 'extension', 'user', 'date_created', 'type', 'status')
|
list_display = ('original_name', 'extension', 'user', 'date_created', 'type', 'status', 'is_ok')
|
||||||
|
|
||||||
list_select_related = ('version__extension', 'user')
|
list_select_related = ('version__extension', 'user')
|
||||||
|
|
||||||
@ -95,6 +104,11 @@ class FileAdmin(admin.ModelAdmin):
|
|||||||
inlines = [FileValidationInlineAdmin]
|
inlines = [FileValidationInlineAdmin]
|
||||||
actions = [scan_selected_files]
|
actions = [scan_selected_files]
|
||||||
|
|
||||||
|
def is_ok(self, obj):
|
||||||
|
return obj.validation.is_ok if hasattr(obj, 'validation') else None
|
||||||
|
|
||||||
|
is_ok.boolean = True
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
admin.site.unregister(background_task.models.Task)
|
admin.site.unregister(background_task.models.Task)
|
||||||
|
@ -32,4 +32,9 @@ class Migration(migrations.Migration):
|
|||||||
model_name='filevalidation',
|
model_name='filevalidation',
|
||||||
name='warnings',
|
name='warnings',
|
||||||
),
|
),
|
||||||
|
migrations.RenameField(
|
||||||
|
model_name='filevalidation',
|
||||||
|
old_name='is_valid',
|
||||||
|
new_name='is_ok',
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
@ -204,10 +204,14 @@ class File(CreatedModifiedMixin, TrackChangesMixin, SoftDeleteMixin, models.Mode
|
|||||||
def get_submit_url(self) -> str:
|
def get_submit_url(self) -> str:
|
||||||
return self.extension.get_draft_url()
|
return self.extension.get_draft_url()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_ok(self):
|
||||||
|
return self.validation.is_ok if hasattr(self, 'validation') else None
|
||||||
|
|
||||||
|
|
||||||
class FileValidation(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
class FileValidation(CreatedModifiedMixin, TrackChangesMixin, models.Model):
|
||||||
track_changes_to_fields = {'is_valid', 'results'}
|
track_changes_to_fields = {'is_ok', 'results'}
|
||||||
|
|
||||||
file = models.OneToOneField(File, related_name='validation', on_delete=models.CASCADE)
|
file = models.OneToOneField(File, related_name='validation', on_delete=models.CASCADE)
|
||||||
is_valid = models.BooleanField(default=False)
|
is_ok = models.BooleanField(default=False)
|
||||||
results = models.JSONField()
|
results = models.JSONField()
|
||||||
|
@ -27,5 +27,5 @@ def scan(file_id: int):
|
|||||||
file_validation, is_new = files.models.FileValidation.objects.get_or_create(
|
file_validation, is_new = files.models.FileValidation.objects.get_or_create(
|
||||||
file=file, defaults={'results': {completed_process.args[0]: scan_result}}
|
file=file, defaults={'results': {completed_process.args[0]: scan_result}}
|
||||||
)
|
)
|
||||||
file_validation.is_valid = completed_process.returncode == 0
|
file_validation.is_ok = completed_process.returncode == 0
|
||||||
file_validation.save()
|
file_validation.save()
|
||||||
|
21
files/templates/files/components/scan_details.html
Normal file
21
files/templates/files/components/scan_details.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{% load common i18n %}
|
||||||
|
{# FIXME: we might want to rephrase is_moderator is terms of Django's (group) permissions #}
|
||||||
|
{% if perms.files.view_file or request.user.is_moderator %}
|
||||||
|
{% with file_validation=file.validation %}
|
||||||
|
{% if file_validation and not file_validation.is_ok %}
|
||||||
|
<section>
|
||||||
|
<div class="card pb-3 pt-4 px-4 mb-3 ext-detail-download-danger">
|
||||||
|
<h3>⚠ Suspicious upload</h3>
|
||||||
|
{% blocktrans asvar alert_text %}Scan of the {{ file }} indicates malicious content.{% endblocktrans %}
|
||||||
|
<h4>
|
||||||
|
{{ alert_text }}
|
||||||
|
{% if perms.files.view_file %}{# Moderators don't necessarily have access to the admin #}
|
||||||
|
{% url 'admin:files_file_change' file.pk as admin_file_url %}
|
||||||
|
<a href="{{ admin_file_url }}" target="_blank">See details</a>
|
||||||
|
{% endif %}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
10
files/templates/files/components/scan_details_flag.html
Normal file
10
files/templates/files/components/scan_details_flag.html
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{% load common i18n %}
|
||||||
|
{# FIXME: we might want to rephrase is_moderator is terms of Django's (group) permissions #}
|
||||||
|
{% if perms.files.view_file or request.user.is_moderator %}
|
||||||
|
{% with file_validation=file.validation %}
|
||||||
|
{% if file_validation and not file_validation.is_ok %}
|
||||||
|
{% blocktrans asvar alert_text %}Scan of the {{ file }} indicates malicious content.{% endblocktrans %}
|
||||||
|
<b class="text-danger pt-2" title="{{ alert_text }}">⚠</b>
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
@ -42,7 +42,7 @@ class FileScanTest(TestCase):
|
|||||||
task_args, task_kwargs = task.params()
|
task_args, task_kwargs = task.params()
|
||||||
files.tasks.scan.task_function(*task_args, **task_kwargs)
|
files.tasks.scan.task_function(*task_args, **task_kwargs)
|
||||||
|
|
||||||
self.assertFalse(file.validation.is_valid)
|
self.assertFalse(file.validation.is_ok)
|
||||||
result = file.validation.results['clamdscan']
|
result = file.validation.results['clamdscan']
|
||||||
self.assertEqual(result['returncode'], 1)
|
self.assertEqual(result['returncode'], 1)
|
||||||
stdout_lines = result['stdout'].split('\n')
|
stdout_lines = result['stdout'].split('\n')
|
||||||
@ -66,7 +66,7 @@ class FileScanTest(TestCase):
|
|||||||
task_args, task_kwargs = task.params()
|
task_args, task_kwargs = task.params()
|
||||||
files.tasks.scan.task_function(*task_args, **task_kwargs)
|
files.tasks.scan.task_function(*task_args, **task_kwargs)
|
||||||
|
|
||||||
self.assertTrue(file.validation.is_valid)
|
self.assertTrue(file.validation.is_ok)
|
||||||
result = file.validation.results['clamdscan']
|
result = file.validation.results['clamdscan']
|
||||||
self.assertEqual(result['returncode'], 0)
|
self.assertEqual(result['returncode'], 0)
|
||||||
stdout_lines = result['stdout'].split('\n')
|
stdout_lines = result['stdout'].split('\n')
|
||||||
|
@ -22,6 +22,7 @@
|
|||||||
<span>{{ extension.review_activity.all.last.date_created|naturaltime_compact }}</span>
|
<span>{{ extension.review_activity.all.last.date_created|naturaltime_compact }}</span>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% include "files/components/scan_details_flag.html" with file=extension.latest_version.file %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ extension.get_review_url }}" class="text-decoration-none">
|
<a href="{{ extension.get_review_url }}" class="text-decoration-none">
|
||||||
|
Loading…
Reference in New Issue
Block a user