mirror of
https://github.com/MobSF/Mobile-Security-Framework-MobSF.git
synced 2026-10-11 22:50:09 +00:00
* Unified async scan timeout * Allow incomplete scan delete after async scan timeout duration * Added support for Android SBOM analysis * Make dependencies unpinned (Address #2458)
156 lines
4.9 KiB
Python
156 lines
4.9 KiB
Python
"""Views to handle asynchronous tasks."""
|
|
import logging
|
|
from datetime import (
|
|
timedelta,
|
|
)
|
|
|
|
from django.dispatch import receiver
|
|
from django.utils import timezone
|
|
from django.shortcuts import render
|
|
from django.conf import settings
|
|
from django.http import (
|
|
HttpResponseRedirect,
|
|
JsonResponse,
|
|
)
|
|
from django.views.decorators.http import require_http_methods
|
|
|
|
from django_q.tasks import async_task
|
|
from django_q.signals import post_execute
|
|
|
|
from mobsf.StaticAnalyzer.models import (
|
|
EnqueuedTask,
|
|
RecentScansDB,
|
|
)
|
|
from mobsf.MobSF.views.authentication import (
|
|
login_required,
|
|
)
|
|
from mobsf.MobSF.utils import (
|
|
append_scan_status,
|
|
get_scan_logs,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@receiver(post_execute)
|
|
def detect_timeout(sender, task, **kwargs):
|
|
"""Detect scan task timeout."""
|
|
result = task.get('result')
|
|
if isinstance(result, str) and 'Task exceeded maximum timeout' in result:
|
|
task_id = task['id']
|
|
EnqueuedTask.objects.filter(task_id=task_id).update(
|
|
app_name='Failed',
|
|
completed_at=timezone.now(),
|
|
status='Scan Timeout',
|
|
)
|
|
logger.error('Task %s exceeded maximum timeout', task_id)
|
|
|
|
|
|
def async_analysis(checksum, api, file_name, func, *args, **kwargs):
|
|
"""Async Analysis Task."""
|
|
# Check if the task is already completed
|
|
recent = RecentScansDB.objects.filter(MD5=checksum)
|
|
scan_completed = recent[0].APP_NAME or recent[0].PACKAGE_NAME
|
|
# Check if the task is updated within the last 60 minutes
|
|
active_recently = recent[0].TIMESTAMP >= timezone.now() - timedelta(
|
|
minutes=settings.ASYNC_ANALYSIS_TIMEOUT)
|
|
# Check if the task is already enqueued within the last 60 minutes
|
|
queued_recently = EnqueuedTask.objects.filter(
|
|
checksum=checksum,
|
|
created_at__gte=timezone.now() - timedelta(
|
|
minutes=settings.ASYNC_ANALYSIS_TIMEOUT),
|
|
).exists()
|
|
|
|
# Additional checks on recent queue
|
|
if queued_recently:
|
|
if scan_completed:
|
|
# scan already completed recently
|
|
msg = 'Analysis already completed in the last 60 minutes'
|
|
logger.warning(msg)
|
|
if api:
|
|
return {'task_id': None, 'message': msg}
|
|
return HttpResponseRedirect('/tasks?q=completed')
|
|
elif active_recently:
|
|
# scan not completed but active recently
|
|
msg = 'Analysis already enqueued in the last 60 minutes'
|
|
logger.warning(msg)
|
|
if api:
|
|
return {'task_id': None, 'message': msg}
|
|
return HttpResponseRedirect('/tasks?q=queued')
|
|
|
|
# Clear old tasks
|
|
queue_size = settings.QUEUE_MAX_SIZE
|
|
task_count = EnqueuedTask.objects.count()
|
|
if task_count > queue_size:
|
|
logger.info('Deleting oldest enqueued tasks')
|
|
# Get IDs of tasks to delete (keep the latest queue_size)
|
|
oldest_task_ids = list(
|
|
EnqueuedTask.objects.order_by('created_at')
|
|
.values_list('id', flat=True)[:task_count - queue_size])
|
|
# Delete tasks by IDs
|
|
EnqueuedTask.objects.filter(id__in=oldest_task_ids).delete()
|
|
|
|
# Enqueue the task
|
|
task_id = async_task(
|
|
func,
|
|
*args,
|
|
queue=True,
|
|
save=False)
|
|
EnqueuedTask.objects.create(
|
|
task_id=task_id,
|
|
checksum=checksum,
|
|
file_name=file_name[:254])
|
|
msg = f'Scan Queued with ID: {task_id}'
|
|
logger.info(msg)
|
|
append_scan_status(checksum, msg)
|
|
if api:
|
|
return {'task_id': task_id, 'message': msg}
|
|
return HttpResponseRedirect('/tasks')
|
|
|
|
|
|
def update_enqueued_task(checksum, app_name, status):
|
|
"""Update the Enqueued Task and others that matches the checksum."""
|
|
EnqueuedTask.objects.filter(checksum=checksum).update(
|
|
app_name=app_name[:254],
|
|
completed_at=timezone.now(),
|
|
status=status[:254],
|
|
)
|
|
return True
|
|
|
|
|
|
def get_live_status(enq):
|
|
"""Get Live Status of the Task."""
|
|
if enq.status == 'Success' or enq.app_name == 'Failed':
|
|
return enq.status
|
|
logs = get_scan_logs(enq.checksum)
|
|
if logs:
|
|
return logs[-1]
|
|
return enq.status
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(['POST', 'GET'])
|
|
def list_tasks(request, api=False):
|
|
if request.method == 'POST':
|
|
enqueued = EnqueuedTask.objects.all().order_by('-created_at')
|
|
task_data = []
|
|
for enq in enqueued:
|
|
# Enqueued task is not in the completed tasks
|
|
task_data.append({
|
|
'task_id': enq.task_id,
|
|
'file_name': enq.file_name,
|
|
'app_name': enq.app_name,
|
|
'checksum': enq.checksum,
|
|
'created_at': enq.created_at,
|
|
'completed_at': enq.completed_at,
|
|
'status': get_live_status(enq),
|
|
})
|
|
if api:
|
|
return task_data
|
|
return JsonResponse(task_data, safe=False)
|
|
context = {
|
|
'title': 'Scan Tasks',
|
|
'version': settings.MOBSF_VER,
|
|
}
|
|
return render(request, 'general/tasks.html', context)
|