diff --git a/base/templates/base.html b/base/templates/base.html index a156083..04aa2cf 100644 --- a/base/templates/base.html +++ b/base/templates/base.html @@ -16,194 +16,214 @@ # --> {% endcomment %} -{% load static %} -{% load bootstrap3 %} -{% load quickparts %} -{% load cache %} - - - - - - - - - - DART - {% block title %}{% endblock %} - - {% bootstrap_css %} - - - - - - - - - - - - - - - - - - - - {% block additional_head_content %}{% endblock %} - - - - - -
- {% bootstrap_messages %} -
- {% block main_heading %}{% endblock %} - - {% block content %}{% endblock %} -
-
- -
- -
- -
-
- - {% cache 600 legend_partial_bottom %}{% legend_partial 'bottom' %}{% endcache %} - - - - {% bootstrap_javascript %} - - - - - - - - - - - - +{% load static %} +{% load bootstrap3 %} +{% load quickparts %} +{% load cache %} + + + + + + + DART - + {% block title %}{% endblock %} + + {% bootstrap_css %} + + + + + + + + + + + + + + + {% block additional_head_content %}{% endblock %} + + + +
+ {% bootstrap_messages %} +
+ {% block main_heading %}{% endblock %} + {% block content %}{% endblock %} +
+
+
+ +
+ +
+
+ + {% cache 600 legend_partial_bottom %}{% legend_partial 'bottom' %}{% endcache %} + + + {% bootstrap_javascript %} + + + + + + + + diff --git a/dart/settings.py b/dart/settings.py index 3f98db7..2296d9f 100644 --- a/dart/settings.py +++ b/dart/settings.py @@ -21,87 +21,88 @@ # Do NOT expose this application to an untrusted network. import os + BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -DART_VERSION_NUMBER = '2.1.1' +DART_VERSION_NUMBER = "2.1.1" # SECURITY WARNING # We are not randomizing this key for you. -SECRET_KEY = '5s9G+t##Trga48t594g1g8sret*(#*/rg-dfgs43wt)((dh/*d' +SECRET_KEY = "5s9G+t##Trga48t594g1g8sret*(#*/rg-dfgs43wt)((dh/*d" -ALLOWED_HOSTS = ['*'] +ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.admindocs', - 'bootstrap3', - 'base', - 'missions.apps.MissionsConfig', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "django.contrib.admindocs", + "bootstrap3", + "base", + "missions.apps.MissionsConfig", ) MIDDLEWARE = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'django.middleware.clickjacking.XFrameOptionsMiddleware', + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", ) -DEBUG=True +DEBUG = True TEMPLATES = [ { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - #Hard coding path for now. Try without this path or use Base_dir as described here; #https://stackoverflow.com/questions/3038459/django-template-path - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.contrib.auth.context_processors.auth', - 'django.template.context_processors.debug', - 'django.template.context_processors.i18n', - 'django.template.context_processors.media', - 'django.template.context_processors.static', - 'django.template.context_processors.tz', - 'django.contrib.messages.context_processors.messages', - 'missions.contextprocessors.context_processors.version_number', - 'django.template.context_processors.request' + "BACKEND": "django.template.backends.django.DjangoTemplates", + # Hard coding path for now. Try without this path or use Base_dir as described here; #https://stackoverflow.com/questions/3038459/django-template-path + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.contrib.auth.context_processors.auth", + "django.template.context_processors.debug", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "django.template.context_processors.tz", + "django.contrib.messages.context_processors.messages", + "missions.contextprocessors.context_processors.version_number", + "django.template.context_processors.request", ], - # SECURITY WARNING - # We run in debug mode so that static files are automatically served - # out by the built-in django webserver for ease of setup. Since you're already running - # this on a trusted network (remember the security warning at the top of this file) you - # are probably okay doing the same. - 'debug': DEBUG, - 'libraries' : { - 'staticfiles': 'django.templatetags.static', - } + # SECURITY WARNING + # We run in debug mode so that static files are automatically served + # out by the built-in django webserver for ease of setup. Since you're already running + # this on a trusted network (remember the security warning at the top of this file) you + # are probably okay doing the same. + "debug": DEBUG, + "libraries": { + "staticfiles": "django.templatetags.static", + }, }, }, ] -ROOT_URLCONF = 'dart.urls' +ROOT_URLCONF = "dart.urls" # WSGI_APPLICATION = 'dart.wsgi.application' DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'data/db.sqlite3'), + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": os.path.join(BASE_DIR, "data/db.sqlite3"), } } -LANGUAGE_CODE = 'en-us' +LANGUAGE_CODE = "en-us" -TIME_ZONE = 'America/New_York' +TIME_ZONE = "America/New_York" USE_I18N = True @@ -109,57 +110,53 @@ USE_TZ = True -STATIC_URL = '/static/' +STATIC_URL = "/static/" -MEDIA_URL = '/data/' -MEDIA_ROOT = os.path.join(BASE_DIR, 'supporting_data') +MEDIA_URL = "/data/" +MEDIA_ROOT = os.path.join(BASE_DIR, "supporting_data") # Bootstrap Settings BOOTSTRAP3 = { - 'css_url': STATIC_URL + 'vendor/css/bootstrap.min.css', - 'javascript_url': STATIC_URL + 'vendor/js/bootstrap.min.js', - 'jquery_url': STATIC_URL + 'vendor/js/jquery.min.js', - 'required_css_class': 'required', + "css_url": STATIC_URL + "vendor/css/bootstrap.min.css", + "javascript_url": STATIC_URL + "vendor/js/bootstrap.min.js", + "jquery_url": STATIC_URL + "vendor/js/jquery.min.js", + "required_css_class": "required", } LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'handlers': { - 'debug_file': { - 'level': 'DEBUG', - 'class': 'logging.handlers.RotatingFileHandler', - 'filename': os.path.join(BASE_DIR, 'logs/debug.log'), - 'maxBytes': 20000000, # 20MB - 'backupCount': 10, - 'formatter': 'verbose', + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "debug_file": { + "level": "DEBUG", + "class": "logging.handlers.RotatingFileHandler", + "filename": os.path.join(BASE_DIR, "logs/debug.log"), + "maxBytes": 20000000, # 20MB + "backupCount": 10, + "formatter": "verbose", }, - 'info_file': { - 'level': 'INFO', - 'class': 'logging.FileHandler', - 'filename': os.path.join(BASE_DIR, 'logs/info.log'), - 'formatter': 'verbose', + "info_file": { + "level": "INFO", + "class": "logging.FileHandler", + "filename": os.path.join(BASE_DIR, "logs/info.log"), + "formatter": "verbose", }, }, - 'loggers': { - 'missions': { - 'handlers': ['debug_file', 'info_file'], - 'level': 'DEBUG', - 'propagate': True, + "loggers": { + "missions": { + "handlers": ["debug_file", "info_file"], + "level": "DEBUG", + "propagate": True, }, }, - 'formatters': { - 'verbose': { - 'format': '%(levelname)s [%(asctime)s] %(name)s: %(message)s' - }, - 'simple': { - 'format': '%(levelname)s %(message)s' - }, + "formatters": { + "verbose": {"format": "%(levelname)s [%(asctime)s] %(name)s: %(message)s"}, + "simple": {"format": "%(levelname)s %(message)s"}, }, } -REPORT_TEMPLATE_PATH = os.path.join(BASE_DIR, '2016_Template.docx') -DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' +REPORT_TEMPLATE_PATH = os.path.join(BASE_DIR, "2016_Template.docx") +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" # Require an interstitial message to be displayed # @@ -168,4 +165,4 @@ # in hours as a positive integer or 0 to indicate it should be displayed once per application logon. # Omitting this setting will bypass the interstitial. # -#REQUIRED_INTERSTITIAL_DISPLAY_INTERVAL = 0 # In hours, or 0 for once per login +# REQUIRED_INTERSTITIAL_DISPLAY_INTERVAL = 0 # In hours, or 0 for once per login diff --git a/dart/urls.py b/dart/urls.py index 47dfb73..9dddf53 100644 --- a/dart/urls.py +++ b/dart/urls.py @@ -13,61 +13,99 @@ # limitations under the License. # -from django.conf.urls import include, url +from django.urls import include, re_path from django.contrib import admin from django.views.generic import RedirectView from django.contrib.staticfiles.urls import staticfiles_urlpatterns import missions.views from django.contrib.auth.decorators import login_required -from django.contrib.auth import logout, login from django.views.static import serve from django.conf import settings from django.contrib.auth.views import LoginView, LogoutView urlpatterns = [ - url(r'^missions/', include('missions.urls')), - - url(r'^$', RedirectView.as_view(url='/missions/', permanent=False)), - - url(r'^admin/', admin.site.urls), - - url(r'^accounts/logout/$', LogoutView.as_view(template_name='login.html'), name='logout'), - #url(r'^accounts/logout/$', logout, name='logout'), - #url(r'^accounts/login/$', login, {'template_name': 'login.html'}, name='login'), - url(r'^accounts/login/$', LoginView.as_view(template_name='login.html'), name='login'), - url(r'^accounts/$', RedirectView.as_view(url='/', permanent=False)), - url(r'^accounts/profile/$', RedirectView.as_view(url='/', permanent=False)), - - url(r'^settings/$', - login_required(missions.views.UpdateDynamicSettingsView.as_view()), name='update-settings'), - url(r'^settings/business-areas/$', - login_required(missions.views.BusinessAreaListView.as_view()), name='list-business-areas'), - url(r'^settings/update-business-area(?:/(?P\d+))?/$', - login_required(missions.views.business_area_handler), name='update-business-area'), - url(r'^settings/classifications/$', - login_required(missions.views.ClassificationListView.as_view()), name='list-classifications'), - url(r'^settings/update-classification(?:/(?P\d+))?/$', - login_required(missions.views.classification_handler), name='update-classification'), - url(r'^settings/colors/$', - login_required(missions.views.ColorListView.as_view()), name='list-colors'), - url(r'^settings/update-color(?:/(?P\d+))?/$', - login_required(missions.views.color_handler), name='update-color'), - url(r'^settings/create-account/$', - missions.views.CreateAccountView.as_view(), name='create-account'), - - url(r'^login-interstitial/$', - login_required(missions.views.LoginInterstitialView.as_view()), name='login-interstitial'), - - url(r'^about/$', - login_required(missions.views.AboutTemplateView.as_view()), name='about'), - - url(r'^hosts/(?P\d+)/$', - login_required(missions.views.mission_host_handler), name='host-detail'), - - url(r'^data/(?P\d+)/$', - login_required(missions.views.DownloadSupportingDataView.as_view()), name='data-view'), - url(r'^data/(?P.*)$', - serve,{'document_root': settings.MEDIA_ROOT, 'show_indexes': False}), + re_path(r"^missions/", include("missions.urls")), + re_path(r"^$", RedirectView.as_view(url="/missions/", permanent=False)), + re_path(r"^admin/", admin.site.urls), + re_path( + r"^accounts/logout/$", + LogoutView.as_view(template_name="login.html"), + name="logout", + ), + # url(r'^accounts/logout/$', logout, name='logout'), + # url(r'^accounts/login/$', login, {'template_name': 'login.html'}, name='login'), + re_path( + r"^accounts/login/$", + LoginView.as_view(template_name="login.html"), + name="login", + ), + re_path(r"^accounts/$", RedirectView.as_view(url="/", permanent=False)), + re_path(r"^accounts/profile/$", RedirectView.as_view(url="/", permanent=False)), + re_path( + r"^settings/$", + login_required(missions.views.UpdateDynamicSettingsView.as_view()), + name="update-settings", + ), + re_path( + r"^settings/business-areas/$", + login_required(missions.views.BusinessAreaListView.as_view()), + name="list-business-areas", + ), + re_path( + r"^settings/update-business-area(?:/(?P\d+))?/$", + login_required(missions.views.business_area_handler), + name="update-business-area", + ), + re_path( + r"^settings/classifications/$", + login_required(missions.views.ClassificationListView.as_view()), + name="list-classifications", + ), + re_path( + r"^settings/update-classification(?:/(?P\d+))?/$", + login_required(missions.views.classification_handler), + name="update-classification", + ), + re_path( + r"^settings/colors/$", + login_required(missions.views.ColorListView.as_view()), + name="list-colors", + ), + re_path( + r"^settings/update-color(?:/(?P\d+))?/$", + login_required(missions.views.color_handler), + name="update-color", + ), + re_path( + r"^settings/create-account/$", + missions.views.CreateAccountView.as_view(), + name="create-account", + ), + re_path( + r"^login-interstitial/$", + login_required(missions.views.LoginInterstitialView.as_view()), + name="login-interstitial", + ), + re_path( + r"^about/$", + login_required(missions.views.AboutTemplateView.as_view()), + name="about", + ), + re_path( + r"^hosts/(?P\d+)/$", + login_required(missions.views.mission_host_handler), + name="host-detail", + ), + re_path( + r"^data/(?P\d+)/$", + login_required(missions.views.DownloadSupportingDataView.as_view()), + name="data-view", + ), + re_path( + r"^data/(?P.*)$", + serve, + {"document_root": settings.MEDIA_ROOT, "show_indexes": False}, + ), ] urlpatterns += staticfiles_urlpatterns() diff --git a/dart/wsgi.py b/dart/wsgi.py index 94f8cf0..32db63b 100644 --- a/dart/wsgi.py +++ b/dart/wsgi.py @@ -20,7 +20,9 @@ """ import os + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dart.settings") from django.core.wsgi import get_wsgi_application + application = get_wsgi_application() diff --git a/missions/apps.py b/missions/apps.py index e73f13c..5b3adcd 100644 --- a/missions/apps.py +++ b/missions/apps.py @@ -16,12 +16,11 @@ import logging from django.apps import AppConfig -from django.conf import settings logger = logging.getLogger(__name__) class MissionsConfig(AppConfig): - name = 'missions' - verbose_name = 'Missions' + name = "missions" + verbose_name = "Missions" diff --git a/missions/contextprocessors/context_processors.py b/missions/contextprocessors/context_processors.py index 4456263..ba1a54e 100644 --- a/missions/contextprocessors/context_processors.py +++ b/missions/contextprocessors/context_processors.py @@ -17,4 +17,4 @@ def version_number(request): - return {'DART_VERSION_NUMBER': settings.DART_VERSION_NUMBER} + return {"DART_VERSION_NUMBER": settings.DART_VERSION_NUMBER} diff --git a/missions/extras/helpers/analytics.py b/missions/extras/helpers/analytics.py index 976a6cb..3e6c320 100644 --- a/missions/extras/helpers/analytics.py +++ b/missions/extras/helpers/analytics.py @@ -23,18 +23,21 @@ class MissionAnalytics(object): - def __init__(self, mission_id): self.mission_id = mission_id # All analytics exclude hidden test cases so we don't report # numbers of tests greater than what the customer ultimately sees - self.testcases = TestDetail.objects.filter(mission=self.mission_id).exclude(test_case_include_flag=False) + self.testcases = TestDetail.objects.filter(mission=self.mission_id).exclude( + test_case_include_flag=False + ) - self.test_case_types_by_mission_week = self._count_of_test_case_types_by_mission_week() + self.test_case_types_by_mission_week = ( + self._count_of_test_case_types_by_mission_week() + ) def count_of_findings(self): - count = self.testcases.exclude(findings='').count() + count = self.testcases.exclude(findings="").count() return count def count_of_test_cases(self): @@ -42,30 +45,32 @@ def count_of_test_cases(self): return count def count_of_executed_test_cases(self): - count = self.testcases.exclude(execution_status='N').count() + count = self.testcases.exclude(execution_status="N").count() return count def count_of_test_cases_approved(self): - count = self.testcases.filter(test_case_status='FINAL').count() + count = self.testcases.filter(test_case_status="FINAL").count() return count def mission_execution_percentage(self): - if self.count_of_test_cases() == 0: # prevent division by 0 percentage = 0 else: - percentage = self.count_of_executed_test_cases() / self.count_of_test_cases() - return '{:.0%}'.format(percentage) + percentage = ( + self.count_of_executed_test_cases() / self.count_of_test_cases() + ) + return "{:.0%}".format(percentage) def mission_completion_percentage(self): - if self.count_of_test_cases() == 0: # prevent division by 0 percentage = 0 else: - percentage = self.count_of_test_cases_approved() / self.count_of_test_cases() - return '{:.0%}'.format(percentage) + percentage = ( + self.count_of_test_cases_approved() / self.count_of_test_cases() + ) + return "{:.0%}".format(percentage) def count_of_test_cases_by_result(self): """ @@ -86,8 +91,8 @@ def nested_tuple_to_shallow_list(tup): for result in TestDetail.EXECUTION_STATUS_OPTIONS: nested_tuple_to_shallow_list(result) - for tc_result in self.testcases.values('execution_status'): - index = output_list[0].index(tc_result['execution_status']) + for tc_result in self.testcases.values("execution_status"): + index = output_list[0].index(tc_result["execution_status"]) output_list[2][index] += 1 return output_list[1:] # No need to return the lookup values list @@ -102,12 +107,16 @@ def count_of_test_cases_by_mission_week(self): return [0] # Get the execution date for each test case in the mission - tc_dates = self.testcases.exclude(execution_status='N').values('attack_time_date') + tc_dates = self.testcases.exclude(execution_status="N").values( + "attack_time_date" + ) # Create a hashmap of the count of TCs per iso calendar week weekly_count = Counter() for tc_date in tc_dates: - isocalendar_week = tc_date['attack_time_date'].isocalendar()[1] # Grab the isocalendar Week # + isocalendar_week = tc_date["attack_time_date"].isocalendar()[ + 1 + ] # Grab the isocalendar Week # weekly_count[isocalendar_week] += 1 # Get the lowest & highest key values - these are week 1 and the last week respectively @@ -134,17 +143,21 @@ def _count_of_test_case_types_by_mission_week(self): """ if self.count_of_executed_test_cases() == 0: - return [['No TCs have been executed yet!']] + return [["No TCs have been executed yet!"]] # Get the execution date & type for each executed test case in the mission - tc_records = self.testcases.exclude(execution_status='N').values('attack_time_date', 'attack_phase') + tc_records = self.testcases.exclude(execution_status="N").values( + "attack_time_date", "attack_phase" + ) # Create a hashmap of the count of TCs per iso calendar week weekly_count = defaultdict(Counter) for tc_record in tc_records: - isocalendar_week = tc_record['attack_time_date'].isocalendar()[1] # Grab the isocalendar Week # - attack_phase = tc_record['attack_phase'] + isocalendar_week = tc_record["attack_time_date"].isocalendar()[ + 1 + ] # Grab the isocalendar Week # + attack_phase = tc_record["attack_phase"] weekly_count[isocalendar_week][attack_phase] += 1 # Get the lowest & highest key values - these are week 1 and the last week respectively @@ -157,11 +170,11 @@ def _count_of_test_case_types_by_mission_week(self): zero_indexed_phase_count_by_week = [] header_row = list() - header_row.append('') + header_row.append("") header_row.extend(list(range(1, week_delta))) total_row = list() - total_row.append('TOTAL') + total_row.append("TOTAL") total_row.extend([0] * week_delta) for phase_tuple in TestDetail.ATTACK_PHASES: diff --git a/missions/extras/helpers/formatters.py b/missions/extras/helpers/formatters.py index 6f64611..3f82b3b 100644 --- a/missions/extras/helpers/formatters.py +++ b/missions/extras/helpers/formatters.py @@ -33,6 +33,8 @@ def join_as_compacted_paragraphs(paragraphs): :return: String with \n separated paragraphs and no extra whitespace """ - paragraphs[:] = [' '.join(p.split()) for p in paragraphs] # Remove extra whitespace & newlines + paragraphs[:] = [ + " ".join(p.split()) for p in paragraphs + ] # Remove extra whitespace & newlines - return '\n'.join(paragraphs) + return "\n".join(paragraphs) diff --git a/missions/extras/helpers/sorters.py b/missions/extras/helpers/sorters.py index 1847b71..e264d74 100644 --- a/missions/extras/helpers/sorters.py +++ b/missions/extras/helpers/sorters.py @@ -22,12 +22,15 @@ class TestSortingHelper(object): - @staticmethod def deconflict_and_update(mission_id, tests=None): - """ Ensures the mission's testdetail_sort_order is accurate and returns an ordered array of current test Ids """ + """Ensures the mission's testdetail_sort_order is accurate and returns an ordered array of current test Ids""" - logger.debug('Performing TC deconflict and update (mission {mission})'.format(mission=mission_id)) + logger.debug( + "Performing TC deconflict and update (mission {mission})".format( + mission=mission_id + ) + ) mission_model = Mission.objects.get(pk=mission_id) sort_order = json.loads(mission_model.testdetail_sort_order) @@ -43,38 +46,52 @@ def deconflict_and_update(mission_id, tests=None): # If tests exist, but there's no sort order, this may be the first run of this DART version; # We should preserve existing sort order and build the testdetail_sort_order field if tests and not sort_order: - logger.debug('TC Deconflict found TCs but no existing order (mission {mission}); ' + - 'Building sort order from test case numbers'.format(mission=mission_id)) - tests.order_by('test_number') + logger.debug( + "TC Deconflict found TCs but no existing order (mission {mission}); " + + "Building sort order from test case numbers".format() + ) + tests.order_by("test_number") sort_order = [test.id for test in tests] # Perform a reconcile to catch edge cases where tests have been added or deleted - logger.debug('TC Deconflict is reconciling sort order with the database (mission {mission})' - .format(mission=mission_id)) + logger.debug( + "TC Deconflict is reconciling sort order with the database (mission {mission})".format( + mission=mission_id + ) + ) test_ids_from_db = [test.id for test in tests] for x in sort_order: if x in test_ids_from_db: deconflicted_sort_order.append(x) else: - logger.info('Sort order is dirty (TC has been deleted from DB) (mission {mission}, tc {tc})' - .format(mission=mission_id, tc=x)) + logger.info( + "Sort order is dirty (TC has been deleted from DB) (mission {mission}, tc {tc})".format( + mission=mission_id, tc=x + ) + ) sort_order_dirty = True # Id in mission's sort order, but not database (it's been deleted) for x in test_ids_from_db: if x not in deconflicted_sort_order: deconflicted_sort_order.append(x) - logger.info('Sort order is dirty (TC has been added to DB) (mission {mission}, tc {tc})' - .format(mission=mission_id, tc=x)) + logger.info( + "Sort order is dirty (TC has been added to DB) (mission {mission}, tc {tc})".format( + mission=mission_id, tc=x + ) + ) sort_order_dirty = True # Id in the database, but not in the mission's sort order (it's been added) # Save the reconciled sort order if sort_order_dirty: mission_model.testdetail_sort_order = json.dumps(deconflicted_sort_order) - mission_model.save(update_fields=['testdetail_sort_order']) - logger.info('Reconciliation Performed (Mission %s): ' - 'Mission Sort Order: %s; ' - 'Database TC Records: %s; ' - 'Result of Deconflict: %s' % (mission_id, sort_order, test_ids_from_db, deconflicted_sort_order)) + mission_model.save(update_fields=["testdetail_sort_order"]) + logger.info( + "Reconciliation Performed (Mission %s): " + "Mission Sort Order: %s; " + "Database TC Records: %s; " + "Result of Deconflict: %s" + % (mission_id, sort_order, test_ids_from_db, deconflicted_sort_order) + ) return deconflicted_sort_order @@ -82,8 +99,11 @@ def deconflict_and_update(mission_id, tests=None): def deconflict_and_update_supporting_data_sort_order(testdetail_id, testdata=None): """Ensures the supporting_data_sort_order of a test is accurate. Returns ordered array of supporting_data ids""" - logger.debug('Performing Supporting Data deconflict and update (testdetail {testdetail})' - .format(testdetail=testdetail_id)) + logger.debug( + "Performing Supporting Data deconflict and update (testdetail {testdetail})".format( + testdetail=testdetail_id + ) + ) testdetail_model = TestDetail.objects.get(pk=testdetail_id) sort_order = json.loads(testdetail_model.supporting_data_sort_order) @@ -92,43 +112,60 @@ def deconflict_and_update_supporting_data_sort_order(testdetail_id, testdata=Non # Preserve existing sort order and build the testdetail_sort_order field if testdata and not sort_order: - logger.debug('Supporting data for test found but no existing order (testdetail {testdetail}); ' + - 'Building default sort order'.format(testdetail=testdetail_id)) + logger.debug( + "Supporting data for test found but no existing order (testdetail {testdetail}); " + + "Building default sort order".format() + ) sort_order = [data.id for data in testdata] # Perform a reconcile to catch edge cases where testdata have been added or deleted - logger.debug('Supporting Data Deconflict is reconciling sort order with the database (testdetail {testdetail})' - .format(testdetail=testdetail_id)) + logger.debug( + "Supporting Data Deconflict is reconciling sort order with the database (testdetail {testdetail})".format( + testdetail=testdetail_id + ) + ) data_ids_from_db = [data.id for data in testdata] for x in sort_order: if x in data_ids_from_db: deconflicted_sort_order.append(x) else: - logger.info('Sort order is dirty (Supporting data has been deleted from DB) ' - '(testdetail {testdetail}, sd {sd})'.format(testdetail=testdetail_id, sd=x)) + logger.info( + "Sort order is dirty (Supporting data has been deleted from DB) " + "(testdetail {testdetail}, sd {sd})".format( + testdetail=testdetail_id, sd=x + ) + ) sort_order_dirty = True # Id in mission's sort order, but not database (it's been deleted) for x in data_ids_from_db: if x not in deconflicted_sort_order: deconflicted_sort_order.append(x) - logger.info('Sort order is dirty (Supporting data has been added to DB) ' - '(testdetail {testdetail}, sd {sd})'.format(testdetail=testdetail_id, sd=x)) + logger.info( + "Sort order is dirty (Supporting data has been added to DB) " + "(testdetail {testdetail}, sd {sd})".format( + testdetail=testdetail_id, sd=x + ) + ) sort_order_dirty = True # Id in the database, but not in the testdetail sort order (it's been added) # Save the reconciled sort order if sort_order_dirty: - testdetail_model.supporting_data_sort_order = json.dumps(deconflicted_sort_order) - testdetail_model.save(update_fields=['supporting_data_sort_order']) - logger.info('Reconciliation Performed (TestDetail %s): ' - 'TestDetail Supporting Data Sort Order: %s; ' - 'Suppporting Data from Database: %s; ' - 'Result of Deconflict: %s' % (testdetail_id, sort_order, data_ids_from_db, deconflicted_sort_order)) + testdetail_model.supporting_data_sort_order = json.dumps( + deconflicted_sort_order + ) + testdetail_model.save(update_fields=["supporting_data_sort_order"]) + logger.info( + "Reconciliation Performed (TestDetail %s): " + "TestDetail Supporting Data Sort Order: %s; " + "Suppporting Data from Database: %s; " + "Result of Deconflict: %s" + % (testdetail_id, sort_order, data_ids_from_db, deconflicted_sort_order) + ) return deconflicted_sort_order @classmethod def get_ordered_testdetails(cls, mission_id, reportable_tests_only=False): - try: tests = TestDetail.objects.filter(mission=mission_id) except TestDetail.DoesNotExist: @@ -139,29 +176,44 @@ def get_ordered_testdetails(cls, mission_id, reportable_tests_only=False): # Order excluded_tests = [] if reportable_tests_only: - excluded_tests = tests.filter(test_case_include_flag=False).values_list('id', flat=True) + excluded_tests = tests.filter(test_case_include_flag=False).values_list( + "id", flat=True + ) test_dict = dict([(test.id, test) for test in tests]) - ordered_tests = [test_dict[test_id] for test_id in sort_order if test_id not in excluded_tests] + ordered_tests = [ + test_dict[test_id] + for test_id in sort_order + if test_id not in excluded_tests + ] return ordered_tests @classmethod - def get_ordered_supporting_data(cls, test_detail_id, reportable_supporting_data_only=False): - + def get_ordered_supporting_data( + cls, test_detail_id, reportable_supporting_data_only=False + ): try: testdata = SupportingData.objects.filter(test_detail=test_detail_id) except TestDetail.DoesNotExist: testdata = () - sort_order = cls.deconflict_and_update_supporting_data_sort_order(test_detail_id, testdata) + sort_order = cls.deconflict_and_update_supporting_data_sort_order( + test_detail_id, testdata + ) # Order excluded_data = [] if reportable_supporting_data_only: - excluded_data = testdata.filter(include_flag=False).values_list('id', flat=True) + excluded_data = testdata.filter(include_flag=False).values_list( + "id", flat=True + ) testdata_dict = dict([(data.id, data) for data in testdata]) - ordered_testdata = [testdata_dict[testdata_id] for testdata_id in sort_order if testdata_id not in excluded_data] + ordered_testdata = [ + testdata_dict[testdata_id] + for testdata_id in sort_order + if testdata_id not in excluded_data + ] return ordered_testdata diff --git a/missions/extras/utils.py b/missions/extras/utils.py index 8a54595..1aabd1b 100644 --- a/missions/extras/utils.py +++ b/missions/extras/utils.py @@ -31,7 +31,11 @@ from docx import Document from docx.shared import Inches, RGBColor -from docx.image.exceptions import UnrecognizedImageError, UnexpectedEndOfFileError, InvalidImageStreamError +from docx.image.exceptions import ( + UnrecognizedImageError, + UnexpectedEndOfFileError, + InvalidImageStreamError, +) from missions.models import Mission, DARTDynamicSettings, TestDetail from .helpers.sorters import TestSortingHelper @@ -42,10 +46,10 @@ class ReturnStatus(object): - def __init__(self, success=True, message='', **kwargs): + def __init__(self, success=True, message="", **kwargs): self.success = success self.message = message - self.data = kwargs.get('data') or {} + self.data = kwargs.get("data") or {} def to_json(self): return json.dumps(self.__dict__) @@ -54,7 +58,7 @@ def to_dict(self): return self.__dict__ def __str__(self): - return str(self.success) + ': ' + str(self.message) + return str(self.success) + ": " + str(self.message) def copy_table(document, table, cut=False): @@ -86,30 +90,32 @@ def get_cleared_paragraph(cell): def generate_report_or_attachments(mission_id, zip_attachments=False): - ''' + """ Generates the report docx or attachments zip. :param mission_id: The id of the mission :param zip_attachments: True to return a zip of attachments, False to get the docx report as an IOStream :return: Returns StringIO if returning a report, a zip object otherwise - ''' - system_classification = DARTDynamicSettings.objects.get_as_object().system_classification + """ + system_classification = ( + DARTDynamicSettings.objects.get_as_object().system_classification + ) system_classification_verbose = system_classification.verbose_legend system_classification_short = system_classification.short_legend mission = Mission.objects.get(id=mission_id) tests = TestSortingHelper.get_ordered_testdetails( - mission_id=mission_id, - reportable_tests_only=True) + mission_id=mission_id, reportable_tests_only=True + ) # Set some values we'll use throughout this section total_reportable_tests = len(tests) total_tests_with_findings = TestDetail.objects.filter( - mission=mission, - has_findings=True).count() + mission=mission, has_findings=True + ).count() total_tests_without_findings = TestDetail.objects.filter( - mission=mission, - has_findings=False).count() - LIGHTEST_PERMISSIBLE_CLASSIFICATION_LABEL_COLOR = 0xbbbbbb + mission=mission, has_findings=False + ).count() + LIGHTEST_PERMISSIBLE_CLASSIFICATION_LABEL_COLOR = 0xBBBBBB DARKEN_OVERLY_LIGHT_CLASSIFICATION_LABEL_COLOR_BY = 0x444444 mission_data_dir = None report_has_attachments = False @@ -117,34 +123,36 @@ def generate_report_or_attachments(mission_id, zip_attachments=False): def replace_document_slugs(doc): """Cycle through the runs in each paragraph in the template & replace handlebar slugs""" - logger.debug('> replace_document_slugs') + logger.debug("> replace_document_slugs") handlebar_slugs = { - r'{{AREA}}': str(mission.business_area), - r'{{MISSION}}': str(mission.mission_name), - r'{{GENERATION_DATE}}': now().strftime('%x'), - r'{{TOTAL_TESTS}}': str(total_reportable_tests), - r'{{TESTS_WITH_FINDINGS}}': str(total_tests_with_findings), - r'{{TESTS_WITHOUT_FINDINGS}}': str(total_tests_without_findings), + r"{{AREA}}": str(mission.business_area), + r"{{MISSION}}": str(mission.mission_name), + r"{{GENERATION_DATE}}": now().strftime("%x"), + r"{{TOTAL_TESTS}}": str(total_reportable_tests), + r"{{TESTS_WITH_FINDINGS}}": str(total_tests_with_findings), + r"{{TESTS_WITHOUT_FINDINGS}}": str(total_tests_without_findings), } for p in doc.paragraphs: for r in p.runs: for pattern in list(handlebar_slugs.keys()): if re.search(pattern, r.text): - logger.debug('>> Replaced: {old} With: {new}'.format( - old=r.text.encode('utf-8'), - new=handlebar_slugs[pattern].encode('utf-8') + logger.debug( + ">> Replaced: {old} With: {new}".format( + old=r.text.encode("utf-8"), + new=handlebar_slugs[pattern].encode("utf-8"), ) ) r.text = re.sub(pattern, handlebar_slugs[pattern], r.text) def get_or_create_mission_data_dir(mission_data_dir): - if mission_data_dir is None : + if mission_data_dir is None: mission_data_dir = os.path.join( settings.BASE_DIR, - 'SUPPORTING_DATA_PACKAGE', - str(mission.id) + "_" + str(now().strftime('%Y%m%d-%H%M%S'))) + "SUPPORTING_DATA_PACKAGE", + str(mission.id) + "_" + str(now().strftime("%Y%m%d-%H%M%S")), + ) if not os.path.isdir(mission_data_dir): os.makedirs(mission_data_dir) return mission_data_dir @@ -159,16 +167,13 @@ def add_to_data_dir(mission_data_dir, test_case_number, supporting_data): os.makedirs(path) # Copy file to destination path - shutil.copy( - os.path.join(settings.MEDIA_ROOT, supporting_data.filename()), - path - ) + shutil.copy(os.path.join(settings.MEDIA_ROOT, supporting_data.filename()), path) def prepend_classification(text): - return '(' + system_classification_short + ') ' + text + return "(" + system_classification_short + ") " + text def portion_mark_and_insert(paragraphs, document): - for paragraph in normalize_newlines(paragraphs).split('\n'): + for paragraph in normalize_newlines(paragraphs).split("\n"): if len(paragraph) > 0: document.add_paragraph(prepend_classification(paragraph)) @@ -181,49 +186,51 @@ def portion_mark_and_insert(paragraphs, document): data_table = document.tables[2] # Set the classification legend color from the background color of the banner - classification_style = document.styles['Table Classification'] + classification_style = document.styles["Table Classification"] classification_font = classification_style.font # RGBColor doesn't handle shorthand hex codes, so let's just go ahead and expand it # if we come across a legacy or "misguided" entry if len(system_classification.background_color.hex_color_code) == 3: - new_hex_code = '' + new_hex_code = "" for char in system_classification.background_color.hex_color_code: new_hex_code += char + char system_classification.background_color.hex_color_code = new_hex_code system_classification.background_color.save() if len(system_classification.text_color.hex_color_code) == 3: - new_hex_code = '' + new_hex_code = "" for char in system_classification.text_color.hex_color_code: new_hex_code += char + char system_classification.text_color.hex_color_code = new_hex_code system_classification.text_color.save() - classification_font.color.rgb = RGBColor.from_string(system_classification.get_report_label_color().hex_color_code) + classification_font.color.rgb = RGBColor.from_string( + system_classification.get_report_label_color().hex_color_code + ) # Intro H1 and text - document.add_heading('Introduction', level=1) + document.add_heading("Introduction", level=1) portion_mark_and_insert(mission.introduction, document) # Scope H1 and text - document.add_heading('Scope', level=1) + document.add_heading("Scope", level=1) portion_mark_and_insert(mission.scope, document) # Objectives H1 and text - document.add_heading('Objectives', level=1) + document.add_heading("Objectives", level=1) portion_mark_and_insert(mission.objectives, document) # Exec Summary H1 and text - document.add_heading('Executive Summary', level=1) + document.add_heading("Executive Summary", level=1) portion_mark_and_insert(mission.executive_summary, document) # Technical Assessment / Attack Architecture and text H1 - document.add_heading('Technical Assessment / Attack Architecture', level=1) + document.add_heading("Technical Assessment / Attack Architecture", level=1) portion_mark_and_insert(mission.technical_assessment_overview, document) # Technical Assessment / Test Cases and Results and loop - document.add_heading('Technical Assessment / Test Cases and Results', level=1) + document.add_heading("Technical Assessment / Test Cases and Results", level=1) # For each test, Test # - Objective Attack Phase: H2 @@ -252,7 +259,6 @@ def portion_mark_and_insert(paragraphs, document): mission_data_dir = get_or_create_mission_data_dir(mission_data_dir) for t in tests: - if test_case_number > 0: document.add_page_break() @@ -266,31 +272,31 @@ def portion_mark_and_insert(paragraphs, document): tests_without_findings += 1 if t.enclave: - test_title += "(%s) %s" % ( - t.enclave, - t.test_objective - ) + test_title += "(%s) %s" % (t.enclave, t.test_objective) else: - test_title += "%s" % ( - t.test_objective, - ) + test_title += "%s" % (t.test_objective,) document.add_heading(test_title, level=2) # Duplicate one of the pre-made tables; if this is the last of a specific type of # test case (findings / no findings), use the cut operation to remove the blank # table. - #TODO: convert to logging: print("T w/ F: {0}\nTotal: {1}\nT w/o F: {2}\nTotal: {3}".format(tests_with_findings, total_tests_with_findings, tests_without_findings, total_tests_without_findings)) - is_last_test_case = True if test_case_number == total_reportable_tests else False + # TODO: convert to logging: print("T w/ F: {0}\nTotal: {1}\nT w/o F: {2}\nTotal: {3}".format(tests_with_findings, total_tests_with_findings, tests_without_findings, total_tests_without_findings)) + is_last_test_case = ( + True if test_case_number == total_reportable_tests else False + ) if t.has_findings: if tests_with_findings == total_tests_with_findings or is_last_test_case: table = copy_table(document, table_with_findings, cut=True) else: - table = copy_table(document,table_with_findings, cut=False) + table = copy_table(document, table_with_findings, cut=False) else: - if tests_without_findings == total_tests_without_findings or is_last_test_case: + if ( + tests_without_findings == total_tests_without_findings + or is_last_test_case + ): table = copy_table(document, table_no_findings, cut=True) else: - table = copy_table(document,table_no_findings, cut=False) + table = copy_table(document, table_no_findings, cut=False) # Classification Marking - Top cell = table.cell(0, 0) @@ -303,9 +309,11 @@ def portion_mark_and_insert(paragraphs, document): # Test Case Number (Table Header Row) cell = table.cell(1, 0) if mission.test_case_identifier: - get_cleared_paragraph(cell).text = 'Test #{0}-{1}'.format(mission.test_case_identifier, test_case_number) + get_cleared_paragraph(cell).text = "Test #{0}-{1}".format( + mission.test_case_identifier, test_case_number + ) else: - get_cleared_paragraph(cell).text = 'Test #{0}'.format(test_case_number) + get_cleared_paragraph(cell).text = "Test #{0}".format(test_case_number) row_number = 0 @@ -324,14 +332,18 @@ def portion_mark_and_insert(paragraphs, document): include_attack_phase = False include_attack_type = False - if mission.attack_phase_include_flag \ - and t.attack_phase_include_flag \ - and len(t.get_attack_phase_display())> 0: + if ( + mission.attack_phase_include_flag + and t.attack_phase_include_flag + and len(t.get_attack_phase_display()) > 0 + ): include_attack_phase = True - if mission.attack_type_include_flag \ - and t.attack_type_include_flag \ - and len(t.attack_type) > 0: + if ( + mission.attack_type_include_flag + and t.attack_type_include_flag + and len(t.attack_type) > 0 + ): include_attack_type = True left_cell = table.cell(row_number, 0) @@ -340,7 +352,9 @@ def portion_mark_and_insert(paragraphs, document): if include_attack_phase or include_attack_type: if include_attack_phase and include_attack_type: # Table text in column 1 assumes both items are included already - right_cell.text = ' - '.join([t.get_attack_phase_display(), t.attack_type]) + right_cell.text = " - ".join( + [t.get_attack_phase_display(), t.attack_type] + ) elif include_attack_phase: get_cleared_paragraph(left_cell).text = "Attack Phase:" right_cell.text = t.get_attack_phase_display() @@ -348,7 +362,7 @@ def portion_mark_and_insert(paragraphs, document): get_cleared_paragraph(left_cell).text = "Attack Type:" right_cell.text = t.attack_type else: - logger.debug('Removing Attack Phase/Type Row') + logger.debug("Removing Attack Phase/Type Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -360,7 +374,7 @@ def portion_mark_and_insert(paragraphs, document): cell = get_cleared_paragraph(table.cell(row_number, 1)) cell.text = standardize_report_output_field(t.assumptions) else: - logger.debug('Removing Assumptions Row') + logger.debug("Removing Assumptions Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -371,11 +385,15 @@ def portion_mark_and_insert(paragraphs, document): if mission.test_description_include_flag and t.test_description_include_flag: cell = get_cleared_paragraph(table.cell(row_number, 1)) if t.re_eval_test_case_number: - cell.text = standardize_report_output_field('This is a reevaluation; reference previous test case #{0}\n\n{1}'.format(t.re_eval_test_case_number, t.test_description)) + cell.text = standardize_report_output_field( + "This is a reevaluation; reference previous test case #{0}\n\n{1}".format( + t.re_eval_test_case_number, t.test_description + ) + ) else: cell.text = standardize_report_output_field(t.test_description) else: - logger.debug('Removing Description Row') + logger.debug("Removing Description Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -387,7 +405,7 @@ def portion_mark_and_insert(paragraphs, document): cell = get_cleared_paragraph(table.cell(row_number, 1)) cell.text = standardize_report_output_field(t.findings) else: - logger.debug('Removing Findings Row') + logger.debug("Removing Findings Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -399,7 +417,7 @@ def portion_mark_and_insert(paragraphs, document): cell = get_cleared_paragraph(table.cell(row_number, 1)) cell.text = standardize_report_output_field(t.mitigation) else: - logger.debug('Removing Mitigations Row') + logger.debug("Removing Mitigations Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -411,7 +429,7 @@ def portion_mark_and_insert(paragraphs, document): cell = get_cleared_paragraph(table.cell(row_number, 1)) cell.text = standardize_report_output_field(t.tools_used) else: - logger.debug('Removing Tools Row') + logger.debug("Removing Tools Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -423,7 +441,7 @@ def portion_mark_and_insert(paragraphs, document): cell = get_cleared_paragraph(table.cell(row_number, 1)) cell.text = standardize_report_output_field(t.command_syntax) else: - logger.debug('Removing Commands/Syntax Row') + logger.debug("Removing Commands/Syntax Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -433,9 +451,9 @@ def portion_mark_and_insert(paragraphs, document): row_number += 1 if mission.targets_include_flag and t.targets_include_flag: cell = get_cleared_paragraph(table.cell(row_number, 1)) - cell.text = '\n'.join([str(x) for x in t.target_hosts.all()]) + cell.text = "\n".join([str(x) for x in t.target_hosts.all()]) else: - logger.debug('Removing Targets Row') + logger.debug("Removing Targets Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -445,10 +463,10 @@ def portion_mark_and_insert(paragraphs, document): row_number += 1 if mission.sources_include_flag and t.sources_include_flag: cell = get_cleared_paragraph(table.cell(row_number, 1)) - cell.text = '\n'.join([str(x) for x in t.source_hosts.all()]) + cell.text = "\n".join([str(x) for x in t.source_hosts.all()]) else: - logger.debug('Removing Sources Row') + logger.debug("Removing Sources Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -458,9 +476,9 @@ def portion_mark_and_insert(paragraphs, document): row_number += 1 if mission.attack_time_date_include_flag and t.attack_time_date_include_flag: cell = get_cleared_paragraph(table.cell(row_number, 1)) - cell.text = localtime(t.attack_time_date).strftime('%b %d, %Y @ %I:%M %p') + cell.text = localtime(t.attack_time_date).strftime("%b %d, %Y @ %I:%M %p") else: - logger.debug('Removing Date/Time Row') + logger.debug("Removing Date/Time Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -468,11 +486,14 @@ def portion_mark_and_insert(paragraphs, document): # Side Effects # row_number += 1 - if mission.attack_side_effects_include_flag and t.attack_side_effects_include_flag: + if ( + mission.attack_side_effects_include_flag + and t.attack_side_effects_include_flag + ): cell = get_cleared_paragraph(table.cell(row_number, 1)) cell.text = standardize_report_output_field(t.attack_side_effects) else: - logger.debug('Removing Side Effects Row') + logger.debug("Removing Side Effects Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -480,11 +501,14 @@ def portion_mark_and_insert(paragraphs, document): # Details # row_number += 1 - if mission.test_result_observation_include_flag and t.test_result_observation_include_flag: + if ( + mission.test_result_observation_include_flag + and t.test_result_observation_include_flag + ): cell = get_cleared_paragraph(table.cell(row_number, 1)) cell.text = standardize_report_output_field(t.test_result_observation) else: - logger.debug('Removing Details Row') + logger.debug("Removing Details Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -498,7 +522,7 @@ def portion_mark_and_insert(paragraphs, document): supporting_data_cell = get_cleared_paragraph(table.cell(row_number, 1)) supporting_data_row = table.rows[row_number] else: - logger.debug('Removing Supporting Data Row') + logger.debug("Removing Supporting Data Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -506,8 +530,8 @@ def portion_mark_and_insert(paragraphs, document): # Notes - Used for post report generation notes / customer use # row_number += 1 - if False: #TODO: add mission-level toggle - logger.debug('Removing Customer Notes Row') + if False: # TODO: add mission-level toggle + logger.debug("Removing Customer Notes Row") remove_row(table, table.rows[row_number]) row_number -= 1 @@ -517,40 +541,44 @@ def portion_mark_and_insert(paragraphs, document): if mission.supporting_data_include_flag: my_data = TestSortingHelper.get_ordered_supporting_data( - test_detail_id=t.id, - reportable_supporting_data_only=True) + test_detail_id=t.id, reportable_supporting_data_only=True + ) if len(my_data) > 0: - is_first_screenshot = True for d in my_data: allowed_image_types = [ - 'gif', - 'tiff', - 'jpeg', - 'bmp', - 'png', + "gif", + "tiff", + "jpeg", + "bmp", + "png", ] try: file_path = os.path.join(settings.MEDIA_ROOT, d.filename()) - logger.debug('>> Beginning processing of {} at {}.' - .format( - d.filename(), - file_path, - )) + logger.debug( + ">> Beginning processing of {} at {}.".format( + d.filename(), + file_path, + ) + ) if imghdr.what(file_path) not in allowed_image_types: - raise UnrecognizedImageError('File type is not in the allowed image types. ' - 'Handling as non-image.') + raise UnrecognizedImageError( + "File type is not in the allowed image types. " + "Handling as non-image." + ) if is_first_screenshot: - document.add_heading('Screenshots / Diagrams', level=3) - logger.debug('This is the first screenshot of this test case.') + document.add_heading("Screenshots / Diagrams", level=3) + logger.debug( + "This is the first screenshot of this test case." + ) is_first_screenshot = False image_table = copy_table(document, data_table) - logger.debug('Creating a new image table.') + logger.debug("Creating a new image table.") document.add_paragraph() # Classification Marking - Top @@ -563,59 +591,82 @@ def portion_mark_and_insert(paragraphs, document): content_cell = image_table.cell(1, 0) - get_cleared_paragraph(content_cell).add_run().add_picture(d.test_file, width=Inches(5)) + get_cleared_paragraph(content_cell).add_run().add_picture( + d.test_file, width=Inches(5) + ) content_cell.paragraphs[0].add_run("\r" + d.caption) - except UnrecognizedImageError as e: - logger.debug('>> Attachment {attachment_name} not recognized as an image; adding as file.' - .format(attachment_name=d.filename())) - supporting_data_cell_items.append('- {filename}: {caption}'.format( - filename=d.filename(), - caption=d.caption, - )) + except UnrecognizedImageError: + logger.debug( + ">> Attachment {attachment_name} not recognized as an image; adding as file.".format( + attachment_name=d.filename() + ) + ) + supporting_data_cell_items.append( + "- {filename}: {caption}".format( + filename=d.filename(), + caption=d.caption, + ) + ) if zip_attachments: add_to_data_dir(mission_data_dir, test_case_number, d) - except (InvalidImageStreamError, - UnexpectedEndOfFileError) as e: - logger.warning('>> Attempting to add {file_name} to the report output resulted in an error: ' - '\n{trace}'.format(file_name=d.filename, trace=traceback.format_exc(10))) - except OSError as e: - logger.warning('>> Attempting to add {file_name} to the report output resulted in an error: ' - '\n{trace}'.format(file_name=d.filename, trace=traceback.format_exc(10))) + except (InvalidImageStreamError, UnexpectedEndOfFileError): + logger.warning( + ">> Attempting to add {file_name} to the report output resulted in an error: " + "\n{trace}".format( + file_name=d.filename, trace=traceback.format_exc(10) + ) + ) + except OSError: + logger.warning( + ">> Attempting to add {file_name} to the report output resulted in an error: " + "\n{trace}".format( + file_name=d.filename, trace=traceback.format_exc(10) + ) + ) try: if not d.test_file.closed: d.test_file.close() - except IOError as e: - logger.warning('>> Attempting to close {file_name} resulted in an error: ' - '\n{trace}'.format(file_name=d.filename, trace=traceback.format_exc(10))) + except IOError: + logger.warning( + ">> Attempting to close {file_name} resulted in an error: " + "\n{trace}".format( + file_name=d.filename, trace=traceback.format_exc(10) + ) + ) pass if len(supporting_data_cell_items) > 0: - logger.debug('There are {} data cell items for TC {}.'.format( - len(supporting_data_cell_items), - t.id) + logger.debug( + "There are {} data cell items for TC {}.".format( + len(supporting_data_cell_items), t.id + ) ) - supporting_data_cell.text = '\n'.join(supporting_data_cell_items) + supporting_data_cell.text = "\n".join(supporting_data_cell_items) if len(supporting_data_cell_items) == 0: - logger.debug('There are no supporting_data_cell_items; removing the supporting data row.') + logger.debug( + "There are no supporting_data_cell_items; removing the supporting data row." + ) remove_row(table, supporting_data_row) # Conclusion H1 and text - document.add_heading('Conclusion', level=1) + document.add_heading("Conclusion", level=1) portion_mark_and_insert(mission.conclusion, document) data_table.cell(0, 0).text = "" - get_cleared_paragraph(data_table.cell(1, 0)).text = "This table is used during report generation and can be deleted in the final report output." + get_cleared_paragraph( + data_table.cell(1, 0) + ).text = "This table is used during report generation and can be deleted in the final report output." data_table.cell(2, 0).text = "" # Replace document slugs replace_document_slugs(document) name = mission.mission_name if zip_attachments: - zip_file = shutil.make_archive(mission_data_dir, 'zip', mission_data_dir) - with open(mission_data_dir + '.zip', 'rb') as f: + zip_file = shutil.make_archive(mission_data_dir, "zip", mission_data_dir) + with open(mission_data_dir + ".zip", "rb") as f: return io.BytesIO(f.read()), name else: diff --git a/missions/extras/validators.py b/missions/extras/validators.py index 286d82d..926ed20 100644 --- a/missions/extras/validators.py +++ b/missions/extras/validators.py @@ -16,13 +16,15 @@ import re from django.core.exceptions import ValidationError -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ def validate_host_format_string(value): - regex_match = re.match(r'^((?:{name}|{ip}|-| |\(|\)|[a-zA-Z:]))+$', value) + regex_match = re.match(r"^((?:{name}|{ip}|-| |\(|\)|[a-zA-Z:]))+$", value) if not regex_match: raise ValidationError( - _('%(value)s includes invalid characters or tokens ({name}, {ip}, A-Z, a-z, :, (, and ) allowed.'), - params={'value': value}, + _( + "%(value)s includes invalid characters or tokens ({name}, {ip}, A-Z, a-z, :, (, and ) allowed." + ), + params={"value": value}, ) diff --git a/missions/management/commands/removeallusers.py b/missions/management/commands/removeallusers.py index 08f8b42..0d0ee25 100644 --- a/missions/management/commands/removeallusers.py +++ b/missions/management/commands/removeallusers.py @@ -13,18 +13,19 @@ # limitations under the License. # -from django.core.management.base import BaseCommand, CommandError +from django.core.management.base import BaseCommand from django.contrib.auth import get_user_model class Command(BaseCommand): - help = 'removes all users in the system, but leaves the data intact' + help = "removes all users in the system, but leaves the data intact" def handle(self, *args, **options): - User = get_user_model() users = User.objects.all() users.delete() - self.stdout.write('Successfully deleted all users. You should now be able to create a new user account via the ' - 'web interface.') + self.stdout.write( + "Successfully deleted all users. You should now be able to create a new user account via the " + "web interface." + ) diff --git a/missions/management/commands/setup-db.py b/missions/management/commands/setup-db.py index 15527c1..e2f36ab 100644 --- a/missions/management/commands/setup-db.py +++ b/missions/management/commands/setup-db.py @@ -27,36 +27,35 @@ import subprocess import sys -MANAGER = os.path.join(os. getcwd(), "manage.py") +MANAGER = os.path.join(os.getcwd(), "manage.py") PYTHON_INTERPRETER = sys.executable -print('\n\nDART First Run Script\nCreated by the Lockheed Martin Red Team') +print("\n\nDART First Run Script\nCreated by the Lockheed Martin Red Team") input("\n\nPress Enter to continue...") -print('\nEnsuring all migrations are made...') +print("\nEnsuring all migrations are made...") subprocess.call([PYTHON_INTERPRETER, MANAGER, "makemigrations"]) -print('\nExecuting migrations...') +print("\nExecuting migrations...") subprocess.call([PYTHON_INTERPRETER, MANAGER, "migrate"]) -print('\nSeeding the database...\nIf this is not the first time running this script. This step will reset to the default colors, classifications, or BAs from the initial data load. New entries will not be affected.') +print( + "\nSeeding the database...\nIf this is not the first time running this script. This step will reset to the default colors, classifications, or BAs from the initial data load. New entries will not be affected." +) seed_data = input("\nDo you want to seed the database? [y/N]") -if len(seed_data) and seed_data.strip().upper()[0] == 'Y': - print('\nRunning Fixture: common_classifications') - subprocess.call([PYTHON_INTERPRETER, MANAGER, "loaddata", - "common_classifications"]) +if len(seed_data) and seed_data.strip().upper()[0] == "Y": + print("\nRunning Fixture: common_classifications") + subprocess.call([PYTHON_INTERPRETER, MANAGER, "loaddata", "common_classifications"]) - print('\nRunning Fixture: common_bas') - subprocess.call([PYTHON_INTERPRETER, MANAGER, - "loaddata", "common_bas"]) + print("\nRunning Fixture: common_bas") + subprocess.call([PYTHON_INTERPRETER, MANAGER, "loaddata", "common_bas"]) add_user = input("\nDo you want add a super user? [y/N]") -while len(add_user) and add_user.strip().upper()[0] == 'Y': - subprocess.call([PYTHON_INTERPRETER, MANAGER, - "createsuperuser"]) +while len(add_user) and add_user.strip().upper()[0] == "Y": + subprocess.call([PYTHON_INTERPRETER, MANAGER, "createsuperuser"]) add_user = input("\nDo you want add a user? [y/N]") diff --git a/missions/middleware.py b/missions/middleware.py index 340c249..b292e9d 100644 --- a/missions/middleware.py +++ b/missions/middleware.py @@ -21,9 +21,6 @@ from django.core.urlresolvers import reverse_lazy from django.shortcuts import redirect from django.utils.timezone import timedelta, now -from django.contrib.auth import login -from django.contrib.auth.models import User -from django.http.response import HttpResponseServerError logger = logging.getLogger(__name__) @@ -48,22 +45,30 @@ def process_request(self, request): # Setting not defined, so assume we don't want the interstitial to display return None try: - if display_interval == 0 \ - and request.session['last_acknowledged_interstitial']: + if ( + display_interval == 0 + and request.session["last_acknowledged_interstitial"] + ): return None else: max_age = timedelta(hours=display_interval).total_seconds() - if timegm(now().timetuple()) - request.session['last_acknowledged_interstitial'] < max_age: + if ( + timegm(now().timetuple()) + - request.session["last_acknowledged_interstitial"] + < max_age + ): return None except KeyError: pass path = request.get_full_path() - if re.match(str(reverse_lazy('login-interstitial')), path) or \ - re.match(str(reverse_lazy('login')), path) or \ - re.match(str(reverse_lazy('logout')), path) or \ - re.match(settings.STATIC_URL + r'.+', path): + if ( + re.match(str(reverse_lazy("login-interstitial")), path) + or re.match(str(reverse_lazy("login")), path) + or re.match(str(reverse_lazy("logout")), path) + or re.match(settings.STATIC_URL + r".+", path) + ): return None - return redirect('login-interstitial') + return redirect("login-interstitial") diff --git a/missions/migrations/0001_initial.py b/missions/migrations/0001_initial.py index 2ca9262..f947b5e 100644 --- a/missions/migrations/0001_initial.py +++ b/missions/migrations/0001_initial.py @@ -23,151 +23,632 @@ class Migration(migrations.Migration): - - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='BusinessArea', + name="BusinessArea", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('name', models.TextField()), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("name", models.TextField()), ], ), migrations.CreateModel( - name='ClassificationLegend', + name="ClassificationLegend", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('verbose_legend', models.CharField(default=b'', max_length=200)), - ('short_legend', models.CharField(default=b'', max_length=100)), - ('report_label_color_selection', models.CharField(default=b'B', max_length=1, choices=[(b'T', b'Text Color'), (b'B', b'Back Color')])), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("verbose_legend", models.CharField(default=b"", max_length=200)), + ("short_legend", models.CharField(default=b"", max_length=100)), + ( + "report_label_color_selection", + models.CharField( + default=b"B", + max_length=1, + choices=[(b"T", b"Text Color"), (b"B", b"Back Color")], + ), + ), ], ), migrations.CreateModel( - name='Color', + name="Color", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('display_text', models.CharField(default=b'', max_length=30)), - ('hex_color_code', models.CharField(default=b'', max_length=6)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("display_text", models.CharField(default=b"", max_length=30)), + ("hex_color_code", models.CharField(default=b"", max_length=6)), ], ), migrations.CreateModel( - name='DARTDynamicSettings', + name="DARTDynamicSettings", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('host_output_format', models.CharField(default=b'{ip} ({name})', help_text=b'Use "{ip}" and "{name}" to specify how you want hosts to be displayed.', max_length=50, validators=[missions.extras.validators.validate_host_format_string])), - ('system_classification', models.ForeignKey(to='missions.ClassificationLegend', on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "host_output_format", + models.CharField( + default=b"{ip} ({name})", + help_text=b'Use "{ip}" and "{name}" to specify how you want hosts to be displayed.', + max_length=50, + validators=[ + missions.extras.validators.validate_host_format_string + ], + ), + ), + ( + "system_classification", + models.ForeignKey( + to="missions.ClassificationLegend", on_delete=models.CASCADE + ), + ), ], ), migrations.CreateModel( - name='Host', + name="Host", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('host_name', models.CharField(default=b'', max_length=100, blank=True)), - ('ip_address', models.GenericIPAddressField(null=True, blank=True)), - ('is_no_hit', models.BooleanField(default=False)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "host_name", + models.CharField(default=b"", max_length=100, blank=True), + ), + ("ip_address", models.GenericIPAddressField(null=True, blank=True)), + ("is_no_hit", models.BooleanField(default=False)), ], ), migrations.CreateModel( - name='Mission', + name="Mission", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('mission_name', models.CharField(max_length=255, verbose_name=b'Mission Name')), - ('mission_number', models.CharField(max_length=5, verbose_name=b'Mission Number')), - ('introduction', models.TextField(default=missions.models.introduction_default, verbose_name=b'Introduction', blank=True)), - ('executive_summary', models.TextField(default=missions.models.executive_summary_default, verbose_name=b'Executive Summary', blank=True)), - ('scope', models.TextField(default=missions.models.scope_default, verbose_name=b'Scope', blank=True)), - ('objectives', models.TextField(default=missions.models.objectives_default, verbose_name=b'Objectives', blank=True)), - ('technical_assessment_overview', models.TextField(default=missions.models.technical_assessment_overview_default, verbose_name=b'Technical Assessment / Attack Architecture Overview', blank=True)), - ('conclusion', models.TextField(default=missions.models.conclusion_default, verbose_name=b'Conclusion', blank=True)), - ('attack_phase_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Attack Phases in report?')), - ('attack_type_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Attack Types in report?')), - ('assumptions_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Assumptions in report?')), - ('test_description_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Test Descriptions in report?')), - ('findings_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Findings in report?')), - ('mitigation_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Mitigations in report?')), - ('tools_used_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Tools Used in report?')), - ('command_syntax_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Command Syntaxes in report?')), - ('targets_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Attack Targets in report?')), - ('sources_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Attack Sources in report?')), - ('attack_time_date_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Attack Times in report?')), - ('attack_side_effects_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Attack Side Effects in report?')), - ('test_result_observation_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Test Details in report?')), - ('supporting_data_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include Supporting Data Information in report?')), - ('customer_notes_include_flag', models.BooleanField(default=True, verbose_name=b'Mission Option: Include customer notes section in report?')), - ('testdetail_sort_order', models.TextField(default=b'[]', blank=True)), - ('business_area', models.ForeignKey(verbose_name=b'Business Area', to='missions.BusinessArea',on_delete=models.CASCADE)), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "mission_name", + models.CharField(max_length=255, verbose_name=b"Mission Name"), + ), + ( + "mission_number", + models.CharField(max_length=5, verbose_name=b"Mission Number"), + ), + ( + "introduction", + models.TextField( + default=missions.models.introduction_default, + verbose_name=b"Introduction", + blank=True, + ), + ), + ( + "executive_summary", + models.TextField( + default=missions.models.executive_summary_default, + verbose_name=b"Executive Summary", + blank=True, + ), + ), + ( + "scope", + models.TextField( + default=missions.models.scope_default, + verbose_name=b"Scope", + blank=True, + ), + ), + ( + "objectives", + models.TextField( + default=missions.models.objectives_default, + verbose_name=b"Objectives", + blank=True, + ), + ), + ( + "technical_assessment_overview", + models.TextField( + default=missions.models.technical_assessment_overview_default, + verbose_name=b"Technical Assessment / Attack Architecture Overview", + blank=True, + ), + ), + ( + "conclusion", + models.TextField( + default=missions.models.conclusion_default, + verbose_name=b"Conclusion", + blank=True, + ), + ), + ( + "attack_phase_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Attack Phases in report?", + ), + ), + ( + "attack_type_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Attack Types in report?", + ), + ), + ( + "assumptions_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Assumptions in report?", + ), + ), + ( + "test_description_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Test Descriptions in report?", + ), + ), + ( + "findings_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Findings in report?", + ), + ), + ( + "mitigation_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Mitigations in report?", + ), + ), + ( + "tools_used_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Tools Used in report?", + ), + ), + ( + "command_syntax_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Command Syntaxes in report?", + ), + ), + ( + "targets_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Attack Targets in report?", + ), + ), + ( + "sources_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Attack Sources in report?", + ), + ), + ( + "attack_time_date_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Attack Times in report?", + ), + ), + ( + "attack_side_effects_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Attack Side Effects in report?", + ), + ), + ( + "test_result_observation_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Test Details in report?", + ), + ), + ( + "supporting_data_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include Supporting Data Information in report?", + ), + ), + ( + "customer_notes_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Mission Option: Include customer notes section in report?", + ), + ), + ("testdetail_sort_order", models.TextField(default=b"[]", blank=True)), + ( + "business_area", + models.ForeignKey( + verbose_name=b"Business Area", + to="missions.BusinessArea", + on_delete=models.CASCADE, + ), + ), ], ), migrations.CreateModel( - name='SupportingData', + name="SupportingData", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('caption', models.TextField(verbose_name=b'Caption', blank=True)), - ('include_flag', models.BooleanField(default=True, verbose_name=b'Include attachment in report')), - ('test_file', models.FileField(upload_to=b'', verbose_name=b'Supporting Data')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("caption", models.TextField(verbose_name=b"Caption", blank=True)), + ( + "include_flag", + models.BooleanField( + default=True, verbose_name=b"Include attachment in report" + ), + ), + ( + "test_file", + models.FileField(upload_to=b"", verbose_name=b"Supporting Data"), + ), ], ), migrations.CreateModel( - name='TestDetail', + name="TestDetail", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('test_number', models.IntegerField(default=0, verbose_name=b'Test Number')), - ('test_case_include_flag', models.BooleanField(default=True, verbose_name=b'Include Test Case in report?')), - ('test_case_status', models.CharField(default=b'NEW', max_length=100, verbose_name=b'Test Case Status', choices=[(b'NEW', b'Not started'), (b'IN_WORK', b'In work'), (b'REVIEW', b'Ready for review'), (b'FINAL', b'Approved / Final')])), - ('enclave', models.CharField(max_length=100, verbose_name=b'Enclave Test Executed From', blank=True)), - ('test_objective', models.CharField(help_text=b'Brief objective (ex.Port Scan against xyz)', max_length=255, verbose_name=b'Test Objective / Title')), - ('attack_phase', models.CharField(max_length=20, verbose_name=b'Attack Phase', choices=[(b'RECON', b'Reconnaissance'), (b'WEP', b'Weaponization'), (b'DEL', b'Delivery'), (b'EXP', b'Exploitation'), (b'INS', b'Installation'), (b'C2', b'Command & Control'), (b'AOO', b'Actions on Objectives')])), - ('attack_phase_include_flag', models.BooleanField(default=True, verbose_name=b'Include Attack Phase in report?')), - ('attack_type', models.CharField(help_text=b'Example: SYN flood, UDP flood, malformed packets, web, fuzz, etc.', max_length=255, verbose_name=b'Attack Type', blank=True)), - ('attack_type_include_flag', models.BooleanField(default=True, verbose_name=b'Include Attack Type in report?')), - ('assumptions', models.TextField(help_text=b'Example: Attacker has a presence in xyz segment, etc.', verbose_name=b'Assumptions', blank=True)), - ('assumptions_include_flag', models.BooleanField(default=True, verbose_name=b'Include Assumptions in report?')), - ('test_description', models.TextField(help_text=b'Describe test case to be performed, what is the objective of this test case (Is it to deny, disrupt, penetrate, modify etc.)', verbose_name=b'Description', blank=True)), - ('test_description_include_flag', models.BooleanField(default=True, verbose_name=b'Include Test Description in report?')), - ('sources_include_flag', models.BooleanField(default=True, verbose_name=b'Include Attack Sources in report?')), - ('targets_include_flag', models.BooleanField(default=True, verbose_name=b'Include Attack Targets in report?')), - ('attack_time_date', models.DateTimeField(default=django.utils.timezone.now, help_text=b'Date/time attack was launched', verbose_name=b'Attack Date / Time', blank=True)), - ('attack_time_date_include_flag', models.BooleanField(default=True, verbose_name=b'Include Attack Time in report?')), - ('tools_used', models.TextField(help_text=b'Example: Burp Suite, Wireshark, NMap, etc.', verbose_name=b'Tools Used', blank=True)), - ('tools_used_include_flag', models.BooleanField(default=True, verbose_name=b'Include Tools Used in report?')), - ('command_syntax', models.TextField(help_text=b'Include sample command/syntax used if possible. If a script was used/created, what commands will run it?', verbose_name=b'Command/Syntax', blank=True)), - ('command_syntax_include_flag', models.BooleanField(default=True, verbose_name=b'Include Command Syntax in report?')), - ('test_result_observation', models.TextField(help_text=b'Example: An average of 8756 SYN-ACK/sec were received at the attack laptop over the 30 second attack period. Plots of traffic flows showed degradation of all flow performance during time 5s \xe2\x80\x93 40s after which they recovered.', verbose_name=b'Test Result Details', blank=True)), - ('test_result_observation_include_flag', models.BooleanField(default=True, verbose_name=b'Include Test Result in report?')), - ('attack_side_effects', models.TextField(help_text=b'List any observed side effects, if any. Example: Firewall X froze and subsequently crashed.', verbose_name=b'Attack Side Effects', blank=True)), - ('attack_side_effects_include_flag', models.BooleanField(default=True, verbose_name=b'Include Attack Side Effects in report?')), - ('execution_status', models.CharField(default=b'N', help_text=b'Execution status of the test case.', max_length=2, verbose_name=b'Execution Status', choices=[(b'N', b'Not Run'), (b'R', b'Run'), (b'C', b'Cancelled'), (b'NA', b'N/A')])), - ('has_findings', models.BooleanField(default=False)), - ('findings', models.TextField(help_text=b'Document the specific finding related to this test or against the main test objectives if applicable', verbose_name=b'Findings', blank=True)), - ('findings_include_flag', models.BooleanField(default=True, verbose_name=b'Include Findings in report?')), - ('mitigation', models.TextField(help_text=b'Example: Configure xyz, implement xyz, etc.', verbose_name=b'Mitigation', blank=True)), - ('mitigation_include_flag', models.BooleanField(default=True, verbose_name=b'Include Mitigations in report?')), - ('point_of_contact', models.CharField(default=b'', help_text=b'Individual working or most familiar with this test case.', max_length=20, verbose_name=b'POC', blank=True)), - ('mission', models.ForeignKey(verbose_name=b'Mission', to='missions.Mission',on_delete=models.CASCADE)), - ('source_hosts', models.ManyToManyField(related_name='source_set', to='missions.Host')), - ('target_hosts', models.ManyToManyField(related_name='target_set', to='missions.Host')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ( + "test_number", + models.IntegerField(default=0, verbose_name=b"Test Number"), + ), + ( + "test_case_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Test Case in report?" + ), + ), + ( + "test_case_status", + models.CharField( + default=b"NEW", + max_length=100, + verbose_name=b"Test Case Status", + choices=[ + (b"NEW", b"Not started"), + (b"IN_WORK", b"In work"), + (b"REVIEW", b"Ready for review"), + (b"FINAL", b"Approved / Final"), + ], + ), + ), + ( + "enclave", + models.CharField( + max_length=100, + verbose_name=b"Enclave Test Executed From", + blank=True, + ), + ), + ( + "test_objective", + models.CharField( + help_text=b"Brief objective (ex.Port Scan against xyz)", + max_length=255, + verbose_name=b"Test Objective / Title", + ), + ), + ( + "attack_phase", + models.CharField( + max_length=20, + verbose_name=b"Attack Phase", + choices=[ + (b"RECON", b"Reconnaissance"), + (b"WEP", b"Weaponization"), + (b"DEL", b"Delivery"), + (b"EXP", b"Exploitation"), + (b"INS", b"Installation"), + (b"C2", b"Command & Control"), + (b"AOO", b"Actions on Objectives"), + ], + ), + ), + ( + "attack_phase_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Attack Phase in report?" + ), + ), + ( + "attack_type", + models.CharField( + help_text=b"Example: SYN flood, UDP flood, malformed packets, web, fuzz, etc.", + max_length=255, + verbose_name=b"Attack Type", + blank=True, + ), + ), + ( + "attack_type_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Attack Type in report?" + ), + ), + ( + "assumptions", + models.TextField( + help_text=b"Example: Attacker has a presence in xyz segment, etc.", + verbose_name=b"Assumptions", + blank=True, + ), + ), + ( + "assumptions_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Assumptions in report?" + ), + ), + ( + "test_description", + models.TextField( + help_text=b"Describe test case to be performed, what is the objective of this test case (Is it to deny, disrupt, penetrate, modify etc.)", + verbose_name=b"Description", + blank=True, + ), + ), + ( + "test_description_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Include Test Description in report?", + ), + ), + ( + "sources_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Attack Sources in report?" + ), + ), + ( + "targets_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Attack Targets in report?" + ), + ), + ( + "attack_time_date", + models.DateTimeField( + default=django.utils.timezone.now, + help_text=b"Date/time attack was launched", + verbose_name=b"Attack Date / Time", + blank=True, + ), + ), + ( + "attack_time_date_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Attack Time in report?" + ), + ), + ( + "tools_used", + models.TextField( + help_text=b"Example: Burp Suite, Wireshark, NMap, etc.", + verbose_name=b"Tools Used", + blank=True, + ), + ), + ( + "tools_used_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Tools Used in report?" + ), + ), + ( + "command_syntax", + models.TextField( + help_text=b"Include sample command/syntax used if possible. If a script was used/created, what commands will run it?", + verbose_name=b"Command/Syntax", + blank=True, + ), + ), + ( + "command_syntax_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Command Syntax in report?" + ), + ), + ( + "test_result_observation", + models.TextField( + help_text=b"Example: An average of 8756 SYN-ACK/sec were received at the attack laptop over the 30 second attack period. Plots of traffic flows showed degradation of all flow performance during time 5s \xe2\x80\x93 40s after which they recovered.", + verbose_name=b"Test Result Details", + blank=True, + ), + ), + ( + "test_result_observation_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Test Result in report?" + ), + ), + ( + "attack_side_effects", + models.TextField( + help_text=b"List any observed side effects, if any. Example: Firewall X froze and subsequently crashed.", + verbose_name=b"Attack Side Effects", + blank=True, + ), + ), + ( + "attack_side_effects_include_flag", + models.BooleanField( + default=True, + verbose_name=b"Include Attack Side Effects in report?", + ), + ), + ( + "execution_status", + models.CharField( + default=b"N", + help_text=b"Execution status of the test case.", + max_length=2, + verbose_name=b"Execution Status", + choices=[ + (b"N", b"Not Run"), + (b"R", b"Run"), + (b"C", b"Cancelled"), + (b"NA", b"N/A"), + ], + ), + ), + ("has_findings", models.BooleanField(default=False)), + ( + "findings", + models.TextField( + help_text=b"Document the specific finding related to this test or against the main test objectives if applicable", + verbose_name=b"Findings", + blank=True, + ), + ), + ( + "findings_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Findings in report?" + ), + ), + ( + "mitigation", + models.TextField( + help_text=b"Example: Configure xyz, implement xyz, etc.", + verbose_name=b"Mitigation", + blank=True, + ), + ), + ( + "mitigation_include_flag", + models.BooleanField( + default=True, verbose_name=b"Include Mitigations in report?" + ), + ), + ( + "point_of_contact", + models.CharField( + default=b"", + help_text=b"Individual working or most familiar with this test case.", + max_length=20, + verbose_name=b"POC", + blank=True, + ), + ), + ( + "mission", + models.ForeignKey( + verbose_name=b"Mission", + to="missions.Mission", + on_delete=models.CASCADE, + ), + ), + ( + "source_hosts", + models.ManyToManyField( + related_name="source_set", to="missions.Host" + ), + ), + ( + "target_hosts", + models.ManyToManyField( + related_name="target_set", to="missions.Host" + ), + ), ], ), migrations.AddField( - model_name='supportingdata', - name='test_detail', - field=models.ForeignKey(verbose_name=b'Test Details', to='missions.TestDetail',on_delete=models.CASCADE), + model_name="supportingdata", + name="test_detail", + field=models.ForeignKey( + verbose_name=b"Test Details", + to="missions.TestDetail", + on_delete=models.CASCADE, + ), ), migrations.AddField( - model_name='host', - name='mission', - field=models.ForeignKey(to='missions.Mission',on_delete=models.CASCADE), + model_name="host", + name="mission", + field=models.ForeignKey(to="missions.Mission", on_delete=models.CASCADE), ), migrations.AddField( - model_name='classificationlegend', - name='background_color', - field=models.ForeignKey(related_name='classificationlegend_background_set', to='missions.Color',on_delete=models.CASCADE), + model_name="classificationlegend", + name="background_color", + field=models.ForeignKey( + related_name="classificationlegend_background_set", + to="missions.Color", + on_delete=models.CASCADE, + ), ), migrations.AddField( - model_name='classificationlegend', - name='text_color', - field=models.ForeignKey(related_name='classificationlegend_text_set', to='missions.Color',on_delete=models.CASCADE), + model_name="classificationlegend", + name="text_color", + field=models.ForeignKey( + related_name="classificationlegend_text_set", + to="missions.Color", + on_delete=models.CASCADE, + ), ), ] diff --git a/missions/migrations/0002_dataload_minimal_classification.py b/missions/migrations/0002_dataload_minimal_classification.py index 0f7edd0..fe62abe 100644 --- a/missions/migrations/0002_dataload_minimal_classification.py +++ b/missions/migrations/0002_dataload_minimal_classification.py @@ -31,24 +31,23 @@ def load_data(apps, schema_editor): - # Colors - Color = apps.get_model('missions', 'Color') - black = Color.objects.create(display_text='Black', hex_color_code='000000') - white = Color.objects.create(display_text='White', hex_color_code='ffffff') + Color = apps.get_model("missions", "Color") + black = Color.objects.create(display_text="Black", hex_color_code="000000") + white = Color.objects.create(display_text="White", hex_color_code="ffffff") # Legends - ClassificationLegend = apps.get_model('missions', 'ClassificationLegend') + ClassificationLegend = apps.get_model("missions", "ClassificationLegend") unrestricted = ClassificationLegend.objects.create( - verbose_legend='UNRESTRICTED', - short_legend='', + verbose_legend="UNRESTRICTED", + short_legend="", text_color=white, background_color=black, - report_label_color_selection='B', + report_label_color_selection="B", ) # Create an initial (default) settings load - DARTDynamicSettings = apps.get_model('missions', 'DARTDynamicSettings') + DARTDynamicSettings = apps.get_model("missions", "DARTDynamicSettings") qryset = DARTDynamicSettings.objects.all() @@ -64,11 +63,8 @@ def unload_data(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ - ('missions', '0001_initial'), + ("missions", "0001_initial"), ] - operations = [ - migrations.RunPython(load_data, unload_data) - ] + operations = [migrations.RunPython(load_data, unload_data)] diff --git a/missions/migrations/0003_auto_event-id.py b/missions/migrations/0003_auto_event-id.py index 3ccf55e..77d3f8f 100644 --- a/missions/migrations/0003_auto_event-id.py +++ b/missions/migrations/0003_auto_event-id.py @@ -20,20 +20,30 @@ class Migration(migrations.Migration): - dependencies = [ - ('missions', '0002_dataload_minimal_classification'), + ("missions", "0002_dataload_minimal_classification"), ] operations = [ migrations.AddField( - model_name='mission', - name='test_case_identifier', - field=models.CharField(default=b'', max_length=20, verbose_name=b'Test Case Identifier', blank=True), + model_name="mission", + name="test_case_identifier", + field=models.CharField( + default=b"", + max_length=20, + verbose_name=b"Test Case Identifier", + blank=True, + ), ), migrations.AddField( - model_name='testdetail', - name='re_eval_test_case_number', - field=models.CharField(default=b'', help_text=b'Adds previous test case reference to description in report.', max_length=25, verbose_name=b'Re-Evaluate Test Case #', blank=True), + model_name="testdetail", + name="re_eval_test_case_number", + field=models.CharField( + default=b"", + help_text=b"Adds previous test case reference to description in report.", + max_length=25, + verbose_name=b"Re-Evaluate Test Case #", + blank=True, + ), ), ] diff --git a/missions/migrations/0004_testdetail_supporting_data_sort_order.py b/missions/migrations/0004_testdetail_supporting_data_sort_order.py index de5205a..7abc0fa 100644 --- a/missions/migrations/0004_testdetail_supporting_data_sort_order.py +++ b/missions/migrations/0004_testdetail_supporting_data_sort_order.py @@ -21,15 +21,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('missions', '0003_auto_event-id'), + ("missions", "0003_auto_event-id"), ] operations = [ migrations.AddField( - model_name='testdetail', - name='supporting_data_sort_order', - field=models.TextField(blank=True, default=b'[]'), + model_name="testdetail", + name="supporting_data_sort_order", + field=models.TextField(blank=True, default=b"[]"), ), ] diff --git a/missions/migrations/0005_auto_20230708_1306.py b/missions/migrations/0005_auto_20230708_1306.py index a63b9e6..3b9cc14 100644 --- a/missions/migrations/0005_auto_20230708_1306.py +++ b/missions/migrations/0005_auto_20230708_1306.py @@ -23,365 +23,579 @@ class Migration(migrations.Migration): - dependencies = [ - ('missions', '0004_testdetail_supporting_data_sort_order'), + ("missions", "0004_testdetail_supporting_data_sort_order"), ] operations = [ migrations.AlterField( - model_name='classificationlegend', - name='report_label_color_selection', - field=models.CharField(choices=[('T', 'Text Color'), ('B', 'Back Color')], default='B', max_length=1), - ), - migrations.AlterField( - model_name='classificationlegend', - name='short_legend', - field=models.CharField(default='', max_length=100), - ), - migrations.AlterField( - model_name='classificationlegend', - name='verbose_legend', - field=models.CharField(default='', max_length=200), - ), - migrations.AlterField( - model_name='color', - name='display_text', - field=models.CharField(default='', max_length=30), - ), - migrations.AlterField( - model_name='color', - name='hex_color_code', - field=models.CharField(default='', max_length=6), - ), - migrations.AlterField( - model_name='dartdynamicsettings', - name='host_output_format', - field=models.CharField(default='{ip} ({name})', help_text='Use "{ip}" and "{name}" to specify how you want hosts to be displayed.', max_length=50, validators=[missions.extras.validators.validate_host_format_string]), - ), - migrations.AlterField( - model_name='host', - name='host_name', - field=models.CharField(blank=True, default='', max_length=100), - ), - migrations.AlterField( - model_name='mission', - name='assumptions_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Assumptions in report?'), - ), - migrations.AlterField( - model_name='mission', - name='attack_phase_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Attack Phases in report?'), - ), - migrations.AlterField( - model_name='mission', - name='attack_side_effects_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Attack Side Effects in report?'), - ), - migrations.AlterField( - model_name='mission', - name='attack_time_date_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Attack Times in report?'), - ), - migrations.AlterField( - model_name='mission', - name='attack_type_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Attack Types in report?'), - ), - migrations.AlterField( - model_name='mission', - name='business_area', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='missions.businessarea', verbose_name='Business Area'), - ), - migrations.AlterField( - model_name='mission', - name='command_syntax_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Command Syntaxes in report?'), - ), - migrations.AlterField( - model_name='mission', - name='conclusion', - field=models.TextField(blank=True, default=missions.models.conclusion_default, verbose_name='Conclusion'), - ), - migrations.AlterField( - model_name='mission', - name='customer_notes_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include customer notes section in report?'), - ), - migrations.AlterField( - model_name='mission', - name='executive_summary', - field=models.TextField(blank=True, default=missions.models.executive_summary_default, verbose_name='Executive Summary'), - ), - migrations.AlterField( - model_name='mission', - name='findings_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Findings in report?'), - ), - migrations.AlterField( - model_name='mission', - name='introduction', - field=models.TextField(blank=True, default=missions.models.introduction_default, verbose_name='Introduction'), - ), - migrations.AlterField( - model_name='mission', - name='mission_name', - field=models.CharField(max_length=255, verbose_name='Mission Name'), - ), - migrations.AlterField( - model_name='mission', - name='mission_number', - field=models.CharField(max_length=5, verbose_name='Mission Number'), - ), - migrations.AlterField( - model_name='mission', - name='mitigation_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Mitigations in report?'), - ), - migrations.AlterField( - model_name='mission', - name='objectives', - field=models.TextField(blank=True, default=missions.models.objectives_default, verbose_name='Objectives'), - ), - migrations.AlterField( - model_name='mission', - name='scope', - field=models.TextField(blank=True, default=missions.models.scope_default, verbose_name='Scope'), - ), - migrations.AlterField( - model_name='mission', - name='sources_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Attack Sources in report?'), - ), - migrations.AlterField( - model_name='mission', - name='supporting_data_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Supporting Data Information in report?'), - ), - migrations.AlterField( - model_name='mission', - name='targets_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Attack Targets in report?'), - ), - migrations.AlterField( - model_name='mission', - name='technical_assessment_overview', - field=models.TextField(blank=True, default=missions.models.technical_assessment_overview_default, verbose_name='Technical Assessment / Attack Architecture Overview'), - ), - migrations.AlterField( - model_name='mission', - name='test_case_identifier', - field=models.CharField(blank=True, default='', max_length=20, verbose_name='Test Case Identifier'), - ), - migrations.AlterField( - model_name='mission', - name='test_description_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Test Descriptions in report?'), - ), - migrations.AlterField( - model_name='mission', - name='test_result_observation_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Test Details in report?'), + model_name="classificationlegend", + name="report_label_color_selection", + field=models.CharField( + choices=[("T", "Text Color"), ("B", "Back Color")], + default="B", + max_length=1, + ), ), migrations.AlterField( - model_name='mission', - name='testdetail_sort_order', - field=models.TextField(blank=True, default='[]'), + model_name="classificationlegend", + name="short_legend", + field=models.CharField(default="", max_length=100), ), migrations.AlterField( - model_name='mission', - name='tools_used_include_flag', - field=models.BooleanField(default=True, verbose_name='Mission Option: Include Tools Used in report?'), + model_name="classificationlegend", + name="verbose_legend", + field=models.CharField(default="", max_length=200), ), migrations.AlterField( - model_name='supportingdata', - name='caption', - field=models.TextField(blank=True, verbose_name='Caption'), + model_name="color", + name="display_text", + field=models.CharField(default="", max_length=30), ), migrations.AlterField( - model_name='supportingdata', - name='include_flag', - field=models.BooleanField(default=True, verbose_name='Include attachment in report'), + model_name="color", + name="hex_color_code", + field=models.CharField(default="", max_length=6), ), migrations.AlterField( - model_name='supportingdata', - name='test_detail', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='missions.testdetail', verbose_name='Test Details'), + model_name="dartdynamicsettings", + name="host_output_format", + field=models.CharField( + default="{ip} ({name})", + help_text='Use "{ip}" and "{name}" to specify how you want hosts to be displayed.', + max_length=50, + validators=[missions.extras.validators.validate_host_format_string], + ), ), migrations.AlterField( - model_name='supportingdata', - name='test_file', - field=models.FileField(upload_to='', verbose_name='Supporting Data'), + model_name="host", + name="host_name", + field=models.CharField(blank=True, default="", max_length=100), ), migrations.AlterField( - model_name='testdetail', - name='assumptions', - field=models.TextField(blank=True, help_text='Example: Attacker has a presence in xyz segment, etc.', verbose_name='Assumptions'), + model_name="mission", + name="assumptions_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Assumptions in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='assumptions_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Assumptions in report?'), + model_name="mission", + name="attack_phase_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Attack Phases in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='attack_phase', - field=models.CharField(choices=[('RECON', 'Reconnaissance'), ('WEP', 'Weaponization'), ('DEL', 'Delivery'), ('EXP', 'Exploitation'), ('INS', 'Installation'), ('C2', 'Command & Control'), ('AOO', 'Actions on Objectives')], max_length=20, verbose_name='Attack Phase'), + model_name="mission", + name="attack_side_effects_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Attack Side Effects in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='attack_phase_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Attack Phase in report?'), + model_name="mission", + name="attack_time_date_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Attack Times in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='attack_side_effects', - field=models.TextField(blank=True, help_text='List any observed side effects, if any. Example: Firewall X froze and subsequently crashed.', verbose_name='Attack Side Effects'), + model_name="mission", + name="attack_type_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Attack Types in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='attack_side_effects_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Attack Side Effects in report?'), + model_name="mission", + name="business_area", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="missions.businessarea", + verbose_name="Business Area", + ), ), migrations.AlterField( - model_name='testdetail', - name='attack_time_date', - field=models.DateTimeField(blank=True, default=django.utils.timezone.now, help_text='Date/time attack was launched', verbose_name='Attack Date / Time'), + model_name="mission", + name="command_syntax_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Command Syntaxes in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='attack_time_date_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Attack Time in report?'), + model_name="mission", + name="conclusion", + field=models.TextField( + blank=True, + default=missions.models.conclusion_default, + verbose_name="Conclusion", + ), ), migrations.AlterField( - model_name='testdetail', - name='attack_type', - field=models.CharField(blank=True, help_text='Example: SYN flood, UDP flood, malformed packets, web, fuzz, etc.', max_length=255, verbose_name='Attack Type'), + model_name="mission", + name="customer_notes_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include customer notes section in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='attack_type_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Attack Type in report?'), + model_name="mission", + name="executive_summary", + field=models.TextField( + blank=True, + default=missions.models.executive_summary_default, + verbose_name="Executive Summary", + ), ), migrations.AlterField( - model_name='testdetail', - name='command_syntax', - field=models.TextField(blank=True, help_text='Include sample command/syntax used if possible. If a script was used/created, what commands will run it?', verbose_name='Command/Syntax'), + model_name="mission", + name="findings_include_flag", + field=models.BooleanField( + default=True, verbose_name="Mission Option: Include Findings in report?" + ), ), migrations.AlterField( - model_name='testdetail', - name='command_syntax_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Command Syntax in report?'), + model_name="mission", + name="introduction", + field=models.TextField( + blank=True, + default=missions.models.introduction_default, + verbose_name="Introduction", + ), ), migrations.AlterField( - model_name='testdetail', - name='enclave', - field=models.CharField(blank=True, max_length=100, verbose_name='Enclave Test Executed From'), + model_name="mission", + name="mission_name", + field=models.CharField(max_length=255, verbose_name="Mission Name"), ), migrations.AlterField( - model_name='testdetail', - name='execution_status', - field=models.CharField(choices=[('N', 'Not Run'), ('R', 'Run'), ('C', 'Cancelled'), ('NA', 'N/A')], default='N', help_text='Execution status of the test case.', max_length=2, verbose_name='Execution Status'), + model_name="mission", + name="mission_number", + field=models.CharField(max_length=5, verbose_name="Mission Number"), ), migrations.AlterField( - model_name='testdetail', - name='findings', - field=models.TextField(blank=True, help_text='Document the specific finding related to this test or against the main test objectives if applicable', verbose_name='Findings'), + model_name="mission", + name="mitigation_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Mitigations in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='findings_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Findings in report?'), + model_name="mission", + name="objectives", + field=models.TextField( + blank=True, + default=missions.models.objectives_default, + verbose_name="Objectives", + ), ), migrations.AlterField( - model_name='testdetail', - name='mission', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='missions.mission', verbose_name='Mission'), + model_name="mission", + name="scope", + field=models.TextField( + blank=True, default=missions.models.scope_default, verbose_name="Scope" + ), ), migrations.AlterField( - model_name='testdetail', - name='mitigation', - field=models.TextField(blank=True, help_text='Example: Configure xyz, implement xyz, etc.', verbose_name='Mitigation'), + model_name="mission", + name="sources_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Attack Sources in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='mitigation_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Mitigations in report?'), + model_name="mission", + name="supporting_data_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Supporting Data Information in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='point_of_contact', - field=models.CharField(blank=True, default='', help_text='Individual working or most familiar with this test case.', max_length=20, verbose_name='POC'), + model_name="mission", + name="targets_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Attack Targets in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='re_eval_test_case_number', - field=models.CharField(blank=True, default='', help_text='Adds previous test case reference to description in report.', max_length=25, verbose_name='Re-Evaluate Test Case #'), + model_name="mission", + name="technical_assessment_overview", + field=models.TextField( + blank=True, + default=missions.models.technical_assessment_overview_default, + verbose_name="Technical Assessment / Attack Architecture Overview", + ), ), migrations.AlterField( - model_name='testdetail', - name='sources_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Attack Sources in report?'), + model_name="mission", + name="test_case_identifier", + field=models.CharField( + blank=True, + default="", + max_length=20, + verbose_name="Test Case Identifier", + ), ), migrations.AlterField( - model_name='testdetail', - name='supporting_data_sort_order', - field=models.TextField(blank=True, default='[]'), + model_name="mission", + name="test_description_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Test Descriptions in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='targets_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Attack Targets in report?'), + model_name="mission", + name="test_result_observation_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Test Details in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='test_case_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Test Case in report?'), + model_name="mission", + name="testdetail_sort_order", + field=models.TextField(blank=True, default="[]"), ), migrations.AlterField( - model_name='testdetail', - name='test_case_status', - field=models.CharField(choices=[('NEW', 'Not started'), ('IN_WORK', 'In work'), ('REVIEW', 'Ready for review'), ('FINAL', 'Approved / Final')], default='NEW', max_length=100, verbose_name='Test Case Status'), + model_name="mission", + name="tools_used_include_flag", + field=models.BooleanField( + default=True, + verbose_name="Mission Option: Include Tools Used in report?", + ), ), migrations.AlterField( - model_name='testdetail', - name='test_description', - field=models.TextField(blank=True, help_text='Describe test case to be performed, what is the objective of this test case (Is it to deny, disrupt, penetrate, modify etc.)', verbose_name='Description'), + model_name="supportingdata", + name="caption", + field=models.TextField(blank=True, verbose_name="Caption"), + ), + migrations.AlterField( + model_name="supportingdata", + name="include_flag", + field=models.BooleanField( + default=True, verbose_name="Include attachment in report" + ), ), - migrations.AlterField( - model_name='testdetail', - name='test_description_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Test Description in report?'), + migrations.AlterField( + model_name="supportingdata", + name="test_detail", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="missions.testdetail", + verbose_name="Test Details", + ), ), - migrations.AlterField( - model_name='testdetail', - name='test_number', - field=models.IntegerField(default=0, verbose_name='Test Number'), + migrations.AlterField( + model_name="supportingdata", + name="test_file", + field=models.FileField(upload_to="", verbose_name="Supporting Data"), ), migrations.AlterField( - model_name='testdetail', - name='test_objective', - field=models.CharField(help_text='Brief objective (ex.Port Scan against xyz)', max_length=255, verbose_name='Test Objective / Title'), + model_name="testdetail", + name="assumptions", + field=models.TextField( + blank=True, + help_text="Example: Attacker has a presence in xyz segment, etc.", + verbose_name="Assumptions", + ), ), migrations.AlterField( - model_name='testdetail', - name='test_result_observation', - field=models.TextField(blank=True, help_text='Example: An average of 8756 SYN-ACK/sec were received at the attack laptop over the 30 second attack period. Plots of traffic flows showed degradation of all flow performance during time 5s – 40s after which they recovered.', verbose_name='Test Result Details'), + model_name="testdetail", + name="assumptions_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Assumptions in report?" + ), ), migrations.AlterField( - model_name='testdetail', - name='test_result_observation_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Test Result in report?'), - ), + model_name="testdetail", + name="attack_phase", + field=models.CharField( + choices=[ + ("RECON", "Reconnaissance"), + ("WEP", "Weaponization"), + ("DEL", "Delivery"), + ("EXP", "Exploitation"), + ("INS", "Installation"), + ("C2", "Command & Control"), + ("AOO", "Actions on Objectives"), + ], + max_length=20, + verbose_name="Attack Phase", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="attack_phase_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Attack Phase in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="attack_side_effects", + field=models.TextField( + blank=True, + help_text="List any observed side effects, if any. Example: Firewall X froze and subsequently crashed.", + verbose_name="Attack Side Effects", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="attack_side_effects_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Attack Side Effects in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="attack_time_date", + field=models.DateTimeField( + blank=True, + default=django.utils.timezone.now, + help_text="Date/time attack was launched", + verbose_name="Attack Date / Time", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="attack_time_date_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Attack Time in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="attack_type", + field=models.CharField( + blank=True, + help_text="Example: SYN flood, UDP flood, malformed packets, web, fuzz, etc.", + max_length=255, + verbose_name="Attack Type", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="attack_type_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Attack Type in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="command_syntax", + field=models.TextField( + blank=True, + help_text="Include sample command/syntax used if possible. If a script was used/created, what commands will run it?", + verbose_name="Command/Syntax", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="command_syntax_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Command Syntax in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="enclave", + field=models.CharField( + blank=True, max_length=100, verbose_name="Enclave Test Executed From" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="execution_status", + field=models.CharField( + choices=[ + ("N", "Not Run"), + ("R", "Run"), + ("C", "Cancelled"), + ("NA", "N/A"), + ], + default="N", + help_text="Execution status of the test case.", + max_length=2, + verbose_name="Execution Status", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="findings", + field=models.TextField( + blank=True, + help_text="Document the specific finding related to this test or against the main test objectives if applicable", + verbose_name="Findings", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="findings_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Findings in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="mission", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="missions.mission", + verbose_name="Mission", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="mitigation", + field=models.TextField( + blank=True, + help_text="Example: Configure xyz, implement xyz, etc.", + verbose_name="Mitigation", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="mitigation_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Mitigations in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="point_of_contact", + field=models.CharField( + blank=True, + default="", + help_text="Individual working or most familiar with this test case.", + max_length=20, + verbose_name="POC", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="re_eval_test_case_number", + field=models.CharField( + blank=True, + default="", + help_text="Adds previous test case reference to description in report.", + max_length=25, + verbose_name="Re-Evaluate Test Case #", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="sources_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Attack Sources in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="supporting_data_sort_order", + field=models.TextField(blank=True, default="[]"), + ), + migrations.AlterField( + model_name="testdetail", + name="targets_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Attack Targets in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="test_case_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Test Case in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="test_case_status", + field=models.CharField( + choices=[ + ("NEW", "Not started"), + ("IN_WORK", "In work"), + ("REVIEW", "Ready for review"), + ("FINAL", "Approved / Final"), + ], + default="NEW", + max_length=100, + verbose_name="Test Case Status", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="test_description", + field=models.TextField( + blank=True, + help_text="Describe test case to be performed, what is the objective of this test case (Is it to deny, disrupt, penetrate, modify etc.)", + verbose_name="Description", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="test_description_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Test Description in report?" + ), + ), + migrations.AlterField( + model_name="testdetail", + name="test_number", + field=models.IntegerField(default=0, verbose_name="Test Number"), + ), + migrations.AlterField( + model_name="testdetail", + name="test_objective", + field=models.CharField( + help_text="Brief objective (ex.Port Scan against xyz)", + max_length=255, + verbose_name="Test Objective / Title", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="test_result_observation", + field=models.TextField( + blank=True, + help_text="Example: An average of 8756 SYN-ACK/sec were received at the attack laptop over the 30 second attack period. Plots of traffic flows showed degradation of all flow performance during time 5s – 40s after which they recovered.", + verbose_name="Test Result Details", + ), + ), + migrations.AlterField( + model_name="testdetail", + name="test_result_observation_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Test Result in report?" + ), + ), migrations.AlterField( - model_name='testdetail', - name='tools_used', - field=models.TextField(blank=True, help_text='Example: Burp Suite, Wireshark, NMap, etc.', verbose_name='Tools Used'), + model_name="testdetail", + name="tools_used", + field=models.TextField( + blank=True, + help_text="Example: Burp Suite, Wireshark, NMap, etc.", + verbose_name="Tools Used", + ), ), migrations.AlterField( - model_name='testdetail', - name='tools_used_include_flag', - field=models.BooleanField(default=True, verbose_name='Include Tools Used in report?'), + model_name="testdetail", + name="tools_used_include_flag", + field=models.BooleanField( + default=True, verbose_name="Include Tools Used in report?" + ), ), ] diff --git a/missions/models.py b/missions/models.py index 615ad88..cbf32d5 100644 --- a/missions/models.py +++ b/missions/models.py @@ -34,27 +34,27 @@ def introduction_default(): - return Mission.get_default_text('introduction.txt') + return Mission.get_default_text("introduction.txt") def executive_summary_default(): - return Mission.get_default_text('executive_summary.txt') + return Mission.get_default_text("executive_summary.txt") def scope_default(): - return Mission.get_default_text('scope.txt') + return Mission.get_default_text("scope.txt") def objectives_default(): - return Mission.get_default_text('objectives.txt') + return Mission.get_default_text("objectives.txt") def technical_assessment_overview_default(): - return Mission.get_default_text('technical_assessment.txt') + return Mission.get_default_text("technical_assessment.txt") def conclusion_default(): - return Mission.get_default_text('conclusion.txt') + return Mission.get_default_text("conclusion.txt") """ @@ -63,11 +63,10 @@ def conclusion_default(): class Mission(models.Model): - @staticmethod def get_default_text(file_name): - TEMPALTE_DIR = os.path.join(settings.BASE_DIR, 'templates') - with open(os.path.join(TEMPALTE_DIR, file_name), 'r') as template: + TEMPALTE_DIR = os.path.join(settings.BASE_DIR, "templates") + with open(os.path.join(TEMPALTE_DIR, file_name), "r") as template: output = join_as_compacted_paragraphs(template.readlines()) return output @@ -91,7 +90,7 @@ def get_default_text(file_name): ) business_area = models.ForeignKey( - 'BusinessArea', + "BusinessArea", verbose_name="Business Area", on_delete=models.CASCADE, ) @@ -103,33 +102,23 @@ def get_default_text(file_name): ) executive_summary = models.TextField( - blank=True, - verbose_name="Executive Summary", - default=executive_summary_default + blank=True, verbose_name="Executive Summary", default=executive_summary_default ) - scope = models.TextField( - blank=True, - verbose_name="Scope", - default=scope_default - ) + scope = models.TextField(blank=True, verbose_name="Scope", default=scope_default) objectives = models.TextField( - blank=True, - verbose_name="Objectives", - default=objectives_default + blank=True, verbose_name="Objectives", default=objectives_default ) technical_assessment_overview = models.TextField( blank=True, verbose_name="Technical Assessment / Attack Architecture Overview", - default=technical_assessment_overview_default + default=technical_assessment_overview_default, ) conclusion = models.TextField( - blank=True, - verbose_name="Conclusion", - default=conclusion_default + blank=True, verbose_name="Conclusion", default=conclusion_default ) # Mission-wide reporting include flags @@ -200,18 +189,15 @@ def get_default_text(file_name): supporting_data_include_flag = models.BooleanField( default=True, - verbose_name="Mission Option: Include Supporting Data Information in report?" + verbose_name="Mission Option: Include Supporting Data Information in report?", ) customer_notes_include_flag = models.BooleanField( default=True, - verbose_name="Mission Option: Include customer notes section in report?" + verbose_name="Mission Option: Include customer notes section in report?", ) - testdetail_sort_order = models.TextField( - blank=True, - default="[]" - ) + testdetail_sort_order = models.TextField(blank=True, default="[]") def __str__(self): return "%s (%s)" % (self.mission_name, self.mission_number) @@ -240,14 +226,18 @@ class Host(models.Model): @staticmethod def get_host_output_format_string(): - format_string = str(DARTDynamicSettings.objects.get_as_object().host_output_format) + format_string = str( + DARTDynamicSettings.objects.get_as_object().host_output_format + ) return format_string def __str__(self): - format_string = cache.get('host_output_format_string') + format_string = cache.get("host_output_format_string") if format_string is None: - cache.set('host_output_format_string', Host.get_host_output_format_string(), 300) - format_string = cache.get('host_output_format_string') + cache.set( + "host_output_format_string", Host.get_host_output_format_string(), 300 + ) + format_string = cache.get("host_output_format_string") return format_string.format(name=self.host_name, ip=self.ip_address) def get_absolute_url(self): @@ -255,37 +245,36 @@ def get_absolute_url(self): class TestDetail(models.Model): - mission = models.ForeignKey(Mission, verbose_name="Mission", on_delete=models.CASCADE,) + mission = models.ForeignKey( + Mission, + verbose_name="Mission", + on_delete=models.CASCADE, + ) test_number = models.IntegerField( - blank=False, - verbose_name="Test Number", - default=0 + blank=False, verbose_name="Test Number", default=0 ) test_case_include_flag = models.BooleanField( - default=True, - verbose_name="Include Test Case in report?" + default=True, verbose_name="Include Test Case in report?" ) TEST_CASE_STATUSES = ( - ('NEW', 'Not started'), - ('IN_WORK', 'In work'), - ('REVIEW', 'Ready for review'), - ('FINAL', 'Approved / Final'), + ("NEW", "Not started"), + ("IN_WORK", "In work"), + ("REVIEW", "Ready for review"), + ("FINAL", "Approved / Final"), ) test_case_status = models.CharField( choices=TEST_CASE_STATUSES, max_length=100, verbose_name="Test Case Status", - default='NEW', + default="NEW", ) enclave = models.CharField( - blank=True, - max_length=100, - verbose_name='Enclave Test Executed From' + blank=True, max_length=100, verbose_name="Enclave Test Executed From" ) test_objective = models.CharField( @@ -297,13 +286,13 @@ class TestDetail(models.Model): # Note: Attack Phases based on the Lockheed Martin Kill Chain(R) ATTACK_PHASES = ( - ('RECON', 'Reconnaissance'), - ('WEP', 'Weaponization'), - ('DEL', 'Delivery'), - ('EXP', 'Exploitation'), - ('INS', 'Installation'), - ('C2', 'Command & Control'), - ('AOO', 'Actions on Objectives'), + ("RECON", "Reconnaissance"), + ("WEP", "Weaponization"), + ("DEL", "Delivery"), + ("EXP", "Exploitation"), + ("INS", "Installation"), + ("C2", "Command & Control"), + ("AOO", "Actions on Objectives"), ) attack_phase = models.CharField( @@ -332,7 +321,7 @@ class TestDetail(models.Model): assumptions = models.TextField( blank=True, verbose_name="Assumptions", - help_text="Example: Attacker has a presence in xyz segment, etc." + help_text="Example: Attacker has a presence in xyz segment, etc.", ) assumptions_include_flag = models.BooleanField( @@ -343,7 +332,7 @@ class TestDetail(models.Model): test_description = models.TextField( blank=True, verbose_name="Description", - help_text="Describe test case to be performed, what is the objective of this test case (Is it to deny, disrupt, penetrate, modify etc.)" + help_text="Describe test case to be performed, what is the objective of this test case (Is it to deny, disrupt, penetrate, modify etc.)", ) test_description_include_flag = models.BooleanField( @@ -352,8 +341,8 @@ class TestDetail(models.Model): ) source_hosts = models.ManyToManyField( - 'Host', - related_name='source_set', + "Host", + related_name="source_set", ) sources_include_flag = models.BooleanField( @@ -362,8 +351,8 @@ class TestDetail(models.Model): ) target_hosts = models.ManyToManyField( - 'Host', - related_name='target_set', + "Host", + related_name="target_set", ) targets_include_flag = models.BooleanField( @@ -375,7 +364,7 @@ class TestDetail(models.Model): blank=True, default=timezone.now, verbose_name="Attack Date / Time", - help_text="Date/time attack was launched" + help_text="Date/time attack was launched", ) attack_time_date_include_flag = models.BooleanField( @@ -386,7 +375,7 @@ class TestDetail(models.Model): tools_used = models.TextField( blank=True, verbose_name="Tools Used", - help_text="Example: Burp Suite, Wireshark, NMap, etc." + help_text="Example: Burp Suite, Wireshark, NMap, etc.", ) tools_used_include_flag = models.BooleanField( @@ -397,7 +386,7 @@ class TestDetail(models.Model): command_syntax = models.TextField( blank=True, verbose_name="Command/Syntax", - help_text="Include sample command/syntax used if possible. If a script was used/created, what commands will run it?" + help_text="Include sample command/syntax used if possible. If a script was used/created, what commands will run it?", ) command_syntax_include_flag = models.BooleanField( @@ -409,8 +398,8 @@ class TestDetail(models.Model): blank=True, verbose_name="Test Result Details", help_text="Example: An average of 8756 SYN-ACK/sec were received at the attack laptop over the 30 second attack " - "period. Plots of traffic flows showed degradation of all flow performance during time 5s – 40s " - "after which they recovered." + "period. Plots of traffic flows showed degradation of all flow performance during time 5s – 40s " + "after which they recovered.", ) test_result_observation_include_flag = models.BooleanField( @@ -421,7 +410,7 @@ class TestDetail(models.Model): attack_side_effects = models.TextField( blank=True, verbose_name="Attack Side Effects", - help_text="List any observed side effects, if any. Example: Firewall X froze and subsequently crashed." + help_text="List any observed side effects, if any. Example: Firewall X froze and subsequently crashed.", ) attack_side_effects_include_flag = models.BooleanField( @@ -430,18 +419,18 @@ class TestDetail(models.Model): ) EXECUTION_STATUS_OPTIONS = ( - ('N', 'Not Run'), - ('R', 'Run'), - ('C', 'Cancelled'), - ('NA', 'N/A'), + ("N", "Not Run"), + ("R", "Run"), + ("C", "Cancelled"), + ("NA", "N/A"), ) execution_status = models.CharField( choices=EXECUTION_STATUS_OPTIONS, max_length=2, verbose_name="Execution Status", - default='N', - help_text="Execution status of the test case." + default="N", + help_text="Execution status of the test case.", ) has_findings = models.BooleanField( @@ -451,7 +440,7 @@ class TestDetail(models.Model): findings = models.TextField( blank=True, verbose_name="Findings", - help_text="Document the specific finding related to this test or against the main test objectives if applicable" + help_text="Document the specific finding related to this test or against the main test objectives if applicable", ) findings_include_flag = models.BooleanField( @@ -462,7 +451,7 @@ class TestDetail(models.Model): mitigation = models.TextField( blank=True, verbose_name="Mitigation", - help_text="Example: Configure xyz, implement xyz, etc." + help_text="Example: Configure xyz, implement xyz, etc.", ) mitigation_include_flag = models.BooleanField( @@ -475,7 +464,7 @@ class TestDetail(models.Model): blank=True, default="", verbose_name="POC", - help_text="Individual working or most familiar with this test case." + help_text="Individual working or most familiar with this test case.", ) re_eval_test_case_number = models.CharField( @@ -483,13 +472,10 @@ class TestDetail(models.Model): blank=True, default="", verbose_name="Re-Evaluate Test Case #", - help_text="Adds previous test case reference to description in report." + help_text="Adds previous test case reference to description in report.", ) - supporting_data_sort_order = models.TextField( - blank=True, - default="[]" - ) + supporting_data_sort_order = models.TextField(blank=True, default="[]") def count_of_supporting_data(self): return len(SupportingData.objects.filter(test_detail=self.pk)) @@ -503,7 +489,11 @@ def save(self, *args, **kwargs): class SupportingData(models.Model): - test_detail = models.ForeignKey(TestDetail, verbose_name="Test Details",on_delete=models.CASCADE,) + test_detail = models.ForeignKey( + TestDetail, + verbose_name="Test Details", + on_delete=models.CASCADE, + ) caption = models.TextField( blank=True, @@ -511,19 +501,16 @@ class SupportingData(models.Model): ) include_flag = models.BooleanField( - default=True, - verbose_name="Include attachment in report" + default=True, verbose_name="Include attachment in report" ) - test_file = models.FileField( - verbose_name="Supporting Data" - ) + test_file = models.FileField(verbose_name="Supporting Data") def filename(self): return os.path.basename(self.test_file.name) def get_absolute_url(self): - return reverse_lazy('data-view', {'supportingdata': self.pk}) + return reverse_lazy("data-view", {"supportingdata": self.pk}) class BusinessArea(models.Model): @@ -547,7 +534,7 @@ class Color(models.Model): ) def __str__(self): - return '{0.display_text}'.format(self) + return "{0.display_text}".format(self) class ClassificationLegend(models.Model): @@ -564,37 +551,37 @@ class ClassificationLegend(models.Model): ) text_color = models.ForeignKey( - 'Color', - related_name='classificationlegend_text_set', + "Color", + related_name="classificationlegend_text_set", on_delete=models.CASCADE, ) background_color = models.ForeignKey( - 'Color', - related_name='classificationlegend_background_set', + "Color", + related_name="classificationlegend_background_set", on_delete=models.CASCADE, ) REPORT_LABEL_COLOR_OPTIONS = ( - ('T', 'Text Color'), - ('B', 'Back Color'), + ("T", "Text Color"), + ("B", "Back Color"), ) report_label_color_selection = models.CharField( choices=REPORT_LABEL_COLOR_OPTIONS, max_length=1, - default='B', + default="B", blank=False, ) def get_report_label_color(self): - if self.report_label_color_selection == 'T': + if self.report_label_color_selection == "T": return self.text_color else: return self.background_color def __str__(self): - return '{0.verbose_legend} ({0.short_legend})'.format(self) + return "{0.verbose_legend} ({0.short_legend})".format(self) class DARTDynamicSettingsManager(models.Manager): @@ -614,15 +601,15 @@ def get_as_object(self): class DARTDynamicSettings(models.Model): system_classification = models.ForeignKey( - 'ClassificationLegend', + "ClassificationLegend", on_delete=models.CASCADE, ) host_output_format = models.CharField( max_length=50, - default='{ip} ({name})', + default="{ip} ({name})", help_text='Use "{ip}" and "{name}" to specify how you want hosts to be displayed.', - validators=[validate_host_format_string] + validators=[validate_host_format_string], ) # Since we're treating Dynamic Settings as a singleton, diff --git a/missions/templates/about.html b/missions/templates/about.html index cb4c5fe..07cd4f3 100644 --- a/missions/templates/about.html +++ b/missions/templates/about.html @@ -18,29 +18,20 @@ --> {% endcomment %} {% load bootstrap3 %} - -{% block title %} - About DART -{% endblock %} - -{% block main_heading %} -

- About DART -

-{% endblock %} - +{% block title %}About DART{% endblock %} +{% block main_heading %}

About DART

{% endblock %} {% block content %} -

What is it?

-

DART is a tool created by the Lockheed Martin Red Team to document and report results from penetration tests, particularly in isolated network environments.

- -

Who created it?

-

- Everyone on the Lockheed Martin Red Team has contributed to this tool - through development, ideas, and "hey wouldn't it be cool if..." feature requests. - It has been tailored to support the Red Team testing best practices created at Lockheed Martin. -

- -

Where can I get it?

-

- You can download DART on GitHub -

+

What is it?

+

+ DART is a tool created by the Lockheed Martin Red Team to document and report results from penetration tests, particularly in isolated network environments. +

+

Who created it?

+

+ Everyone on the Lockheed Martin Red Team has contributed to this tool - through development, ideas, and "hey wouldn't it be cool if..." feature requests. + It has been tailored to support the Red Team testing best practices created at Lockheed Martin. +

+

Where can I get it?

+

+ You can download DART on GitHub +

{% endblock %} diff --git a/missions/templates/business_area_list.html b/missions/templates/business_area_list.html index a64e286..e2094e0 100644 --- a/missions/templates/business_area_list.html +++ b/missions/templates/business_area_list.html @@ -19,39 +19,44 @@ {% endcomment %} {% load bootstrap3 %} {% block title %}Business Area Update{% endblock %} - {% block extra_nav_bar_content_right %} -
  • +
  • System Settings -
  • + {% endblock %} - {% block main_heading %} -

    Business Area Update

    +

    + Business Area Update + +

    {% endblock %} {% block content %} - -
     
    - - - - - - - {% for ba in object_list %} +
     
    +
    NameActions
    - - + + - {% endfor %} -
    {{ ba.name }} - {% bootstrap_icon "pencil" %}  - {% bootstrap_icon "trash" %}
    -
    NameActions
    - - - + {% endblock %} diff --git a/missions/templates/classification_list.html b/missions/templates/classification_list.html index cbdfa9a..b85694a 100644 --- a/missions/templates/classification_list.html +++ b/missions/templates/classification_list.html @@ -19,87 +19,106 @@ {% endcomment %} {% load bootstrap3 %} {% block title %}Classification Legend Update{% endblock %} - {% block extra_nav_bar_content_right %} -
  • +
  • System Settings -
  • + {% endblock %} - {% block main_heading %} -

    Classification Legend Update

    +

    + Classification Legend Update +
    + +
    +

    {% endblock %} {% block content %} - -
     
    - - - - - - - - - - - {% for class in object_list %} - - - - - + + + + + + + {% endfor %} +
    Verbose LegendShort LegendText ColorBackground ColorReport Label ColorActions
    - {{ class.verbose_legend }} - - - {{ class.short_legend }} - - - {{ class.text_color }} - {# Autocomplete attribute required for FF compatibility 8-( #} - - - {{ class.background_color }} - + + + + + + + + + {% for class in object_list %} + + + + + + - - - - {% endfor %} -
    Verbose LegendShort LegendText ColorBackground ColorReport Label ColorActions
    + {{ class.verbose_legend }} + + + {{ class.short_legend }} + + + {{ class.text_color }} + {# Autocomplete attribute required for FF compatibility 8-( #} + + + {{ class.background_color }} + + + {{ class.get_report_label_color_selection_display }} + - - {{ class.get_report_label_color_selection_display }} - - - {% bootstrap_icon "pencil" %}  - {% bootstrap_icon "trash" %}
    -
    - - + $(function() { + $("#classification-legend-list").on("keyup", ".form-control", function(event) { + /* On enter, update the classification */ + if (event.keyCode == 13) { + updateClassification($(this).closest("tr")); + } + }); + }); + {% endblock %} diff --git a/missions/templates/color_list.html b/missions/templates/color_list.html index 58dcf9c..56f51eb 100644 --- a/missions/templates/color_list.html +++ b/missions/templates/color_list.html @@ -19,47 +19,57 @@ {% endcomment %} {% load bootstrap3 %} {% block title %}Color Options Update{% endblock %} - {% block extra_nav_bar_content_right %} -
  • +
  • System Settings -
  • + {% endblock %} - {% block main_heading %} -

    Color Options Update

    +

    + Color Options Update +
    + +
    +

    {% endblock %} {% block content %} - -
     
    - - - - - - - - {% for color in object_list %} - - - - +
     
    +
    Display Text6-Character Hex Code (RGB)Actions
    - {{ color.display_text }} - - - {{ color.hex_color_code }} - - - {% bootstrap_icon "pencil" %}  - {% bootstrap_icon "trash" %}
    -
    + + + + - {% endfor %} -
    Display Text6-Character Hex Code (RGB)Actions
    - - + $(function() { + $("#color-options-list").on("keyup", ".form-control", function(event) { + /* On enter, update the color */ + if (event.keyCode == 13) { + updateColor($(this).closest("tr")); + } + }); + }); + {% endblock %} diff --git a/missions/templates/create_account.html b/missions/templates/create_account.html index a85b244..38a6f2e 100644 --- a/missions/templates/create_account.html +++ b/missions/templates/create_account.html @@ -19,54 +19,60 @@ {% endcomment %} {% load bootstrap3 %} {% load static %} - -{% block title %} - Account Creation -{% endblock %} - +{% block title %}Account Creation{% endblock %} {% block main_heading %} -

    - {% if any_accounts_configured %} - Account Help - {% else %} - Account Creation - {% endif %} -

    +

    + {% if any_accounts_configured %} + Account Help + {% else %} + Account Creation + {% endif %} +

    {% endblock %} - {% block content %} -{% if any_accounts_configured %} -
    -
    Forgotten Credentials Assistance
    -
    -

    If you have forgotten your credentials you will be unable to access the web page until you have - removed all user accounts (which will allow you to create a new user using this page). - Don't fret, your mission data will be preserved.

    -

    Removing All User Accounts

    -
      -
    1. Execute the following command from the DART directory:
      - python manage.py removeallusers -
    2. -
    + {% if any_accounts_configured %} +
    +
    Forgotten Credentials Assistance
    +
    +

    + If you have forgotten your credentials you will be unable to access the web page until you have + removed all user accounts (which will allow you to create a new user using this page). + Don't fret, your mission data will be preserved. +

    +

    Removing All User Accounts

    +
      +
    1. + Execute the following command from the DART directory: +
      + python manage.py removeallusers +
    2. +
    +
    - - -
    -{% else %} + {% else %}
    Create an Account

    It looks like you're the first user here, go ahead and enter some credentials to get started!

    {% csrf_token %} - - +
    -{% endif %} + {% endif %} {% endblock %} diff --git a/missions/templates/delete_mission.html b/missions/templates/delete_mission.html index 833dae2..45e439e 100644 --- a/missions/templates/delete_mission.html +++ b/missions/templates/delete_mission.html @@ -18,26 +18,15 @@ --> {% endcomment %} {% load bootstrap3 %} -{% block title %} - Confirm Mission Deletion -{% endblock %} - -{% block main_heading %} -

    - Confirm Mission Deletion -

    -{% endblock %} - +{% block title %}Confirm Mission Deletion{% endblock %} +{% block main_heading %}

    Confirm Mission Deletion

    {% endblock %} {% block content %} -

    Are you sure you want to permanently delete this mission and all the hard work you put into it?

    -
    -{% csrf_token %} - -{% buttons %} - I've made a horrible mistake! Take me back! - -{% endbuttons %} +

    Are you sure you want to permanently delete this mission and all the hard work you put into it?

    + + {% csrf_token %} + {% buttons %} + I've made a horrible mistake! Take me back! + + {% endbuttons %}
    {% endblock %} diff --git a/missions/templates/delete_test.html b/missions/templates/delete_test.html index 81fea10..69b9cd0 100644 --- a/missions/templates/delete_test.html +++ b/missions/templates/delete_test.html @@ -18,27 +18,18 @@ --> {% endcomment %} {% load bootstrap3 %} -{% block title %} - Confirm Test Deletion -{% endblock %} - -{% block main_heading %} -

    - Confirm Test Deletion -

    -{% endblock %} - +{% block title %}Confirm Test Deletion{% endblock %} +{% block main_heading %}

    Confirm Test Deletion

    {% endblock %} {% block content %} -

    Are you sure you want to permanently delete this test case from this mission and all the hard work you put into it? -

    -
    -{% csrf_token %} - -{% buttons %} - I've made a horrible mistake! Take me back! - -{% endbuttons %} +

    + Are you sure you want to permanently delete this test case from this mission and all the hard work you put into it? +

    + + {% csrf_token %} + {% buttons %} + I've made a horrible mistake! Take me back! + + {% endbuttons %}
    {% endblock %} diff --git a/missions/templates/delete_test_data.html b/missions/templates/delete_test_data.html index e584905..7786197 100644 --- a/missions/templates/delete_test_data.html +++ b/missions/templates/delete_test_data.html @@ -18,36 +18,23 @@ --> {% endcomment %} {% load bootstrap3 %} -{% block title %} - Confirm Supporting Data Deletion -{% endblock %} - -{% block main_heading %} -

    - Confirm Supporting Data Deletion -

    -{% endblock %} - +{% block title %}Confirm Supporting Data Deletion{% endblock %} +{% block main_heading %}

    Confirm Supporting Data Deletion

    {% endblock %} {% block content %} -

    Are you sure you want to permanently delete this supporting data and all the hard work you put into it? -

    - - - -
    -{% csrf_token %} - -{% buttons %} - I've made a horrible mistake! Take me back! - -{% endbuttons %} +

    Are you sure you want to permanently delete this supporting data and all the hard work you put into it?

    + + + {% csrf_token %} + {% buttons %} + I've made a horrible mistake! Take me back! + + {% endbuttons %}
    {% endblock %} diff --git a/missions/templates/edit_mission.html b/missions/templates/edit_mission.html index 9836754..2ceb3a1 100644 --- a/missions/templates/edit_mission.html +++ b/missions/templates/edit_mission.html @@ -26,26 +26,23 @@ {% endif %} {% endblock %} {% block main_heading %} -

    - {% if mission.id %} - Edit Mission - {% else %} - Add Mission - {% endif %} -

    +

    + {% if mission.id %} + Edit Mission + {% else %} + Add Mission + {% endif %} +

    {% endblock %} - {% block content %} -
    -{% csrf_token %} -{% bootstrap_form form %} -{% buttons %} - - {% if mission.id %} - {% bootstrap_icon "trash" %} Delete - {% endif %} -{% endbuttons %} + + {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + {% if mission.id %} + {% bootstrap_icon "trash" %} Delete + {% endif %} + {% endbuttons %}
    {% endblock %} diff --git a/missions/templates/edit_mission_hosts.html b/missions/templates/edit_mission_hosts.html index 53097c7..6846a14 100644 --- a/missions/templates/edit_mission_hosts.html +++ b/missions/templates/edit_mission_hosts.html @@ -18,76 +18,68 @@ --> {% endcomment %} {% load bootstrap3 %} - -{% block title %} -Edit Mission Hosts -{% endblock %} - -{% block main_heading %} -

    -Edit Mission Hosts -

    -{% endblock %} - +{% block title %}Edit Mission Hosts{% endblock %} +{% block main_heading %}

    Edit Mission Hosts

    {% endblock %} {% block content %} -
     
    - -{% include 'modals/edit_missionhosts_modal.html' with mission_id=mission.pk%} - -{% csrf_token %} -
    -
    - - - - - - - - {% for host in mission.host_set.all %} - - - - - - - {% endfor %} -
    NameIP AddressNo-HitActions
    {{ host.host_name }}{{ host.ip_address }} - {% if host.is_no_hit %} - Yes - {% else %} - No - {% endif %} - - - {% bootstrap_icon "pencil" %} -     - {% bootstrap_icon "trash" %} -
    +
     
    + {% include 'modals/edit_missionhosts_modal.html' with mission_id=mission.pk %} + {% csrf_token %} +
    +
    + + + + + + + + {% for host in mission.host_set.all %} + + + + + + + {% endfor %} +
    NameIP AddressNo-HitActions
    {{ host.host_name }}{{ host.ip_address }} + {% if host.is_no_hit %} + Yes + {% else %} + No + {% endif %} + + {% bootstrap_icon "pencil" %} +     + {% bootstrap_icon "trash" %} +
    +
    -
    -
    -
    -
    +
    +
    +
    + +
    +
    -
    - - + // addRow + // hostAjaxErrorHandler + {% endblock %} diff --git a/missions/templates/edit_mission_test.html b/missions/templates/edit_mission_test.html index e3f21e9..15a3033 100644 --- a/missions/templates/edit_mission_test.html +++ b/missions/templates/edit_mission_test.html @@ -19,7 +19,6 @@ {% endcomment %} {% load bootstrap3 %} {% load quickparts %} - {% block title %} {% if this_mission and testdetail.pk > 0 %} {% if is_read_only %} @@ -31,33 +30,29 @@ Add Test {% endif %} {% endblock %} - {% if testdetail.pk > 0 %} {% block extra_nav_bar_content_right %} {% if is_read_only %}
  • -   - + +  
  • {% elif this_mission and testdetail.pk > 0 %}
  • -   - + +  
  • {% endif %} - {% if this_mission and testdetail.pk > 0 %}
  • {% manage_data_button this_mission.id testdetail.pk testdetail.count_of_supporting_data as_button=True %} @@ -65,104 +60,120 @@ {% endif %} {% endblock %} {% endif %} - -{% block main_heading %}

    - {% if this_mission and testdetail.pk > 0 %} - {% if is_read_only %} - Review Test +{% block main_heading %} +

    + {% if this_mission and testdetail.pk > 0 %} + {% if is_read_only %} + Review Test + {% else %} + Edit Test + {% endif %} + {{ testdetail.pk }} ({{ testdetail.test_objective }}) {{ object.pk }} {% else %} - Edit Test + Add Test for {{ this_mission }} {% endif %} - {{testdetail.pk}} ({{testdetail.test_objective}}) {{object.pk}} - {% else %} - Add Test for {{this_mission}} - {% endif %}

    -

    << Back to Tests

    + +

    + << Back to Tests +

    {% endblock %} - {% block content %} -{% if this_mission and testdetail.pk > 0 %} - {% include 'modals/edit_testhosts_modal.html' with mission_id=this_mission.pk test_id=testdetail.pk %} -{% endif %} - - - - - {% endblock %} diff --git a/missions/templates/edit_test_data.html b/missions/templates/edit_test_data.html index 8ab71c6..3b9989d 100644 --- a/missions/templates/edit_test_data.html +++ b/missions/templates/edit_test_data.html @@ -27,28 +27,27 @@ {% endblock %} {% block main_heading %}

    - {% if supportingdata.pk %} - Edit - {% else %} - Add - {% endif %} - Data for Test {{this_test.test_objective}}

    + {% if supportingdata.pk %} + Edit + {% else %} + Add + {% endif %} + Data for Test {{ this_test.test_objective }} +

    - << Back to Data List + << Back to Data List

    {% endblock %} - {% block content %} -
    -{% csrf_token %} -{% bootstrap_form form %} -{% buttons %} - - {% if supportingdata.pk %} - {% bootstrap_icon "trash" %} Delete - {% endif %} -{% endbuttons %} + + {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + {% if supportingdata.pk %} + {% bootstrap_icon "trash" %} Delete + {% endif %} + {% endbuttons %}
    {% endblock %} diff --git a/missions/templates/edit_testhosts_partial.html b/missions/templates/edit_testhosts_partial.html index 23d6033..cdb9bc8 100644 --- a/missions/templates/edit_testhosts_partial.html +++ b/missions/templates/edit_testhosts_partial.html @@ -17,17 +17,16 @@ --> {% endcomment %} {# Requires testdetail to be set to a test case record #} -
    -
    +
    - {% for host in testdetail.source_hosts.all %} -
    {{ host }}
    - {% endfor %} + {% for host in testdetail.source_hosts.all %}
    {{ host }}
    {% endfor %}
    @@ -35,9 +34,7 @@
    - {% for host in testdetail.target_hosts.all %} -
    {{ host }}
    - {% endfor %} + {% for host in testdetail.target_hosts.all %}
    {{ host }}
    {% endfor %}
    diff --git a/missions/templates/login.html b/missions/templates/login.html index 0a9d32e..cc9c6bb 100644 --- a/missions/templates/login.html +++ b/missions/templates/login.html @@ -22,24 +22,15 @@ {% endif %} {% endblock %} - -{% block title %} -Login -{% endblock %} - -{% block main_heading %} -

    - Welcome to DART -

    -{% endblock %} - +{% block title %}Login{% endblock %} +{% block main_heading %}

    Welcome to DART

    {% endblock %} {% block content %} -

    Login above to continue.

    -

    If you are having trouble logging in or need to create your initial system account, please - click here. -

    - - +

    Login above to continue.

    +

    + If you are having trouble logging in or need to create your initial system account, please + click here. +

    + {% endblock %} diff --git a/missions/templates/login_interstitial.html b/missions/templates/login_interstitial.html index 33e2b2f..38f01bf 100644 --- a/missions/templates/login_interstitial.html +++ b/missions/templates/login_interstitial.html @@ -19,43 +19,30 @@ {% endcomment %} {% load bootstrap3 %} {% load static %} - -{% block title %} - Application Use Agreement -{% endblock %} - -{% block main_heading %} -

    - System Access Notice -

    -{% endblock %} - +{% block title %}Application Use Agreement{% endblock %} +{% block main_heading %}

    System Access Notice

    {% endblock %} {% block content %} -
    -
    System Access Notice
    -
    - {% comment %} +
    +
    System Access Notice
    +
    + {% comment %} {# Fun fact: If you remove the surrounding comment tags, this is a great place to put your company logo. #} -

    - {% endcomment %} -

    You must accept the following items to use this application.

    -
      -
    1. Use of this application is governed by the organization granting access to the system.
    2. -
    -

    If you do not agree to any of the above items you may not use this application.

    -
    - +
    - -
    - {% endblock %} diff --git a/missions/templates/mission_list.html b/missions/templates/mission_list.html index 850d77f..55b8dcc 100644 --- a/missions/templates/mission_list.html +++ b/missions/templates/mission_list.html @@ -19,42 +19,48 @@ {% endcomment %} {% load bootstrap3 %} {% block title %}Missions{% endblock %} - {% block extra_nav_bar_content_right %} -
  • +
  • System Settings -
  • + {% endblock %} - {% block main_heading %} -

    Mission List

    +

    + Mission List + +

    {% endblock %} {% block content %} - -
     
    - - - - - - - - - - {% for m in missions %} +
     
    +
    Mission NameMission #Business AreaActions
    - - - - - + + + + + - {% endfor %} -
    {% bootstrap_icon "edit" %} Edit{{ m.mission_name }}{{ m.mission_number }}{{ m.business_area }} - {% bootstrap_icon "stats" %} Test Cases
    - {% bootstrap_icon "hdd" %} Mission Hosts
    - {% bootstrap_icon "file" %} Generate Report
    - {% bootstrap_icon "gift" %} Generate Data Package -
    Mission NameMission #Business AreaActions
    + {% for m in missions %} +
    + {% bootstrap_icon "edit" %} Edit + {{ m.mission_name }}{{ m.mission_number }}{{ m.business_area }} + {% bootstrap_icon "stats" %} Test Cases +
    + {% bootstrap_icon "hdd" %} Mission Hosts +
    + {% bootstrap_icon "file" %} Generate Report +
    + {% bootstrap_icon "gift" %} Generate Data Package +
    {% bootstrap_pagination missions size="medium" %} {% endblock %} diff --git a/missions/templates/mission_list_tests.html b/missions/templates/mission_list_tests.html index 32e5f9e..a9f4319 100644 --- a/missions/templates/mission_list_tests.html +++ b/missions/templates/mission_list_tests.html @@ -20,173 +20,157 @@ {% load bootstrap3 %} {% load dart_bootstrap_formatting_helpers %} {% load quickparts %} - {% block title %}Mission Test Cases{% endblock %} - -{% block additional_head_content %} - - -{% endblock %} - +{% block additional_head_content %}{% endblock %} {% block extra_nav_bar_content_right %} -
  • -   - -
  • + {% endblock %} - {% block main_heading %} -

    Mission Test Cases for {{this_mission.mission_name}} - -

    +

    + Mission Test Cases for {{ this_mission.mission_name }} + +

    {% endblock %} - -{% block content%} - - -