Files
Mobile-Security-Framework-M…/mobsf/StaticAnalyzer/views/common/async_task.py
T
Ajin Abraham b4cf9b70ac 4.2.5 (#2457)
* Unified async scan timeout
* Allow incomplete scan delete after async scan timeout duration
* Added support for Android SBOM analysis
* Make dependencies unpinned (Address #2458)
2024-11-23 13:48:12 -08:00

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)