Skip to content

Commit 021f104

Browse files
committed
[wip] add domain upload and download features
1 parent a935f70 commit 021f104

File tree

7 files changed

+353
-26
lines changed

7 files changed

+353
-26
lines changed

dashboard/internet_nl_dashboard/logic/domains.py

+26
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from actstream import action
88
from constance import config
99
from django.db.models import Count, Prefetch
10+
from django.http import JsonResponse
1011
from django.utils import timezone
1112
from websecmap.organizations.models import Url
1213
from websecmap.scanners.models import Endpoint
@@ -18,6 +19,9 @@
1819
determine_next_scan_moment)
1920
from dashboard.internet_nl_dashboard.scanners.scan_internet_nl_per_account import (initialize_scan,
2021
update_state)
22+
import pyexcel as p
23+
24+
from dashboard.internet_nl_dashboard.views.download_spreadsheet import create_spreadsheet_download
2125

2226
log = logging.getLogger(__package__)
2327

@@ -796,3 +800,25 @@ def delete_url_from_urllist(account: Account, urllist_id: int, url_id: int) -> b
796800
urllist.urls.remove(url_is_in_list)
797801

798802
return True
803+
804+
805+
def download_as_spreadsheet(account: Account, urllist_id: int, file_type: str = "xlsx") -> Any:
806+
807+
urls = TaggedUrlInUrllist.objects.all().filter(
808+
urllist__account=account,
809+
urllist__pk=urllist_id
810+
)
811+
812+
if not urls:
813+
return JsonResponse({})
814+
815+
# results is a matrix / 2-d array / array with arrays.
816+
data: List[List[Any]] = []
817+
data += [["List(s)", "Domain(s)", "Tags"]]
818+
819+
for url in urls.all():
820+
data += [[url.urllist.name, url.url.url, ", ".join(url.tags.values_list('name', flat=True))]]
821+
822+
book = p.get_book(bookdict={"Domains": data})
823+
824+
return create_spreadsheet_download("internet dashboard list", book, file_type)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 3.2.16 on 2023-03-02 14:18
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('internet_nl_dashboard', '0012_urllistreport_is_shared_on_homepage'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='urllist',
15+
name='automatically_share_new_reports',
16+
field=models.BooleanField(default=False, help_text='Sharing can be disabled and re-enabled where the report code and the share code (password) stay the same. Sharing means that all new reports will be made public under a set of standard urls.'),
17+
),
18+
migrations.AddField(
19+
model_name='urllist',
20+
name='default_public_share_code_for_new_reports',
21+
field=models.CharField(blank=True, default='', help_text='An unencrypted share code that can be seen by all users in an account. Can be modified by all. New reports get this code set automatically. You can change this per report. An empty field means no share code and the report is accessible publicly.', max_length=64),
22+
),
23+
migrations.AddField(
24+
model_name='urllist',
25+
name='enable_report_sharing_page',
26+
field=models.BooleanField(default=False, help_text='When true there will be page under the list-id that shows all reports that are shared publicly.'),
27+
),
28+
]

dashboard/internet_nl_dashboard/models.py

+26
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,32 @@ class UrlList(models.Model):
226226
blank=True
227227
)
228228

229+
enable_report_sharing_page = models.BooleanField(
230+
default=False,
231+
help_text="When true there will be page under the list-id that shows all reports that are shared publicly."
232+
)
233+
234+
# will be available under: /public/account-id/list-id/latest
235+
# will be available under: /public/account-id/list-id/list (for a list of public reports for this list)
236+
# and /public/account-id/list-id/report-id
237+
# and /public/account-id/list-name-slug/latest
238+
# and /public/account-id/list-name-slug/report-id
239+
automatically_share_new_reports = models.BooleanField(
240+
help_text="Sharing can be disabled and re-enabled where the report code and the share code (password) "
241+
"stay the same. Sharing means that all new reports will be made public under a set of standard urls.",
242+
default=False
243+
)
244+
245+
default_public_share_code_for_new_reports = models.CharField(
246+
max_length=64,
247+
help_text="An unencrypted share code that can be seen by all users in an account. Can be modified by all. "
248+
"New reports get this code set automatically. You can change this per report. An empty field "
249+
"means no share code and the report is accessible publicly.",
250+
blank=True,
251+
default=""
252+
)
253+
254+
229255
def __str__(self):
230256
return "%s/%s" % (self.account, self.name)
231257

dashboard/internet_nl_dashboard/urls.py

+1
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def to_url(value):
5050
path('data/urllist/url/save/', domains.alter_url_in_urllist_),
5151
path('data/urllist/url/add/', domains.add_urls_to_urllist),
5252
path('data/urllist/url/delete/', domains.delete_url_from_urllist_),
53+
path('data/urllist/download/', domains.download_list_),
5354

5455
path('data/urllist/tag/add/', tags.add_tag_),
5556
path('data/urllist/tag/remove/', tags.remove_tag_),

dashboard/internet_nl_dashboard/views/domains.py

+10-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from django.contrib.auth.decorators import login_required
55
from django.http import JsonResponse
6+
from django.views.decorators.http import require_http_methods
67
from websecmap.app.common import JSEncoder
78

89
from dashboard.internet_nl_dashboard.logic.domains import (alter_url_in_urllist, cancel_scan,
@@ -13,7 +14,7 @@
1314
get_urllists_from_account,
1415
save_urllist_content,
1516
save_urllist_content_by_name, scan_now,
16-
update_list_settings)
17+
update_list_settings, download_as_spreadsheet)
1718
from dashboard.internet_nl_dashboard.views import LOGIN_URL, get_account, get_json_body
1819

1920

@@ -81,3 +82,11 @@ def cancel_scan_(request):
8182
request = get_json_body(request)
8283
response = cancel_scan(account, request.get('id'))
8384
return JsonResponse(response)
85+
86+
87+
@login_required(login_url=LOGIN_URL)
88+
@require_http_methods(["POST"])
89+
def download_list_(request):
90+
params = get_json_body(request)
91+
return download_as_spreadsheet(get_account(request), params.get('list-id', None), params.get('file-type', None))
92+
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
# SPDX-License-Identifier: Apache-2.0
22
import logging
3+
from typing import Any
34

45
import django_excel as excel
56
from django.contrib.auth.decorators import login_required
67
from django.http import HttpResponse, JsonResponse
78
from django.utils.text import slugify
8-
from websecmap.app.common import JSEncoder
99

1010
from dashboard.internet_nl_dashboard.logic.report_to_spreadsheet import (create_spreadsheet,
1111
upgrade_excel_spreadsheet)
@@ -20,30 +20,39 @@ def download_spreadsheet(request, report_id, file_type) -> HttpResponse:
2020

2121
filename, spreadsheet = create_spreadsheet(account=account, report_id=report_id)
2222

23-
if not spreadsheet:
24-
return JsonResponse({}, encoder=JSEncoder)
25-
2623
if file_type == "xlsx":
2724
# todo: requesting infinite files will flood the system as temp files are saved. Probably load file into
2825
# memory and then remove the original file. With the current group of users the risk is minimal, so no bother
29-
tmp_file_handle = upgrade_excel_spreadsheet(spreadsheet)
30-
with open(tmp_file_handle.name, 'rb') as file_handle:
31-
response = HttpResponse(file_handle.read(),
32-
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
33-
response["Content-Disposition"] = f"attachment; filename={slugify(filename)}.xlsx"
34-
return response
35-
36-
if file_type == "ods":
37-
output: HttpResponse = excel.make_response(spreadsheet, file_type)
38-
output["Content-Disposition"] = f"attachment; filename={slugify(filename)}.ods"
39-
output["Content-type"] = "application/vnd.oasis.opendocument.spreadsheet"
40-
return output
41-
42-
if file_type == "csv":
43-
output = excel.make_response(spreadsheet, file_type)
44-
output["Content-Disposition"] = f"attachment; filename={slugify(filename)}.csv"
45-
output["Content-type"] = "text/csv"
46-
return output
47-
48-
# anything that is not valid at all.
49-
return JsonResponse({}, encoder=JSEncoder)
26+
27+
# Upgrading happens with openpyxl which supports formulas. You cannot open those files with django_excel as
28+
# that does _not_ understand formulas and will simply delete them.
29+
file_type = "xlsx-openpyxl"
30+
spreadsheet = upgrade_excel_spreadsheet(spreadsheet)
31+
32+
return create_spreadsheet_download(filename, spreadsheet, file_type)
33+
34+
35+
def create_spreadsheet_download(file_name: str, spreadsheet_data: Any, file_type: str = "xlsx") -> HttpResponse:
36+
37+
if file_type not in ["xlsx", "ods", "csv", "xlsx-openpyxl"] or not spreadsheet_data or not file_name:
38+
return JsonResponse({})
39+
40+
content_types = {
41+
"csv": "text/csv",
42+
"ods": "application/vnd.oasis.opendocument.spreadsheet",
43+
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
44+
"xlsx-openpyxl": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
45+
}
46+
47+
if file_type == "xlsx-openpyxl":
48+
with open(spreadsheet_data.name, 'rb') as file_handle:
49+
output = HttpResponse(file_handle.read())
50+
file_type = "xlsx"
51+
else:
52+
# Simple xls files and such
53+
output: HttpResponse = excel.make_response(spreadsheet_data, file_type)
54+
55+
output["Content-Disposition"] = f"attachment; filename={slugify(file_name)}.{file_type}"
56+
output["Content-type"] = content_types[file_type]
57+
58+
return output

0 commit comments

Comments
 (0)