예제 #1
0
def cancel_scan(account, scan_id: int):
    """
    :param account: Account
    :param scan_id: AccountInternetNLScan ID
    :return:
    """

    scan = AccountInternetNLScan.objects.all().filter(account=account,
                                                      pk=scan_id).first()

    if not scan:
        return operation_response(error=True, message="scan not found")

    if scan.state == 'finished':
        return operation_response(success=True,
                                  message="scan already finished")

    if scan.state == 'cancelled':
        return operation_response(success=True,
                                  message="scan already cancelled")

    scan.finished_on = timezone.now()
    scan.save()
    update_state("cancelled", scan)

    # Sprinkling an activity stream action.
    action.send(account, verb='cancelled scan', target=scan, public=False)

    return operation_response(success=True, message="scan cancelled")
예제 #2
0
def scan_now(account, user_input) -> Dict[str, Any]:
    urllist = UrlList.objects.all().filter(account=account,
                                           id=user_input.get('id', -1),
                                           is_deleted=False).first()

    # automated scans do not work.
    if not urllist:
        return operation_response(error=True,
                                  message="List could not be found.")

    if not urllist.is_scan_now_available():
        return operation_response(
            error=True,
            message="Not all conditions for initiating a scan are met.")

    # Make sure the fernet key is working fine, you are on the correct queue (-Q storage) and that the correct API
    # version is used.
    # Run this before updating the list, as this might go wrong for many reasons.
    try:
        create_dashboard_scan_tasks(urllist).apply_async()
    except ValueError:
        return operation_response(
            error=True,
            message="Password to the internet.nl API is not set or incorrect.")

    # done: have to update the list info. On the other hand: there is no guarantee that this task already has started
    # ...to fix this issue, we'll use a 'last_manual_scan' field.
    urllist.last_manual_scan = timezone.now()
    urllist.save()

    return operation_response(success=True, message="Scan started")
예제 #3
0
def save_instant_account(request) -> HttpResponse:

    request = get_json_body(request)
    username = request['username']
    password = request['password']

    if User.objects.all().filter(username=username).exists():
        return JsonResponse(operation_response(error=True, message=f"User with username '{username}' already exists."))

    if Account.objects.all().filter(name=username).exists():
        return JsonResponse(operation_response(error=True,
                                               message=f"Account with username {username}' already exists."))

    # Extremely arbitrary password requirements. Just to make sure a password has been filled in.
    if len(password) < 5:
        return JsonResponse(operation_response(error=True, message=f"Password not filled in or not long enough."))

    # all seems fine, let's add the user
    user = User(**{'username': username})
    user.set_password(password)
    user.is_active = True
    user.save()

    account = Account(**{
        'name': username,
        'internet_nl_api_username': username,
        'internet_nl_api_password': Account.encrypt_password(password),
        'can_connect_to_internet_nl_api': Account.connect_to_internet_nl_api(username, password)
    })
    account.save()

    dashboarduser = DashboardUser(**{'user': user, 'account': account})
    dashboarduser.save()

    return JsonResponse(operation_response(success=True, message=f"Account and user with name '{username}' created!"))
def save_user_settings(dashboarduser_id, data):

    user = User.objects.all().filter(dashboarduser=dashboarduser_id).first()

    if not user:
        return operation_response(
            error=True,
            message="save_user_settings_error_could_not_retrieve_user")
    """
    The only fields that can be changed by the user are:
    - first_name
    - last_name
    - mail_preferred_mail_address
    - mail_preferred_language (en, nl)
    - mail_send_mail_after_scan_finished
    """

    expected_keys = [
        'first_name', 'last_name', 'mail_preferred_mail_address',
        'mail_preferred_language', 'mail_send_mail_after_scan_finished'
    ]
    if not keys_are_present_in_object(expected_keys, data):
        return operation_response(
            error=True, message="save_user_settings_error_incomplete_data")

    # validate data. Even if it's correct for the model (like any language), that's not what we'd accept.
    # this breaks the 'form-style' logic. Perhaps we'd move to django rest framework to optimize this.
    # Otoh there's no time for that now. Assuming the form is entered well this is no direct issue now.
    # I'm currently slowly developing a framework. But as long as it's just a few forms and pages it's fine.
    if data['mail_preferred_language'] not in [
            language_code for language_code, name in LANGUAGES
    ]:
        return operation_response(
            error=True,
            message="save_user_settings_error_form_unsupported_language")

    # email is allowed to be empty:
    if data['mail_preferred_mail_address']:
        email_field = forms.EmailField()
        try:
            email_field.clean(data['mail_preferred_mail_address'])
        except ValidationError:
            return operation_response(
                error=True,
                message="save_user_settings_error_form_incorrect_mail_address")

    user.first_name = data['first_name']
    user.last_name = data['last_name']
    user.save()

    user.dashboarduser.mail_preferred_mail_address = data[
        'mail_preferred_mail_address']
    user.dashboarduser.mail_preferred_language = data[
        'mail_preferred_language']
    user.dashboarduser.mail_send_mail_after_scan_finished = data[
        'mail_send_mail_after_scan_finished']
    user.dashboarduser.save()

    return operation_response(success=True,
                              message="save_user_settings_success")
예제 #5
0
def create_list(account: Account, user_input: Dict) -> Dict[str, Any]:
    expected_keys = [
        'id', 'name', 'enable_scans', 'scan_type', 'automated_scan_frequency',
        'scheduled_next_scan'
    ]
    if sorted(user_input.keys()) != sorted(expected_keys):
        return operation_response(error=True, message="Missing settings.")

    frequency = validate_list_automated_scan_frequency(
        user_input['automated_scan_frequency'])
    data = {
        'account': account,
        'name': validate_list_name(user_input['name']),
        'enable_scans': bool(user_input['enable_scans']),
        'scan_type': validate_list_scan_type(user_input['scan_type']),
        'automated_scan_frequency': frequency,
        'scheduled_next_scan': UrlList.determine_next_scan_moment(frequency)
    }

    urllist = UrlList(**data)
    urllist.save()

    # make sure the account is serializable.
    data['account'] = account.id

    # adding the ID makes it possible to add new urls to a new list.
    data['id'] = urllist.pk

    # give a hint if it can be scanned:
    data['scan_now_available'] = urllist.is_scan_now_available()

    return operation_response(success=True, message="List created.", data=data)
def set_account(request) -> HttpResponse:
    request_data = get_json_body(request)
    selected_account_id: int = request_data['id']

    if not selected_account_id:
        return JsonResponse(
            operation_response(error=True, message="No account supplied."))

    dashboard_user = DashboardUser.objects.all().filter(
        user=request.user).first()

    # very new users don't have the dashboarduser fields filled in, and are thus not connected to an account.
    if not dashboard_user:
        dashboard_user = DashboardUser(**{
            'account': Account.objects.all().first(),
            'user': request.user
        })

    dashboard_user.account = Account.objects.get(id=selected_account_id)
    dashboard_user.save()

    return JsonResponse(
        operation_response(success=True,
                           message="switched_account",
                           data={'account_name': dashboard_user.account.name}))
예제 #7
0
def delete_list(account: Account, user_input: dict):
    """
    The first assumption was that a list is not precious or special, and that it can be quickly re-created with an
    import from excel or a csv paste in the web interface. Yet this assumption is wrong. It's valuable to keep the list
    also after it is deleted. This gives insight into what scans have happened in the past on what list.

    To do that, the is_deleted columns have been introduced.

    :param account:
    :param user_input:
    :return:
    """
    urllist = UrlList.objects.all().filter(account=account,
                                           id=user_input.get('id', -1),
                                           is_deleted=False).first()
    if not urllist:
        return operation_response(error=True,
                                  message="List could not be deleted.")

    urllist.is_deleted = True
    urllist.enable_scans = False
    urllist.deleted_on = timezone.now()
    urllist.save()

    # Sprinkling an activity stream action.
    action.send(account, verb='deleted list', target=urllist, public=False)

    return operation_response(success=True, message="List deleted.")
def update_report_code(account, report_id):
    report = get_report_for_sharing(account, report_id, True)

    if not report:
        return operation_response(error=True, message="response_no_report_found")

    report.public_report_code = str(uuid4())
    report.save()

    return operation_response(success=True, message="response_updated_report_code", data=report_sharing_data(report))
def unshare(account, report_id):
    report = get_report_for_sharing(account, report_id, True)

    if not report:
        return operation_response(error=True, message="response_no_report_found")

    report.is_publicly_shared = False
    report.save()

    return operation_response(success=True, message="response_unshared", data=report_sharing_data(report))
def session_logout_(request):
    # If you don't include credentials in your get request, you'll get an AnonymousUser.
    # The preferred method of detecting anonymous users is to see if they are authenticated, according to:
    # https://docs.djangoproject.com/en/3.1/ref/contrib/auth/
    if not request.user.is_authenticated:
        log.debug('User is not authenticated...')
        return operation_response(success=True, message="logged_out")

    logout(request)
    return operation_response(success=True, message="logged_out")
예제 #11
0
def scan_urllist_now_ignoring_business_rules(urllist: UrlList):
    urllist = UrlList.objects.all().filter(pk=urllist.id).first()

    if not urllist:
        return operation_response(error=True,
                                  message="List could not be found.")

    initialize_scan(urllist)

    urllist.last_manual_scan = timezone.now()
    urllist.save()

    return operation_response(success=True, message="Scan started")
def share(account, report_id, share_code):
    report = get_report_for_sharing(account, report_id, False)

    if not report:
        return operation_response(error=True, message="response_no_report_found")

    report.is_publicly_shared = True
    report.public_share_code = share_code

    # Keep the report link the same when disabling and re-enabling sharing.
    if not report.public_report_code:
        report.public_report_code = str(uuid4())
    report.save()

    return operation_response(success=True, message="response_shared", data=report_sharing_data(report))
예제 #13
0
def request_scan(account: Account, urllist_id: int):
    urllist = UrlList.objects.all().filter(account=account,
                                           id=urllist_id).first()
    if not urllist:
        return operation_response(error=True, message="list_does_not_exist")

    # Can only start a scan when the previous one finished:
    last_scan = SubdomainDiscoveryScan.objects.all().filter(
        urllist=urllist_id).last()
    if not last_scan:
        scan = SubdomainDiscoveryScan()
        scan.urllist = urllist
        scan.save()
        update_state(scan.id, "requested")
        return scan_status(account, urllist_id)

    if last_scan.state in ['requested', 'scanning']:
        return scan_status(account, urllist_id)

    scan = SubdomainDiscoveryScan()
    scan.urllist = urllist
    scan.save()
    update_state(scan.id, "requested")

    return scan_status(account, urllist_id)
def remove_tag_(request):
    data = get_json_body(request)
    remove_tag(account=get_account(request),
               urllist_id=data.get('urllist_id', []),
               url_ids=data.get('url_ids', []),
               tag=data.get('tag', ""))
    return JsonResponse(operation_response(success=True), safe=False)
예제 #15
0
def save_urllist_content(account: Account, user_input: Dict[str, Any]) -> Dict:
    """
    This is the 'id' version of save_urllist. It is a bit stricter as in that it requires the list to exist.

    Stores urls in an urllist. If the url doesn't exist yet, it will be added to the database (so the urls
    can be shared with multiple accounts, and only requires one scan).

    Used in the web / ajax frontend and uses operation responses.

    :param account:
    :param user_input:
    :return:
    """

    # how could we validate user_input a better way? Using a validator object?
    list_id: int = int(user_input.get('list_id', -1))
    unfiltered_urls: str = user_input.get('urls', [])

    urllist = UrlList.objects.all().filter(account=account,
                                           id=list_id,
                                           is_deleted=False).first()

    if not urllist:
        return operation_response(error=True,
                                  message="add_domains_list_does_not_exist")

    urls, duplicates_removed = retrieve_possible_urls_from_unfiltered_input(
        unfiltered_urls)
    cleaned_urls = clean_urls(urls)  # type: ignore

    if cleaned_urls['correct']:
        counters = _add_to_urls_to_urllist(account,
                                           urllist,
                                           urls=cleaned_urls['correct'])
    else:
        counters = {'added_to_list': 0, 'already_in_list': 0}

    result = {
        'incorrect_urls': cleaned_urls['incorrect'],
        'added_to_list': counters['added_to_list'],
        'already_in_list': counters['already_in_list'],
        'duplicates_removed': duplicates_removed
    }

    return operation_response(success=True,
                              message="add_domains_valid_urls_added",
                              data=result)
예제 #16
0
def save_urllist_content(account: Account, user_input: Dict[str, Any]) -> Dict:
    """
    This is the 'id' version of save_urllist. It is a bit stricter as in that it requires the list to exist.

    Stores urls in an urllist. If the url doesn't exist yet, it will be added to the database (so the urls
    can be shared with multiple accounts, and only requires one scan).

    Used in the web / ajax frontend and uses operation responses.

    :param account:
    :param user_input:
    :return:
    """

    # how could we validate user_input a better way? Using a validator object?
    list_id = user_input.get('list_id')
    urls = user_input.get('urls')

    urllist = UrlList.objects.all().filter(
        account=account,
        id=list_id,
        is_deleted=False,
    ).first()

    if not urllist:
        return operation_response(error=True, message="List does not exist")

    # todo: how to work with data types in dicts like this?
    cleaned_urls = clean_urls(urls)  # type: ignore

    if cleaned_urls['correct']:
        counters = _add_to_urls_to_urllist(account,
                                           urllist,
                                           urls=cleaned_urls['correct'])
    else:
        counters = {'added_to_list': 0, 'already_in_list': 0}

    result = {
        'incorrect_urls': cleaned_urls['incorrect'],
        'added_to_list': counters['added_to_list'],
        'already_in_list': counters['already_in_list']
    }

    return operation_response(success=True,
                              message="Valid urls have been added",
                              data=result)
예제 #17
0
def scan_urllist_now_ignoring_business_rules(urllist: UrlList):
    urllist = UrlList.objects.all().filter(pk=urllist.id).first()

    if not urllist:
        return operation_response(error=True,
                                  message="List could not be found.")

    try:
        create_dashboard_scan_tasks(urllist).apply_async()
    except ValueError:
        return operation_response(
            error=True,
            message="Password to the internet.nl API is not set or incorrect.")

    urllist.last_manual_scan = timezone.now()
    urllist.save()

    return operation_response(success=True, message="Scan started")
예제 #18
0
def scan_status(account: Account, urllist_id: int):
    urllist = UrlList.objects.all().filter(account=account,
                                           id=urllist_id).first()
    if not urllist:
        return operation_response(error=True, message="list_does_not_exist")

    scan = SubdomainDiscoveryScan.objects.all().filter(urllist=urllist).last()
    if not scan:
        return operation_response(error=True, message="not_scanned_at_all")

    return {
        'success': True,
        'error': False,
        'state': scan.state,
        'state_message': scan.state_message,
        'state_changed_on': scan.state_changed_on,
        'domains_discovered':
        json.loads(scan.domains_discovered) if scan.domains_discovered else {}
    }
예제 #19
0
def scan_now(account, user_input) -> Dict[str, Any]:
    urllist = UrlList.objects.all().filter(
        account=account, id=user_input.get('id', -1),
        is_deleted=False).annotate(num_urls=Count('urls')).first()

    if not urllist:
        return operation_response(error=True,
                                  message="List could not be found.")

    if not urllist.is_scan_now_available():
        return operation_response(
            error=True,
            message="Not all conditions for initiating a scan are met.")

    # make sure there are no errors on this list:
    max_urls = config.DASHBOARD_MAXIMUM_DOMAINS_PER_LIST
    if urllist.num_urls > max_urls:
        return operation_response(
            error=True,
            message=
            f"Cannot scan: Amount of urls exceeds the maximum of {max_urls}.")

    if not account.connect_to_internet_nl_api(account.internet_nl_api_username,
                                              account.decrypt_password()):
        return operation_response(
            error=True,
            message=f"Credentials for the internet.nl API are not valid.")

    # Make sure the fernet key is working fine, you are on the correct queue (-Q storage) and that the correct API
    # version is used.
    # Run this before updating the list, as this might go wrong for many reasons.
    initialize_scan(urllist, manual_or_scheduled="manual")

    # done: have to update the list info. On the other hand: there is no guarantee that this task already has started
    # ...to fix this issue, we'll use a 'last_manual_scan' field.
    urllist.last_manual_scan = timezone.now()
    urllist.save()

    return operation_response(success=True, message="Scan started")
def session_login_(request):
    """
    Note that login is not possible, as the session cookie must be set correctly. The CSRF is tied to the session
    cookie, so you cannot retrieve that in a different fashion. There are frameworks that allow you to login
    such as djoser, but they DO NOT do second factor authentication and there is nothing equivalent to
    django_second_factor_auth. So all logins and session management must be done, until we drop second factor auth
    or when there is a json api available for the latter.

    :param request:
    :return:
    """
    # taken from: https://stackoverflow.com/questions/11891322/setting-up-a-user-login-in-python-django-using-json-and-
    if request.method != 'POST':
        sleep(2)
        return operation_response(error=True, message="post_only")

    # get the json data:
    parameters = get_json_body(request)

    username = parameters.get('username', '').strip()
    password = parameters.get('password', '').strip()

    if not username or not password:
        sleep(2)
        return operation_response(error=True, message="no_credentials_supplied")

    user = authenticate(username=username, password=password)

    if user is None:
        sleep(2)
        return operation_response(error=True, message="invalid_credentials")

    if not user.is_active:
        sleep(2)
        return operation_response(error=True, message="user_not_active")

    # todo: implement generate_challenge and verify_token, so we can do login from new site.
    devices = TOTPDevice.objects.all().filter(user=user, confirmed=True)
    if devices:
        sleep(2)
        return operation_response(error=True, message="second_factor_login_required")

    login(request, user)
    return operation_response(success=True, message="logged_in")
예제 #21
0
def save_report_settings(account, report_settings):
    account.report_settings = report_settings.get('filters', {})
    account.save()

    return operation_response(success=True, message="Settings updated")
예제 #22
0
def get_report_settings(account):
    return operation_response(
        success=True,
        message="report.settings.restored_from_database",
        data=account.report_settings if account.report_settings else {})
예제 #23
0
def update_list_settings(account: Account, user_input: Dict) -> Dict[str, Any]:
    """

    This cannot update the urls, as that would increase complexity too much.

    :param account:
    :param user_input: {
        'id': int,
        'name': str,
        'enable_scans': bool,
        'scan_type': str,

        # todo: Who should set this? Should this be set by admins? How can we avoid permission hell?
        # Probably as long as the settings are not too detailed / too frequently.
        'automated_scan_frequency': str,
    }
    :return:
    """

    expected_keys = [
        'id', 'name', 'enable_scans', 'scan_type', 'automated_scan_frequency',
        'scheduled_next_scan'
    ]
    if check_keys(expected_keys, user_input):
        return operation_response(error=True, message="Missing settings.")

    prefetch_last_scan = Prefetch(
        'accountinternetnlscan_set',
        queryset=AccountInternetNLScan.objects.order_by('-id').select_related(
            'scan'),
        to_attr='last_scan')

    last_report_prefetch = Prefetch(
        'urllistreport_set',
        # filter(pk=UrlListReport.objects.latest('id').pk).
        queryset=UrlListReport.objects.order_by('-id').only('id', 'at_when'),
        to_attr='last_report')

    urllist = UrlList.objects.all().filter(account=account,
                                           id=user_input['id'],
                                           is_deleted=False).prefetch_related(
                                               prefetch_last_scan,
                                               last_report_prefetch).first()

    if not urllist:
        return operation_response(error=True, message="No list of urls found.")

    # Yes, you can try and set any value. Values that are not recognized do not result in errors / error messages,
    # instead they will be overwritten with the default. This means less interaction with users / less annoyance over
    # errors on such simple forms.
    frequency = validate_list_automated_scan_frequency(
        user_input['automated_scan_frequency'])
    data = {
        'id': urllist.id,
        'account': account,
        'name': validate_list_name(user_input['name']),
        'enable_scans': bool(user_input['enable_scans']),
        'scan_type': validate_list_scan_type(user_input['scan_type']),
        'automated_scan_frequency': frequency,
        'scheduled_next_scan': UrlList.determine_next_scan_moment(frequency),
    }

    updated_urllist = UrlList(**data)
    updated_urllist.save()

    # make sure the account is serializable.
    data['account'] = account.id

    # inject the last scan information.
    data['last_scan_id'] = None if not len(
        urllist.last_scan) else urllist.last_scan[0].scan.id
    data['last_scan'] = None if not len(
        urllist.last_scan) else urllist.last_scan[0].scan.started_on.isoformat(
        )
    data['last_scan_finished'] = None if not len(
        urllist.last_scan) else urllist.last_scan[0].scan.finished
    data['last_report_id'] = None if not len(
        urllist.last_report) else urllist.last_report[0].id
    data['last_report_date'] = None if not len(
        urllist.last_report) else urllist.last_report[0].at_when

    data['scan_now_available'] = updated_urllist.is_scan_now_available()

    log.debug(data)

    return operation_response(success=True,
                              message="Updated list settings",
                              data=data)
예제 #24
0
def alter_url_in_urllist(account, data) -> Dict[str, Any]:
    # data = {'list_id': list.id, 'url_id': url.id, 'new_url_string': url.url}

    expected_keys = ['list_id', 'url_id', 'new_url_string']
    if check_keys(expected_keys, data):
        return operation_response(error=True, message="Missing keys in data.")

    # what was the old id we're changing?
    old_url = Url.objects.all().filter(pk=data['url_id']).first()
    if not old_url:
        return operation_response(error=True,
                                  message="The old url does not exist.")

    if old_url.url == data['new_url_string']:
        # no changes
        return operation_response(success=True, message="Saved.")

    # is this really a list?
    urllist = UrlList.objects.all().filter(account=account,
                                           pk=data['list_id']).first()
    if not urllist:
        return operation_response(error=True, message="List does not exist.")

    # is the url valid?
    if not is_valid_url(data['new_url_string']):
        return operation_response(
            error=True, message="New url does not have the correct format.")

    # fetch the url, or create it if it doesn't exist.
    new_url, created = get_url(data['new_url_string'])

    # don't throw away the url, only from the list. (don't call delete, as it will delete the record)
    urllist.urls.remove(old_url)
    # Save after deletion, in case the same url is added it will not cause a foreign key error.
    urllist.save()

    urllist.urls.add(new_url)
    urllist.save()

    # somewhat inefficient to do 4 queries, yet, good enough
    old_url_has_mail_endpoint = Endpoint.objects.all().filter(
        url=old_url, is_dead=False, protocol='dns_soa').exists()
    old_url_has_web_endpoint = Endpoint.objects.all().filter(
        url=old_url, is_dead=False, protocol='dns_a_aaa').exists()

    if not created:
        new_url_has_mail_endpoint = Endpoint.objects.all().filter(
            url=new_url, is_dead=False, protocol='dns_soa').exists()
        new_url_has_web_endpoint = Endpoint.objects.all().filter(
            url=new_url, is_dead=False, protocol='dns_a_aaa').exists()
    else:
        new_url_has_mail_endpoint = 'unknown'
        new_url_has_web_endpoint = 'unknown'

    return operation_response(success=True,
                              message="Saved.",
                              data={
                                  'created': {
                                      'id': new_url.id,
                                      'url': new_url.url,
                                      'created_on': new_url.created_on,
                                      'has_mail_endpoint':
                                      new_url_has_mail_endpoint,
                                      'has_web_endpoint':
                                      new_url_has_web_endpoint
                                  },
                                  'removed': {
                                      'id': old_url.id,
                                      'url': old_url.url,
                                      'created_on': old_url.created_on,
                                      'has_mail_endpoint':
                                      old_url_has_mail_endpoint,
                                      'has_web_endpoint':
                                      old_url_has_web_endpoint
                                  },
                              })
def save_ad_hoc_tagged_report(account: Account, report_id: int, tags: List[str], at_when: Optional[datetime]):
    report = ad_hoc_report_create(account, report_id, tags, at_when)
    # A new ID saves as a new record
    report.id = None
    report.save()
    return operation_response(success=True)
예제 #26
0
def update_list_settings(account: Account, user_input: Dict) -> Dict[str, Any]:
    """

    This cannot update the urls, as that would increase complexity too much.

    :param account:
    :param user_input: {
        'id': int,
        'name': str,
        'enable_scans': bool,
        'scan_type': str,

        # todo: Who should set this? Should this be set by admins? How can we avoid permission hell?
        # Probably as long as the settings are not too detailed / too frequently.
        'automated_scan_frequency': str,
    }
    :return:
    """

    expected_keys = [
        'id', 'name', 'enable_scans', 'scan_type', 'automated_scan_frequency',
        'scheduled_next_scan'
    ]
    if not keys_are_present_in_object(expected_keys, user_input):
        return operation_response(error=True, message="Missing settings.")

    prefetch_last_scan = Prefetch(
        'accountinternetnlscan_set',
        queryset=AccountInternetNLScan.objects.order_by('-id').select_related(
            'scan'),
        to_attr='last_scan')

    last_report_prefetch = Prefetch(
        'urllistreport_set',
        # filter(pk=UrlListReport.objects.latest('id').pk).
        queryset=UrlListReport.objects.order_by('-id').only('id', 'at_when'),
        to_attr='last_report')

    urllist = UrlList.objects.all().filter(
        account=account, id=user_input['id'],
        is_deleted=False).annotate(num_urls=Count('urls')).prefetch_related(
            prefetch_last_scan, last_report_prefetch).first()

    if not urllist:
        return operation_response(error=True, message="No list of urls found.")

    # Yes, you can try and set any value. Values that are not recognized do not result in errors / error messages,
    # instead they will be overwritten with the default. This means less interaction with users / less annoyance over
    # errors on such simple forms.
    frequency = validate_list_automated_scan_frequency(
        user_input['automated_scan_frequency'])
    data = {
        'id': urllist.id,
        'account': account,
        'name': validate_list_name(user_input['name']),
        'enable_scans': bool(user_input['enable_scans']),
        'scan_type': validate_list_scan_type(user_input['scan_type']),
        'automated_scan_frequency': frequency,
        'scheduled_next_scan': determine_next_scan_moment(frequency),
    }

    updated_urllist = UrlList(**data)
    updated_urllist.save()

    # make sure the account is serializable, inject other data.
    data['account'] = account.id
    data['num_urls'] = urllist.num_urls
    data['last_scan_id'] = None
    data['last_scan_state'] = None
    data['last_scan'] = None
    data['last_scan_finished'] = None
    data['last_report_id'] = None
    data['last_report_date'] = None

    if urllist.last_scan:
        data['last_scan_id'] = urllist.last_scan[0].scan.id
        data['last_scan_state'] = urllist.last_scan[0].state
        data['last_scan'] = urllist.last_scan[0].started_on.isoformat()
        data['last_scan_finished'] = urllist.last_scan[0].state in [
            "finished", "cancelled"
        ]

    if urllist.last_report:
        data['last_report_id'] = urllist.last_report[0].id
        data['last_report_date'] = urllist.last_report[0].at_when

    data['scan_now_available'] = updated_urllist.is_scan_now_available()

    # list warnings (might do: make more generic, only if another list warning ever could occur.)
    list_warnings = []
    if urllist.num_urls > config.DASHBOARD_MAXIMUM_DOMAINS_PER_LIST:
        list_warnings.append('WARNING_DOMAINS_IN_LIST_EXCEED_MAXIMUM_ALLOWED')
    data['list_warnings'] = []

    log.debug(data)

    # Sprinkling an activity stream action.
    action.send(account,
                verb='updated list',
                target=updated_urllist,
                public=False)

    return operation_response(success=True,
                              message="Updated list settings",
                              data=data)