diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index 20cec3cb525..018b842f48d 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -2104,8 +2104,14 @@ class CommonImportScanSerializer(serializers.Serializer): required=False, validators=[ImporterFileExtensionValidator()], ) - product_type_name = serializers.CharField(required=False) - product_name = serializers.CharField(required=False) + product_type_name = serializers.CharField( + required=False, + help_text=_("Also referred to as 'Organization' name."), + ) + product_name = serializers.CharField( + required=False, + help_text=_("Also referred to as 'Asset' name."), + ) engagement_name = serializers.CharField(required=False) engagement_end_date = serializers.DateField( required=False, @@ -2160,8 +2166,14 @@ class CommonImportScanSerializer(serializers.Serializer): # confused test_id = serializers.IntegerField(read_only=True) engagement_id = serializers.IntegerField(read_only=True) - product_id = serializers.IntegerField(read_only=True) - product_type_id = serializers.IntegerField(read_only=True) + product_id = serializers.IntegerField( + read_only=True, + help_text=_("Also referred to as 'Asset' ID."), + ) + product_type_id = serializers.IntegerField( + read_only=True, + help_text=_("Also referred to as 'Organization' ID."), + ) statistics = ImportStatisticsSerializer(read_only=True, required=False) pro = serializers.ListField(read_only=True, required=False) apply_tags_to_findings = serializers.BooleanField( diff --git a/dojo/api_v2/views.py b/dojo/api_v2/views.py index 775ff1e90a7..fca1eecbf5a 100644 --- a/dojo/api_v2/views.py +++ b/dojo/api_v2/views.py @@ -85,6 +85,7 @@ get_authorized_jira_issues, get_authorized_jira_projects, ) +from dojo.labels import get_labels from dojo.models import ( Announcement, Answer, @@ -179,6 +180,9 @@ logger = logging.getLogger(__name__) +labels = get_labels() + + def schema_with_prefetch() -> dict: return { "list": extend_schema( @@ -2725,7 +2729,7 @@ def report_generate(request, obj, options): if type(obj).__name__ == "Product_Type": product_type = obj - report_name = "Product Type Report: " + str(product_type) + report_name = labels.ORG_REPORT_WITH_NAME_TITLE % {"name": str(product_type)} findings = report_finding_filter_class( request.GET, @@ -2754,7 +2758,7 @@ def report_generate(request, obj, options): elif type(obj).__name__ == "Product": product = obj - report_name = "Product Report: " + str(product) + report_name = labels.ASSET_REPORT_WITH_NAME_TITLE % {"name": str(product)} findings = report_finding_filter_class( request.GET, diff --git a/dojo/asset/__init__.py b/dojo/asset/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dojo/asset/api/__init__.py b/dojo/asset/api/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dojo/asset/api/filters.py b/dojo/asset/api/filters.py new file mode 100644 index 00000000000..991fd329ac8 --- /dev/null +++ b/dojo/asset/api/filters.py @@ -0,0 +1,121 @@ +from django_filters import BooleanFilter, CharFilter, NumberFilter, OrderingFilter +from django_filters.rest_framework import FilterSet +from drf_spectacular.types import OpenApiTypes +from drf_spectacular.utils import extend_schema_field + +from dojo.filters import ( + CharFieldFilterANDExpression, + CharFieldInFilter, + DateRangeFilter, + DojoFilter, + NumberInFilter, + ProductSLAFilter, + custom_filter, +) +from dojo.labels import get_labels +from dojo.models import ( + Product_API_Scan_Configuration, + Product_Group, + Product_Member, +) + +labels = get_labels() + + +class AssetAPIScanConfigurationFilterSet(FilterSet): + asset = NumberFilter(field_name="product") + + class Meta: + model = Product_API_Scan_Configuration + fields = ("id", "tool_configuration", "service_key_1", "service_key_2", "service_key_3") + + +class ApiAssetFilter(DojoFilter): + # BooleanFilter + external_audience = BooleanFilter(field_name="external_audience") + internet_accessible = BooleanFilter(field_name="internet_accessible") + # CharFilter + name = CharFilter(lookup_expr="icontains") + name_exact = CharFilter(field_name="name", lookup_expr="iexact") + description = CharFilter(lookup_expr="icontains") + business_criticality = CharFilter(method=custom_filter, field_name="business_criticality") + platform = CharFilter(method=custom_filter, field_name="platform") + lifecycle = CharFilter(method=custom_filter, field_name="lifecycle") + origin = CharFilter(method=custom_filter, field_name="origin") + # NumberInFilter + id = NumberInFilter(field_name="id", lookup_expr="in") + asset_manager = NumberInFilter(field_name="product_manager", lookup_expr="in") + technical_contact = NumberInFilter(field_name="technical_contact", lookup_expr="in") + team_manager = NumberInFilter(field_name="team_manager", lookup_expr="in") + prod_type = NumberInFilter(field_name="prod_type", lookup_expr="in") + tid = NumberInFilter(field_name="tid", lookup_expr="in") + prod_numeric_grade = NumberInFilter(field_name="prod_numeric_grade", lookup_expr="in") + user_records = NumberInFilter(field_name="user_records", lookup_expr="in") + regulations = NumberInFilter(field_name="regulations", lookup_expr="in") + + tag = CharFilter(field_name="tags__name", lookup_expr="icontains", label="Tag name contains") + tags = CharFieldInFilter( + field_name="tags__name", + lookup_expr="in", + help_text="Comma separated list of exact tags (uses OR for multiple values)") + tags__and = CharFieldFilterANDExpression( + field_name="tags__name", + help_text="Comma separated list of exact tags to match with an AND expression") + not_tag = CharFilter(field_name="tags__name", lookup_expr="icontains", help_text="Not Tag name contains", exclude="True") + not_tags = CharFieldInFilter(field_name="tags__name", lookup_expr="in", + help_text=labels.ASSET_FILTERS_CSV_TAGS_NOT_HELP, exclude="True") + has_tags = BooleanFilter(field_name="tags", lookup_expr="isnull", exclude=True, label="Has tags") + outside_of_sla = extend_schema_field(OpenApiTypes.NUMBER)(ProductSLAFilter()) + + # DateRangeFilter + created = DateRangeFilter() + updated = DateRangeFilter() + # NumberFilter + revenue = NumberFilter() + + o = OrderingFilter( + # tuple-mapping retains order + fields=( + ("id", "id"), + ("tid", "tid"), + ("name", "name"), + ("created", "created"), + ("prod_numeric_grade", "prod_numeric_grade"), + ("business_criticality", "business_criticality"), + ("platform", "platform"), + ("lifecycle", "lifecycle"), + ("origin", "origin"), + ("revenue", "revenue"), + ("external_audience", "external_audience"), + ("internet_accessible", "internet_accessible"), + ("product_manager", "asset_manager"), + ("product_manager__first_name", "asset_manager__first_name"), + ("product_manager__last_name", "asset_manager__last_name"), + ("technical_contact", "technical_contact"), + ("technical_contact__first_name", "technical_contact__first_name"), + ("technical_contact__last_name", "technical_contact__last_name"), + ("team_manager", "team_manager"), + ("team_manager__first_name", "team_manager__first_name"), + ("team_manager__last_name", "team_manager__last_name"), + ("prod_type", "prod_type"), + ("prod_type__name", "prod_type__name"), + ("updated", "updated"), + ("user_records", "user_records"), + ), + ) + + +class AssetMemberFilterSet(FilterSet): + asset_id = NumberFilter(field_name="product_id") + + class Meta: + model = Product_Member + fields = ("id", "user_id") + + +class AssetGroupFilterSet(FilterSet): + asset_id = NumberFilter(field_name="product_id") + + class Meta: + model = Product_Group + fields = ("id", "group_id") diff --git a/dojo/asset/api/serializers.py b/dojo/asset/api/serializers.py new file mode 100644 index 00000000000..688d772ce9b --- /dev/null +++ b/dojo/asset/api/serializers.py @@ -0,0 +1,160 @@ +from rest_framework import serializers +from rest_framework.exceptions import PermissionDenied, ValidationError + +from dojo.api_v2.serializers import ProductMetaSerializer, TagListSerializerField +from dojo.authorization.authorization import user_has_permission +from dojo.authorization.roles_permissions import Permissions +from dojo.models import ( + Dojo_User, + Product, + Product_API_Scan_Configuration, + Product_Group, + Product_Member, +) +from dojo.organization.api.serializers import RelatedOrganizationField +from dojo.product.queries import get_authorized_products + + +class RelatedAssetField(serializers.PrimaryKeyRelatedField): + def get_queryset(self): + return get_authorized_products(Permissions.Product_View) + + +class AssetAPIScanConfigurationSerializer(serializers.ModelSerializer): + asset = RelatedAssetField(source="product") + + class Meta: + model = Product_API_Scan_Configuration + exclude = ("product",) + + +class AssetSerializer(serializers.ModelSerializer): + findings_count = serializers.SerializerMethodField() + findings_list = serializers.SerializerMethodField() + + tags = TagListSerializerField(required=False) + + # V3 fields + asset_meta = ProductMetaSerializer(source="product_meta", read_only=True, many=True) + organization = RelatedOrganizationField(source="prod_type") + asset_numeric_grade = serializers.IntegerField(source="prod_numeric_grade") + enable_asset_tag_inheritance = serializers.BooleanField(source="enable_product_tag_inheritance") + asset_managers = serializers.PrimaryKeyRelatedField( + source="product_manager", + queryset=Dojo_User.objects.exclude(is_active=False)) + + class Meta: + model = Product + exclude = ( + "tid", + "updated", + "async_updating", + # Below here excluded for V3 migration + "prod_type", + "prod_numeric_grade", + "enable_product_tag_inheritance", + "product_manager", + ) + + def validate(self, data): + async_updating = getattr(self.instance, "async_updating", None) + if async_updating: + new_sla_config = data.get("sla_configuration", None) + old_sla_config = getattr(self.instance, "sla_configuration", None) + if new_sla_config and old_sla_config and new_sla_config != old_sla_config: + msg = "Finding SLA expiration dates are currently being recalculated. The SLA configuration for this asset cannot be changed until the calculation is complete." + raise serializers.ValidationError(msg) + return data + + def get_findings_count(self, obj) -> int: + return obj.findings_count + + # TODO: maybe extend_schema_field is needed here? + def get_findings_list(self, obj) -> list[int]: + return obj.open_findings_list() + + +class AssetMemberSerializer(serializers.ModelSerializer): + asset = RelatedAssetField(source="product") + + class Meta: + model = Product_Member + exclude = ("product",) + + def validate(self, data): + if ( + self.instance is not None + and data.get("asset") != self.instance.product + and not user_has_permission( + self.context["request"].user, + data.get("asset"), + Permissions.Product_Manage_Members, + ) + ): + msg = "You are not permitted to add a member to this Asset" + raise PermissionDenied(msg) + + if ( + self.instance is None + or data.get("asset") != self.instance.product + or data.get("user") != self.instance.user + ): + members = Product_Member.objects.filter( + product=data.get("asset"), user=data.get("user"), + ) + if members.count() > 0: + msg = "Asset Member already exists" + raise ValidationError(msg) + + if data.get("role").is_owner and not user_has_permission( + self.context["request"].user, + data.get("asset"), + Permissions.Product_Member_Add_Owner, + ): + msg = "You are not permitted to add a member as Owner to this Asset" + raise PermissionDenied(msg) + + return data + + +class AssetGroupSerializer(serializers.ModelSerializer): + asset = RelatedAssetField(source="product") + + class Meta: + model = Product_Group + exclude = ("product",) + + def validate(self, data): + if ( + self.instance is not None + and data.get("asset") != self.instance.product + and not user_has_permission( + self.context["request"].user, + data.get("asset"), + Permissions.Product_Group_Add, + ) + ): + msg = "You are not permitted to add a group to this Asset" + raise PermissionDenied(msg) + + if ( + self.instance is None + or data.get("asset") != self.instance.product + or data.get("group") != self.instance.group + ): + members = Product_Group.objects.filter( + product=data.get("asset"), group=data.get("group"), + ) + if members.count() > 0: + msg = "Asset Group already exists" + raise ValidationError(msg) + + if data.get("role").is_owner and not user_has_permission( + self.context["request"].user, + data.get("asset"), + Permissions.Product_Group_Add_Owner, + ): + msg = "You are not permitted to add a group as Owner to this Asset" + raise PermissionDenied(msg) + + return data diff --git a/dojo/asset/api/urls.py b/dojo/asset/api/urls.py new file mode 100644 index 00000000000..706996ea27e --- /dev/null +++ b/dojo/asset/api/urls.py @@ -0,0 +1,15 @@ +from dojo.asset.api.views import ( + AssetAPIScanConfigurationViewSet, + AssetGroupViewSet, + AssetMemberViewSet, + AssetViewSet, +) + + +def add_asset_urls(router): + router.register(r"assets", AssetViewSet, basename="asset") + router.register(r"asset_api_scan_configurations", AssetAPIScanConfigurationViewSet, + basename="asset_api_scan_configuration") + router.register(r"asset_groups", AssetGroupViewSet, basename="asset_group") + router.register(r"asset_members", AssetMemberViewSet, basename="asset_member") + return router diff --git a/dojo/asset/api/views.py b/dojo/asset/api/views.py new file mode 100644 index 00000000000..d3a873f97da --- /dev/null +++ b/dojo/asset/api/views.py @@ -0,0 +1,183 @@ +from django_filters.rest_framework import DjangoFilterBackend +from drf_spectacular.utils import extend_schema, extend_schema_view +from rest_framework import mixins, status, viewsets +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +import dojo.api_v2.mixins as dojo_mixins +from dojo.api_v2 import permissions, prefetch +from dojo.api_v2.serializers import ReportGenerateOptionSerializer, ReportGenerateSerializer +from dojo.api_v2.views import PrefetchDojoModelViewSet, report_generate, schema_with_prefetch +from dojo.asset.api import serializers +from dojo.asset.api.filters import ( + ApiAssetFilter, + AssetAPIScanConfigurationFilterSet, + AssetGroupFilterSet, + AssetMemberFilterSet, +) +from dojo.authorization.roles_permissions import Permissions +from dojo.models import ( + Product, + Product_API_Scan_Configuration, + Product_Group, + Product_Member, +) +from dojo.product.queries import ( + get_authorized_product_api_scan_configurations, + get_authorized_product_groups, + get_authorized_product_members, + get_authorized_products, +) +from dojo.utils import async_delete, get_setting + + +# Authorization: object-based +@extend_schema_view(**schema_with_prefetch()) +class AssetAPIScanConfigurationViewSet( + PrefetchDojoModelViewSet, +): + serializer_class = serializers.AssetAPIScanConfigurationSerializer + queryset = Product_API_Scan_Configuration.objects.none() + filter_backends = (DjangoFilterBackend,) + filterset_class = AssetAPIScanConfigurationFilterSet + permission_classes = ( + IsAuthenticated, + permissions.UserHasProductAPIScanConfigurationPermission, + ) + + def get_queryset(self): + return get_authorized_product_api_scan_configurations( + Permissions.Product_API_Scan_Configuration_View, + ) + + +@extend_schema_view(**schema_with_prefetch()) +class AssetViewSet( + prefetch.PrefetchListMixin, + prefetch.PrefetchRetrieveMixin, + mixins.CreateModelMixin, + mixins.DestroyModelMixin, + mixins.UpdateModelMixin, + viewsets.GenericViewSet, + dojo_mixins.DeletePreviewModelMixin, +): + serializer_class = serializers.AssetSerializer + queryset = Product.objects.none() + filter_backends = (DjangoFilterBackend,) + filterset_class = ApiAssetFilter + permission_classes = ( + IsAuthenticated, + permissions.UserHasProductPermission, + ) + + def get_queryset(self): + return get_authorized_products(Permissions.Product_View).distinct() + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + if get_setting("ASYNC_OBJECT_DELETE"): + async_del = async_delete() + async_del.delete(instance) + else: + instance.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + # def list(self, request): + # # Note the use of `get_queryset()` instead of `self.queryset` + # queryset = self.get_queryset() + # serializer = self.serializer_class(queryset, many=True) + # return Response(serializer.data) + + @extend_schema( + request=ReportGenerateOptionSerializer, + responses={status.HTTP_200_OK: ReportGenerateSerializer}, + ) + @action( + detail=True, methods=["post"], permission_classes=[IsAuthenticated], + ) + def generate_report(self, request, pk=None): + product = self.get_object() + + options = {} + # prepare post data + report_options = ReportGenerateOptionSerializer( + data=request.data, + ) + if report_options.is_valid(): + options["include_finding_notes"] = report_options.validated_data[ + "include_finding_notes" + ] + options["include_finding_images"] = report_options.validated_data[ + "include_finding_images" + ] + options[ + "include_executive_summary" + ] = report_options.validated_data["include_executive_summary"] + options[ + "include_table_of_contents" + ] = report_options.validated_data["include_table_of_contents"] + else: + return Response( + report_options.errors, status=status.HTTP_400_BAD_REQUEST, + ) + + data = report_generate(request, product, options) + report = ReportGenerateSerializer(data) + return Response(report.data) + + +# Authorization: object-based +@extend_schema_view(**schema_with_prefetch()) +class AssetMemberViewSet( + PrefetchDojoModelViewSet, +): + serializer_class = serializers.AssetMemberSerializer + queryset = Product_Member.objects.none() + filter_backends = (DjangoFilterBackend,) + filterset_class = AssetMemberFilterSet + permission_classes = ( + IsAuthenticated, + permissions.UserHasProductMemberPermission, + ) + + def get_queryset(self): + return get_authorized_product_members( + Permissions.Product_View, + ).distinct() + + @extend_schema( + exclude=True, + ) + def partial_update(self, request, pk=None): + # Object authorization won't work if not all data is provided + response = {"message": "Patch function is not offered in this path."} + return Response(response, status=status.HTTP_405_METHOD_NOT_ALLOWED) + + +# Authorization: object-based +@extend_schema_view(**schema_with_prefetch()) +class AssetGroupViewSet( + PrefetchDojoModelViewSet, +): + serializer_class = serializers.AssetGroupSerializer + queryset = Product_Group.objects.none() + filter_backends = (DjangoFilterBackend,) + filterset_class = AssetGroupFilterSet + permission_classes = ( + IsAuthenticated, + permissions.UserHasProductGroupPermission, + ) + + def get_queryset(self): + return get_authorized_product_groups( + Permissions.Product_Group_View, + ).distinct() + + @extend_schema( + exclude=True, + ) + def partial_update(self, request, pk=None): + # Object authorization won't work if not all data is provided + response = {"message": "Patch function is not offered in this path."} + return Response(response, status=status.HTTP_405_METHOD_NOT_ALLOWED) diff --git a/dojo/asset/labels.py b/dojo/asset/labels.py new file mode 100644 index 00000000000..9061d6b05bf --- /dev/null +++ b/dojo/asset/labels.py @@ -0,0 +1,317 @@ +from django.conf import settings +from django.utils.translation import gettext_lazy as _ + + +class AssetLabelsKeys: + + """Directory of text copy used by the Asset model.""" + + ASSET_LABEL = "asset.label" + ASSET_PLURAL_LABEL = "asset.plural_label" + ASSET_ALL_LABEL = "asset.all_label" + ASSET_WITH_NAME_LABEL = "asset.with_name_label" + ASSET_NONE_FOUND_MESSAGE = "asset.none_found_label" + ASSET_MANAGER_LABEL = "asset.manager_label" + ASSET_GLOBAL_ROLE_HELP = "asset.global_role_help" + ASSET_NOTIFICATIONS_HELP = "asset.notifications_help" + ASSET_OPTIONS_LABEL = "asset.options_label" + ASSET_OPTIONS_MENU_LABEL = "asset.options_menu_label" + ASSET_COUNT_LABEL = "asset.count_label" + ASSET_ENGAGEMENTS_BY_LABEL = "asset.engagements_by_label" + ASSET_LIFECYCLE_LABEL = "asset.lifecycle_label" + ASSET_TAG_LABEL = "asset.tag_label" + ASSET_METRICS_TAG_COUNTS_LABEL = "asset.metrics.tag_counts_label" + ASSET_METRICS_TAG_COUNTS_ERROR_MESSAGE = "asset.metrics.tag_counts_error_message" + ASSET_METRICS_CRITICAL_LABEL = "asset.metrics.critical_label" + ASSET_METRICS_NO_CRITICAL_ERROR_MESSAGE = "asset.metrics.no_critical_error_message" + ASSET_METRICS_TOP_TEN_BY_SEVERITY_LABEL = "asset.metrics.top_by_severity_label" + ASSET_NOTIFICATION_WITH_NAME_CREATED_MESSAGE = "asset.notification_with_name_created_message" + ASSET_REPORT_LABEL = "asset.report_label" + ASSET_REPORT_TITLE = "asset.report_title" + ASSET_REPORT_WITH_NAME_TITLE = "asset.report_with_name_title" + ASSET_TRACKED_FILES_ADD_LABEL = "asset.tracked_files.add_label" + ASSET_TRACKED_FILES_ADD_SUCCESS_MESSAGE = "asset.tracked_files.add_success_message" + ASSET_TRACKED_FILES_ID_MISMATCH_ERROR_MESSAGE = "asset.tracked_files.id_mismatch_error_message" + ASSET_FINDINGS_CLOSE_LABEL = "asset.findings_close_label" + ASSET_FINDINGS_CLOSE_HELP = "asset.findings_close_help" + ASSET_TAG_INHERITANCE_ENABLE_LABEL = "asset.tag_inheritance_enable_label" + ASSET_TAG_INHERITANCE_ENABLE_HELP = "asset.tag_inheritance_enable_help" + ASSET_ENDPOINT_HELP = "asset.endpoint_help" + ASSET_CREATE_LABEL = "asset.create.label" + ASSET_CREATE_SUCCESS_MESSAGE = "asset.create.success_message" + ASSET_READ_LIST_LABEL = "asset.read.list_label" + ASSET_UPDATE_LABEL = "asset.update.label" + ASSET_UPDATE_SUCCESS_MESSAGE = "asset.update.success_message" + ASSET_UPDATE_SLA_CHANGED_MESSAGE = "asset.update.sla_changed_message" + ASSET_DELETE_LABEL = "asset.delete.label" + ASSET_DELETE_WITH_NAME_LABEL = "asset.delete.with_name_label" + ASSET_DELETE_CONFIRM_MESSAGE = "asset.delete.confirm_message" + ASSET_DELETE_SUCCESS_MESSAGE = "asset.delete.success_message" + ASSET_DELETE_SUCCESS_ASYNC_MESSAGE = "asset.delete.success_async_message" + ASSET_DELETE_WITH_NAME_SUCCESS_MESSAGE = "asset.delete.with_name_success_message" + ASSET_DELETE_WITH_NAME_WITH_USER_SUCCESS_MESSAGE = "asset.delete.with_name_with_user_success_message" + ASSET_FILTERS_LABEL = "asset.filters.label" + ASSET_FILTERS_NAME_LABEL = "asset.filters.name_label" + ASSET_FILTERS_NAME_HELP = "asset.filters.name_help" + ASSET_FILTERS_NAME_EXACT_LABEL = "asset.filters.name_exact_label" + ASSET_FILTERS_NAME_CONTAINS_LABEL = "asset.filters.name_contains_label" + ASSET_FILTERS_NAME_CONTAINS_HELP = "asset.filters.name_contains_help" + ASSET_FILTERS_TAGS_LABEL = "asset.filters.tags_label" + ASSET_FILTERS_TAGS_HELP = "asset.filters.tags_help" + ASSET_FILTERS_NOT_TAGS_HELP = "asset.filters.not_tags_help" + ASSET_FILTERS_ASSETS_WITHOUT_TAGS_LABEL = "asset.filters.assets_without_tags_label" + ASSET_FILTERS_ASSETS_WITHOUT_TAGS_HELP = "asset.filters.assets_without_tags_help" + ASSET_FILTERS_TAGS_FILTER_LABEL = "asset.filters.tags_filter_label" + ASSET_FILTERS_TAGS_FILTER_HELP = "asset.filters.tags_filter_help" + ASSET_FILTERS_CSV_TAGS_OR_HELP = "asset.filters.csv_tags_or_help" + ASSET_FILTERS_CSV_TAGS_AND_HELP = "asset.filters.csv_tags_and_help" + ASSET_FILTERS_CSV_TAGS_NOT_HELP = "asset.filters.csv_tags_not_help" + ASSET_FILTERS_CSV_LIFECYCLES_LABEL = "asset.filters.csv_lifecycles_label" + ASSET_FILTERS_TAGS_ASSET_LABEL = "asset.filters.tags_asset_label" + ASSET_FILTERS_TAG_ASSET_LABEL = "asset.filters.tag_asset_label" + ASSET_FILTERS_TAG_ASSET_HELP = "asset.filters.tag_asset_help" + ASSET_FILTERS_NOT_TAGS_ASSET_LABEL = "asset.filters.not_tags_asset_label" + ASSET_FILTERS_WITHOUT_TAGS_LABEL = "asset.filters.without_tags_label" + ASSET_FILTERS_TAG_ASSET_CONTAINS_LABEL = "asset.filters.tag_asset_contains_label" + ASSET_FILTERS_TAG_ASSET_CONTAINS_HELP = "asset.filters.tag_asset_contains_help" + ASSET_FILTERS_TAG_NOT_CONTAIN_LABEL = "asset.filters.tag_not_contain_label" + ASSET_FILTERS_TAG_NOT_CONTAIN_HELP = "asset.filters.tag_not_contain_help" + ASSET_FILTERS_TAG_NOT_LABEL = "asset.filters.tag_not_label" + ASSET_FILTERS_TAG_NOT_HELP = "asset.filters.tag_not_help" + ASSET_USERS_ACCESS_LABEL = "asset.users.access_label" + ASSET_USERS_NO_ACCESS_MESSAGE = "asset.users.no_access_message" + ASSET_USERS_ADD_LABEL = "asset.users.add_label" + ASSET_USERS_USERS_ADD_LABEL = "asset.users.users_add_label" + ASSET_USERS_MEMBER_LABEL = "asset.users.member_label" + ASSET_USERS_MEMBER_ADD_LABEL = "asset.users.member_add_label" + ASSET_USERS_MEMBER_ADD_SUCCESS_MESSAGE = "asset.users.member_add_success_message" + ASSET_USERS_MEMBER_UPDATE_LABEL = "asset.users.member_update_label" + ASSET_USERS_MEMBER_UPDATE_SUCCESS_MESSAGE = "asset.users.member_update_success_message" + ASSET_USERS_MEMBER_DELETE_LABEL = "asset.users.member_delete_label" + ASSET_USERS_MEMBER_DELETE_SUCCESS_MESSAGE = "asset.users.member_delete_success_message" + ASSET_GROUPS_ACCESS_LABEL = "asset.groups.access_label" + ASSET_GROUPS_NO_ACCESS_MESSAGE = "asset.groups.no_access_message" + ASSET_GROUPS_MEMBER_LABEL = "asset.groups.member_label" + ASSET_GROUPS_ADD_LABEL = "asset.groups.add_label" + ASSET_GROUPS_ADD_SUCCESS_MESSAGE = "asset.groups.add_success_message" + ASSET_GROUPS_UPDATE_LABEL = "asset.groups.update_label" + ASSET_GROUPS_UPDATE_SUCCESS_MESSAGE = "asset.groups.update_success_message" + ASSET_GROUPS_DELETE_LABEL = "asset.groups.delete_label" + ASSET_GROUPS_DELETE_SUCCESS_MESSAGE = "asset.groups.delete_success_message" + ASSET_GROUPS_ADD_ASSETS_LABEL = "asset.groups.add_assets_label" + ASSET_GROUPS_NUM_ASSETS_LABEL = "asset.groups.num_assets_label" + + +# TODO: remove the else: branch once v3 migration is complete +if settings.ENABLE_V3_ORGANIZATION_ASSET_RELABEL: + labels = { + AssetLabelsKeys.ASSET_LABEL: _("Asset"), + AssetLabelsKeys.ASSET_PLURAL_LABEL: _("Assets"), + AssetLabelsKeys.ASSET_ALL_LABEL: _("All Assets"), + AssetLabelsKeys.ASSET_WITH_NAME_LABEL: _("Asset '%(name)s'"), + AssetLabelsKeys.ASSET_NONE_FOUND_MESSAGE: _("No Assets found."), + AssetLabelsKeys.ASSET_MANAGER_LABEL: _("Asset Manager"), + AssetLabelsKeys.ASSET_GLOBAL_ROLE_HELP: _("The global role will be applied to all Organizations and Assets."), + AssetLabelsKeys.ASSET_NOTIFICATIONS_HELP: _("These are your personal settings for this Asset."), + AssetLabelsKeys.ASSET_OPTIONS_LABEL: _("Asset Options"), + AssetLabelsKeys.ASSET_OPTIONS_MENU_LABEL: _("Asset Options Menu"), + AssetLabelsKeys.ASSET_COUNT_LABEL: _("Asset Count"), + AssetLabelsKeys.ASSET_ENGAGEMENTS_BY_LABEL: _("Engagements by Asset"), + AssetLabelsKeys.ASSET_LIFECYCLE_LABEL: _("Asset Lifecycle"), + AssetLabelsKeys.ASSET_TAG_LABEL: _("Asset Tag"), + AssetLabelsKeys.ASSET_METRICS_TAG_COUNTS_LABEL: _("Asset Tag Counts"), + AssetLabelsKeys.ASSET_METRICS_TAG_COUNTS_ERROR_MESSAGE: _("Please choose month and year and the Asset Tag."), + AssetLabelsKeys.ASSET_METRICS_CRITICAL_LABEL: _("Critical Asset Metrics"), + AssetLabelsKeys.ASSET_METRICS_NO_CRITICAL_ERROR_MESSAGE: _("No Critical Assets registered"), + AssetLabelsKeys.ASSET_METRICS_TOP_TEN_BY_SEVERITY_LABEL: _("Top 10 Assets by bug severity"), + AssetLabelsKeys.ASSET_NOTIFICATION_WITH_NAME_CREATED_MESSAGE: _("Asset %(name)s has been created successfully."), + AssetLabelsKeys.ASSET_REPORT_LABEL: _("Asset Report"), + AssetLabelsKeys.ASSET_REPORT_TITLE: _("Asset Report"), + AssetLabelsKeys.ASSET_REPORT_WITH_NAME_TITLE: _("Asset Report: %(name)s"), + AssetLabelsKeys.ASSET_TRACKED_FILES_ADD_LABEL: _("Add Tracked Files to an Asset"), + AssetLabelsKeys.ASSET_TRACKED_FILES_ADD_SUCCESS_MESSAGE: _("Added Tracked File to an Asset"), + AssetLabelsKeys.ASSET_TRACKED_FILES_ID_MISMATCH_ERROR_MESSAGE: _( + "Asset %(asset_id)s does not match Asset of Object %(object_asset_id)s"), + AssetLabelsKeys.ASSET_FINDINGS_CLOSE_LABEL: _("Close old findings within this Asset"), + AssetLabelsKeys.ASSET_FINDINGS_CLOSE_HELP: _( + "Old findings no longer present in the new report get closed as mitigated when importing. If service has been set, only the findings for this service will be closed; if no service is set, only findings without a service will be closed. This affects findings within the same Asset."), + AssetLabelsKeys.ASSET_TAG_INHERITANCE_ENABLE_LABEL: _("Enable Asset Tag Inheritance"), + AssetLabelsKeys.ASSET_TAG_INHERITANCE_ENABLE_HELP: _( + "Enables Asset tag inheritance. Any tags added on an Asset will automatically be added to all Engagements, Tests, and Findings."), + AssetLabelsKeys.ASSET_ENDPOINT_HELP: _("The Asset this Endpoint should be associated with."), + AssetLabelsKeys.ASSET_CREATE_LABEL: _("Add Asset"), + AssetLabelsKeys.ASSET_CREATE_SUCCESS_MESSAGE: _("Asset added successfully."), + AssetLabelsKeys.ASSET_READ_LIST_LABEL: _("Asset List"), + AssetLabelsKeys.ASSET_UPDATE_LABEL: _("Edit Asset"), + AssetLabelsKeys.ASSET_UPDATE_SUCCESS_MESSAGE: _("Asset updated successfully."), + AssetLabelsKeys.ASSET_UPDATE_SLA_CHANGED_MESSAGE: _( + "All SLA expiration dates for Findings within this Asset will be recalculated asynchronously for the newly assigned SLA configuration."), + AssetLabelsKeys.ASSET_DELETE_LABEL: _("Delete Asset"), + AssetLabelsKeys.ASSET_DELETE_WITH_NAME_LABEL: _("Delete Asset %(name)s"), + AssetLabelsKeys.ASSET_DELETE_CONFIRM_MESSAGE: _( + "Deleting this Asset will remove any related objects associated with it. These relationships are listed below: "), + AssetLabelsKeys.ASSET_DELETE_SUCCESS_MESSAGE: _("Asset and relationships removed."), + AssetLabelsKeys.ASSET_DELETE_SUCCESS_ASYNC_MESSAGE: _("Asset and relationships will be removed in the background."), + AssetLabelsKeys.ASSET_DELETE_WITH_NAME_SUCCESS_MESSAGE: _('The Asset "%(name)s" was deleted'), + AssetLabelsKeys.ASSET_DELETE_WITH_NAME_WITH_USER_SUCCESS_MESSAGE: _('The Asset "%(name)s" was deleted by %(user)s'), + AssetLabelsKeys.ASSET_FILTERS_LABEL: _("Asset"), + AssetLabelsKeys.ASSET_FILTERS_NAME_LABEL: _("Asset Name"), + AssetLabelsKeys.ASSET_FILTERS_NAME_HELP: _("Search for Asset names that are an exact match"), + AssetLabelsKeys.ASSET_FILTERS_NAME_EXACT_LABEL: _("Exact Asset Name"), + AssetLabelsKeys.ASSET_FILTERS_NAME_CONTAINS_LABEL: _("Asset Name Contains"), + AssetLabelsKeys.ASSET_FILTERS_NAME_CONTAINS_HELP: _("Search for Asset names that contain a given pattern"), + AssetLabelsKeys.ASSET_FILTERS_TAGS_LABEL: _("Tags (Asset)"), + AssetLabelsKeys.ASSET_FILTERS_TAGS_HELP: _("Filter for Assets with the given tags"), + AssetLabelsKeys.ASSET_FILTERS_NOT_TAGS_HELP: _("Filter for Assets that do not have the given tags"), + AssetLabelsKeys.ASSET_FILTERS_ASSETS_WITHOUT_TAGS_LABEL: _("Assets without tags"), + AssetLabelsKeys.ASSET_FILTERS_ASSETS_WITHOUT_TAGS_HELP: _( + "Search for tags on an Asset that contain a given pattern, and exclude them"), + AssetLabelsKeys.ASSET_FILTERS_TAGS_FILTER_LABEL: _("Asset with tags"), + AssetLabelsKeys.ASSET_FILTERS_TAGS_FILTER_HELP: _("Filter Assets by the selected tags"), + AssetLabelsKeys.ASSET_FILTERS_CSV_TAGS_OR_HELP: _( + "Comma separated list of exact tags present on Asset (uses OR for multiple values)"), + AssetLabelsKeys.ASSET_FILTERS_CSV_TAGS_AND_HELP: _( + "Comma separated list of exact tags to match with an AND expression present on Asset"), + AssetLabelsKeys.ASSET_FILTERS_CSV_TAGS_NOT_HELP: _("Comma separated list of exact tags not present on Asset"), + AssetLabelsKeys.ASSET_FILTERS_CSV_LIFECYCLES_LABEL: _("Comma separated list of exact Asset lifecycles"), + AssetLabelsKeys.ASSET_FILTERS_TAGS_ASSET_LABEL: _("Asset Tags"), + AssetLabelsKeys.ASSET_FILTERS_TAG_ASSET_LABEL: _("Asset Tag"), + AssetLabelsKeys.ASSET_FILTERS_TAG_ASSET_HELP: _("Search for tags on an Asset that are an exact match"), + AssetLabelsKeys.ASSET_FILTERS_NOT_TAGS_ASSET_LABEL: _("Not Asset Tags"), + AssetLabelsKeys.ASSET_FILTERS_WITHOUT_TAGS_LABEL: _("Asset without tags"), + AssetLabelsKeys.ASSET_FILTERS_TAG_ASSET_CONTAINS_LABEL: _("Asset Tag Contains"), + AssetLabelsKeys.ASSET_FILTERS_TAG_ASSET_CONTAINS_HELP: _("Search for tags on an Asset that contain a given pattern"), + AssetLabelsKeys.ASSET_FILTERS_TAG_NOT_CONTAIN_LABEL: _("Asset Tag Does Not Contain"), + AssetLabelsKeys.ASSET_FILTERS_TAG_NOT_CONTAIN_HELP: _( + "Search for tags on an Asset that contain a given pattern, and exclude them"), + AssetLabelsKeys.ASSET_FILTERS_TAG_NOT_LABEL: _("Not Asset Tag"), + AssetLabelsKeys.ASSET_FILTERS_TAG_NOT_HELP: _("Search for tags on an Asset that are an exact match, and exclude them"), + AssetLabelsKeys.ASSET_USERS_ACCESS_LABEL: _("Assets this User can access"), + AssetLabelsKeys.ASSET_USERS_NO_ACCESS_MESSAGE: _("This User is not assigned to any Assets."), + AssetLabelsKeys.ASSET_USERS_ADD_LABEL: _("Add Assets"), + AssetLabelsKeys.ASSET_USERS_USERS_ADD_LABEL: _("Add Users"), + AssetLabelsKeys.ASSET_USERS_MEMBER_LABEL: _("Asset Member"), + AssetLabelsKeys.ASSET_USERS_MEMBER_ADD_LABEL: _("Add Asset Member"), + AssetLabelsKeys.ASSET_USERS_MEMBER_ADD_SUCCESS_MESSAGE: _("Asset members added successfully."), + AssetLabelsKeys.ASSET_USERS_MEMBER_UPDATE_LABEL: _("Edit Asset Member"), + AssetLabelsKeys.ASSET_USERS_MEMBER_UPDATE_SUCCESS_MESSAGE: _("Asset member updated successfully."), + AssetLabelsKeys.ASSET_USERS_MEMBER_DELETE_LABEL: _("Delete Asset Member"), + AssetLabelsKeys.ASSET_USERS_MEMBER_DELETE_SUCCESS_MESSAGE: _("Asset member deleted successfully."), + AssetLabelsKeys.ASSET_GROUPS_ACCESS_LABEL: _("Assets this Group can access"), + AssetLabelsKeys.ASSET_GROUPS_NO_ACCESS_MESSAGE: _("This Group cannot access any Assets."), + AssetLabelsKeys.ASSET_GROUPS_MEMBER_LABEL: _("Asset Group"), + AssetLabelsKeys.ASSET_GROUPS_ADD_LABEL: _("Add Asset Group"), + AssetLabelsKeys.ASSET_GROUPS_ADD_SUCCESS_MESSAGE: _("Asset groups added successfully."), + AssetLabelsKeys.ASSET_GROUPS_UPDATE_LABEL: _("Edit Asset Group"), + AssetLabelsKeys.ASSET_GROUPS_UPDATE_SUCCESS_MESSAGE: _("Asset group updated successfully."), + AssetLabelsKeys.ASSET_GROUPS_DELETE_LABEL: _("Delete Asset Group"), + AssetLabelsKeys.ASSET_GROUPS_DELETE_SUCCESS_MESSAGE: _("Asset group deleted successfully."), + AssetLabelsKeys.ASSET_GROUPS_ADD_ASSETS_LABEL: _("Add Assets"), + AssetLabelsKeys.ASSET_GROUPS_NUM_ASSETS_LABEL: _("Number of Assets"), + } +else: + labels = { + AssetLabelsKeys.ASSET_LABEL: _("Product"), + AssetLabelsKeys.ASSET_PLURAL_LABEL: _("Products"), + AssetLabelsKeys.ASSET_ALL_LABEL: _("All Products"), + AssetLabelsKeys.ASSET_WITH_NAME_LABEL: _("Product '%(name)s'"), + AssetLabelsKeys.ASSET_NONE_FOUND_MESSAGE: _("No Products found."), + AssetLabelsKeys.ASSET_MANAGER_LABEL: _("Product Manager"), + AssetLabelsKeys.ASSET_GLOBAL_ROLE_HELP: _("The global role will be applied to all Product Types and Products."), + AssetLabelsKeys.ASSET_NOTIFICATIONS_HELP: _("These are your personal settings for this Product."), + AssetLabelsKeys.ASSET_OPTIONS_LABEL: _("Product Options"), + AssetLabelsKeys.ASSET_OPTIONS_MENU_LABEL: _("Product Options Menu"), + AssetLabelsKeys.ASSET_COUNT_LABEL: _("Product Count"), + AssetLabelsKeys.ASSET_ENGAGEMENTS_BY_LABEL: _("Engagements by Product"), + AssetLabelsKeys.ASSET_LIFECYCLE_LABEL: _("Product Lifecycle"), + AssetLabelsKeys.ASSET_TAG_LABEL: _("Product Tag"), + AssetLabelsKeys.ASSET_METRICS_TAG_COUNTS_LABEL: _("Product Tag Counts"), + AssetLabelsKeys.ASSET_METRICS_TAG_COUNTS_ERROR_MESSAGE: _("Please choose month and year and the Product Tag."), + AssetLabelsKeys.ASSET_METRICS_CRITICAL_LABEL: _("Critical Product Metrics"), + AssetLabelsKeys.ASSET_METRICS_NO_CRITICAL_ERROR_MESSAGE: _("No Critical Products registered"), + AssetLabelsKeys.ASSET_METRICS_TOP_TEN_BY_SEVERITY_LABEL: _("Top 10 Products by bug severity"), + AssetLabelsKeys.ASSET_NOTIFICATION_WITH_NAME_CREATED_MESSAGE: _("Product %(name)s has been created successfully."), + AssetLabelsKeys.ASSET_REPORT_LABEL: _("Product Report"), + AssetLabelsKeys.ASSET_REPORT_TITLE: _("Product Report"), + AssetLabelsKeys.ASSET_REPORT_WITH_NAME_TITLE: _("Product Report: %(name)s"), + AssetLabelsKeys.ASSET_TRACKED_FILES_ADD_LABEL: _("Add Tracked Files to a Product"), + AssetLabelsKeys.ASSET_TRACKED_FILES_ADD_SUCCESS_MESSAGE: _("Added Tracked File to a Product"), + AssetLabelsKeys.ASSET_TRACKED_FILES_ID_MISMATCH_ERROR_MESSAGE: _( + "Product %(asset_id)s does not match Product of Object %(object_asset_id)s"), + AssetLabelsKeys.ASSET_FINDINGS_CLOSE_LABEL: _("Close old findings within this Product"), + AssetLabelsKeys.ASSET_FINDINGS_CLOSE_HELP: _( + "Old findings no longer present in the new report get closed as mitigated when importing. If service has been set, only the findings for this service will be closed; if no service is set, only findings without a service will be closed. This affects findings within the same product."), + AssetLabelsKeys.ASSET_TAG_INHERITANCE_ENABLE_LABEL: _("Enable Product Tag Inheritance"), + AssetLabelsKeys.ASSET_TAG_INHERITANCE_ENABLE_HELP: _( + "Enables Product tag inheritance. Any tags added on an Product will automatically be added to all Engagements, Tests, and Findings."), + AssetLabelsKeys.ASSET_ENDPOINT_HELP: _("The Product this Endpoint should be associated with."), + AssetLabelsKeys.ASSET_CREATE_LABEL: _("Add Product"), + AssetLabelsKeys.ASSET_CREATE_SUCCESS_MESSAGE: _("Product added successfully."), + AssetLabelsKeys.ASSET_READ_LIST_LABEL: _("Product List"), + AssetLabelsKeys.ASSET_UPDATE_LABEL: _("Edit Product"), + AssetLabelsKeys.ASSET_UPDATE_SUCCESS_MESSAGE: _("Product updated successfully."), + AssetLabelsKeys.ASSET_UPDATE_SLA_CHANGED_MESSAGE: _( + "All SLA expiration dates for Findings within this Product will be recalculated asynchronously for the newly assigned SLA configuration."), + AssetLabelsKeys.ASSET_DELETE_LABEL: _("Delete Product"), + AssetLabelsKeys.ASSET_DELETE_WITH_NAME_LABEL: _("Delete Product %(name)s"), + AssetLabelsKeys.ASSET_DELETE_CONFIRM_MESSAGE: _( + "Deleting this Product will remove any related objects associated with it. These relationships are listed below: "), + AssetLabelsKeys.ASSET_DELETE_SUCCESS_MESSAGE: _("Product and relationships removed."), + AssetLabelsKeys.ASSET_DELETE_SUCCESS_ASYNC_MESSAGE: _("Product and relationships will be removed in the background."), + AssetLabelsKeys.ASSET_DELETE_WITH_NAME_SUCCESS_MESSAGE: _('The product "%(name)s" was deleted'), + AssetLabelsKeys.ASSET_DELETE_WITH_NAME_WITH_USER_SUCCESS_MESSAGE: _('The product "%(name)s" was deleted by %(user)s'), + AssetLabelsKeys.ASSET_FILTERS_LABEL: _("Product"), + AssetLabelsKeys.ASSET_FILTERS_NAME_LABEL: _("Product Name"), + AssetLabelsKeys.ASSET_FILTERS_NAME_HELP: _("Search for Product names that are an exact match"), + AssetLabelsKeys.ASSET_FILTERS_NAME_EXACT_LABEL: _("Exact Product Name"), + AssetLabelsKeys.ASSET_FILTERS_NAME_CONTAINS_LABEL: _("Product Name Contains"), + AssetLabelsKeys.ASSET_FILTERS_NAME_CONTAINS_HELP: _("Search for Product names that contain a given pattern"), + AssetLabelsKeys.ASSET_FILTERS_TAGS_LABEL: _("Tags (Product)"), + AssetLabelsKeys.ASSET_FILTERS_TAGS_HELP: _("Filter for Products with the given tags"), + AssetLabelsKeys.ASSET_FILTERS_NOT_TAGS_HELP: _("Filter for Products that do not have the given tags"), + AssetLabelsKeys.ASSET_FILTERS_ASSETS_WITHOUT_TAGS_LABEL: _("Products without tags"), + AssetLabelsKeys.ASSET_FILTERS_ASSETS_WITHOUT_TAGS_HELP: _( + "Search for tags on an Product that contain a given pattern, and exclude them"), + AssetLabelsKeys.ASSET_FILTERS_TAGS_FILTER_LABEL: _("Product with tags"), + AssetLabelsKeys.ASSET_FILTERS_TAGS_FILTER_HELP: _("Filter Products by the selected tags"), + AssetLabelsKeys.ASSET_FILTERS_CSV_TAGS_OR_HELP: _( + "Comma separated list of exact tags present on Product (uses OR for multiple values)"), + AssetLabelsKeys.ASSET_FILTERS_CSV_TAGS_AND_HELP: _( + "Comma separated list of exact tags to match with an AND expression present on Product"), + AssetLabelsKeys.ASSET_FILTERS_CSV_TAGS_NOT_HELP: _("Comma separated list of exact tags not present on Product"), + AssetLabelsKeys.ASSET_FILTERS_CSV_LIFECYCLES_LABEL: _("Comma separated list of exact Product lifecycles"), + AssetLabelsKeys.ASSET_FILTERS_TAGS_ASSET_LABEL: _("Product Tags"), + AssetLabelsKeys.ASSET_FILTERS_TAG_ASSET_LABEL: _("Product Tag"), + AssetLabelsKeys.ASSET_FILTERS_TAG_ASSET_HELP: _("Search for tags on an Product that are an exact match"), + AssetLabelsKeys.ASSET_FILTERS_NOT_TAGS_ASSET_LABEL: _("Not Product Tags"), + AssetLabelsKeys.ASSET_FILTERS_WITHOUT_TAGS_LABEL: _("Product without tags"), + AssetLabelsKeys.ASSET_FILTERS_TAG_ASSET_CONTAINS_LABEL: _("Product Tag Contains"), + AssetLabelsKeys.ASSET_FILTERS_TAG_ASSET_CONTAINS_HELP: _("Search for tags on an Product that contain a given pattern"), + AssetLabelsKeys.ASSET_FILTERS_TAG_NOT_CONTAIN_LABEL: _("Product Tag Does Not Contain"), + AssetLabelsKeys.ASSET_FILTERS_TAG_NOT_CONTAIN_HELP: _( + "Search for tags on an Product that contain a given pattern, and exclude them"), + AssetLabelsKeys.ASSET_FILTERS_TAG_NOT_LABEL: _("Not Product Tag"), + AssetLabelsKeys.ASSET_FILTERS_TAG_NOT_HELP: _("Search for tags on an Product that are an exact match, and exclude them"), + AssetLabelsKeys.ASSET_USERS_ACCESS_LABEL: _("Products this User can access"), + AssetLabelsKeys.ASSET_USERS_NO_ACCESS_MESSAGE: _("This User is not assigned to any Products."), + AssetLabelsKeys.ASSET_USERS_ADD_LABEL: _("Add Products"), + AssetLabelsKeys.ASSET_USERS_USERS_ADD_LABEL: _("Add Users"), + AssetLabelsKeys.ASSET_USERS_MEMBER_LABEL: _("Product Member"), + AssetLabelsKeys.ASSET_USERS_MEMBER_ADD_LABEL: _("Add Product Member"), + AssetLabelsKeys.ASSET_USERS_MEMBER_ADD_SUCCESS_MESSAGE: _("Product members added successfully."), + AssetLabelsKeys.ASSET_USERS_MEMBER_UPDATE_LABEL: _("Edit Product Member"), + AssetLabelsKeys.ASSET_USERS_MEMBER_UPDATE_SUCCESS_MESSAGE: _("Product member updated successfully."), + AssetLabelsKeys.ASSET_USERS_MEMBER_DELETE_LABEL: _("Delete Product Member"), + AssetLabelsKeys.ASSET_USERS_MEMBER_DELETE_SUCCESS_MESSAGE: _("Product member deleted successfully."), + AssetLabelsKeys.ASSET_GROUPS_ACCESS_LABEL: _("Products this Group can access"), + AssetLabelsKeys.ASSET_GROUPS_NO_ACCESS_MESSAGE: _("This Group cannot access any Products."), + AssetLabelsKeys.ASSET_GROUPS_MEMBER_LABEL: _("Product Group"), + AssetLabelsKeys.ASSET_GROUPS_ADD_LABEL: _("Add Product Group"), + AssetLabelsKeys.ASSET_GROUPS_ADD_SUCCESS_MESSAGE: _("Product groups added successfully."), + AssetLabelsKeys.ASSET_GROUPS_UPDATE_LABEL: _("Edit Product Group"), + AssetLabelsKeys.ASSET_GROUPS_UPDATE_SUCCESS_MESSAGE: _("Product group updated successfully."), + AssetLabelsKeys.ASSET_GROUPS_DELETE_LABEL: _("Delete Product Group"), + AssetLabelsKeys.ASSET_GROUPS_DELETE_SUCCESS_MESSAGE: _("Product group deleted successfully."), + AssetLabelsKeys.ASSET_GROUPS_ADD_ASSETS_LABEL: _("Add Products"), + AssetLabelsKeys.ASSET_GROUPS_NUM_ASSETS_LABEL: _("Number of Products"), + } diff --git a/dojo/asset/urls.py b/dojo/asset/urls.py new file mode 100644 index 00000000000..e248348b74b --- /dev/null +++ b/dojo/asset/urls.py @@ -0,0 +1,317 @@ +from django.conf import settings +from django.urls import re_path + +from dojo.engagement import views as dojo_engagement_views +from dojo.product import views +from dojo.utils import redirect_view + +# TODO: remove the else: branch once v3 migration is complete +if settings.ENABLE_V3_ORGANIZATION_ASSET_RELABEL: + urlpatterns = [ + re_path( + r"^asset$", + views.product, + name="product", + ), + re_path( + r"^asset/(?P\d+)$", + views.view_product, + name="view_product", + ), + re_path( + r"^asset/(?P\d+)/components$", + views.view_product_components, + name="view_product_components", + ), + re_path( + r"^asset/(?P\d+)/engagements$", + views.view_engagements, + name="view_engagements", + ), + re_path( + r"^asset/(?P\d+)/import_scan_results$", + dojo_engagement_views.ImportScanResultsView.as_view(), + name="import_scan_results_prod", + ), + re_path( + r"^asset/(?P\d+)/metrics$", + views.view_product_metrics, + name="view_product_metrics", + ), + re_path( + r"^asset/(?P\d+)/async_burndown_metrics$", + views.async_burndown_metrics, + name="async_burndown_metrics", + ), + re_path( + r"^asset/(?P\d+)/edit$", + views.edit_product, + name="edit_product", + ), + re_path( + r"^asset/(?P\d+)/delete$", + views.delete_product, + name="delete_product", + ), + re_path( + r"^asset/add", + views.new_product, + name="new_product", + ), + re_path( + r"^asset/(?P\d+)/new_engagement$", + views.new_eng_for_app, + name="new_eng_for_prod", + ), + re_path( + r"^asset/(?P\d+)/new_technology$", + views.new_tech_for_prod, + name="new_tech_for_prod", + ), + re_path( + r"^technology/(?P\d+)/edit$", + views.edit_technology, + name="edit_technology", + ), + re_path( + r"^technology/(?P\d+)/delete$", + views.delete_technology, + name="delete_technology", + ), + re_path( + r"^asset/(?P\d+)/new_engagement/cicd$", + views.new_eng_for_app_cicd, + name="new_eng_for_prod_cicd", + ), + re_path( + r"^asset/(?P\d+)/add_meta_data$", + views.add_meta_data, + name="add_meta_data", + ), + re_path( + r"^asset/(?P\d+)/edit_notifications$", + views.edit_notifications, + name="edit_notifications", + ), + re_path( + r"^asset/(?P\d+)/edit_meta_data$", + views.edit_meta_data, + name="edit_meta_data", + ), + re_path( + r"^asset/(?P\d+)/ad_hoc_finding$", + views.AdHocFindingView.as_view(), + name="ad_hoc_finding", + ), + re_path( + r"^asset/(?P\d+)/engagement_presets$", + views.engagement_presets, + name="engagement_presets", + ), + re_path( + r"^asset/(?P\d+)/engagement_presets/(?P\d+)/edit$", + views.edit_engagement_presets, + name="edit_engagement_presets", + ), + re_path( + r"^asset/(?P\d+)/engagement_presets/add$", + views.add_engagement_presets, + name="add_engagement_presets", + ), + re_path( + r"^asset/(?P\d+)/engagement_presets/(?P\d+)/delete$", + views.delete_engagement_presets, + name="delete_engagement_presets", + ), + re_path( + r"^asset/(?P\d+)/add_member$", + views.add_product_member, + name="add_product_member", + ), + re_path( + r"^asset/member/(?P\d+)/edit$", + views.edit_product_member, + name="edit_product_member", + ), + re_path( + r"^asset/member/(?P\d+)/delete$", + views.delete_product_member, + name="delete_product_member", + ), + re_path( + r"^asset/(?P\d+)/add_api_scan_configuration$", + views.add_api_scan_configuration, + name="add_api_scan_configuration", + ), + re_path( + r"^asset/(?P\d+)/view_api_scan_configurations$", + views.view_api_scan_configurations, + name="view_api_scan_configurations", + ), + re_path( + r"^asset/(?P\d+)/edit_api_scan_configuration/(?P\d+)$", + views.edit_api_scan_configuration, + name="edit_api_scan_configuration", + ), + re_path( + r"^asset/(?P\d+)/delete_api_scan_configuration/(?P\d+)$", + views.delete_api_scan_configuration, + name="delete_api_scan_configuration", + ), + re_path( + r"^asset/(?P\d+)/add_group$", + views.add_product_group, + name="add_product_group", + ), + re_path( + r"^asset/group/(?P\d+)/edit$", + views.edit_product_group, + name="edit_product_group", + ), + re_path( + r"^asset/group/(?P\d+)/delete$", + views.delete_product_group, + name="delete_product_group", + ), + # TODO: Backwards compatibility; remove after v3 migration is complete + re_path(r"^product$", redirect_view("product")), + re_path(r"^product/(?P\d+)$", redirect_view("view_product")), + re_path(r"^product/(?P\d+)/components$", redirect_view("view_product_components")), + re_path(r"^product/(?P\d+)/engagements$", redirect_view("view_engagements")), + re_path(r"^product/(?P\d+)/import_scan_results$", redirect_view("import_scan_results_prod")), + re_path(r"^product/(?P\d+)/metrics$", redirect_view("view_product_metrics")), + re_path(r"^product/(?P\d+)/async_burndown_metrics$", redirect_view("async_burndown_metrics")), + re_path(r"^product/(?P\d+)/edit$", redirect_view("edit_product")), + re_path(r"^product/(?P\d+)/delete$", redirect_view("delete_product")), + re_path(r"^product/add", redirect_view("new_product")), + re_path(r"^product/(?P\d+)/new_engagement$", redirect_view("new_eng_for_prod")), + re_path(r"^product/(?P\d+)/new_technology$", redirect_view("new_tech_for_prod")), + re_path(r"^product/(?P\d+)/new_engagement/cicd$", redirect_view("new_eng_for_prod_cicd")), + re_path(r"^product/(?P\d+)/add_meta_data$", redirect_view("add_meta_data")), + re_path(r"^product/(?P\d+)/edit_notifications$", redirect_view("edit_notifications")), + re_path(r"^product/(?P\d+)/edit_meta_data$", redirect_view("edit_meta_data")), + re_path(r"^product/(?P\d+)/ad_hoc_finding$", redirect_view("ad_hoc_finding")), + re_path(r"^product/(?P\d+)/engagement_presets$", redirect_view("engagement_presets")), + re_path(r"^product/(?P\d+)/engagement_presets/(?P\d+)/edit$", redirect_view("edit_engagement_presets")), + re_path(r"^product/(?P\d+)/engagement_presets/add$", redirect_view("add_engagement_presets")), + re_path(r"^product/(?P\d+)/engagement_presets/(?P\d+)/delete$", redirect_view("delete_engagement_presets")), + re_path(r"^product/(?P\d+)/add_member$", redirect_view("add_product_member")), + re_path(r"^product/member/(?P\d+)/edit$", redirect_view("edit_product_member")), + re_path(r"^product/member/(?P\d+)/delete$", redirect_view("delete_product_member")), + re_path(r"^product/(?P\d+)/add_api_scan_configuration$", redirect_view("add_api_scan_configuration")), + re_path(r"^product/(?P\d+)/view_api_scan_configurations$", redirect_view("view_api_scan_configurations")), + re_path(r"^product/(?P\d+)/edit_api_scan_configuration/(?P\d+)$", redirect_view("edit_api_scan_configuration")), + re_path(r"^product/(?P\d+)/delete_api_scan_configuration/(?P\d+)$", redirect_view("delete_api_scan_configuration")), + re_path(r"^product/(?P\d+)/add_group$", redirect_view("add_product_group")), + re_path(r"^product/group/(?P\d+)/edit$", redirect_view("edit_product_group")), + re_path(r"^product/group/(?P\d+)/delete$", redirect_view("delete_product_group")), + ] +else: + urlpatterns = [ + # product + re_path(r"^product$", views.product, name="product"), + re_path(r"^product/(?P\d+)$", views.view_product, + name="view_product"), + re_path(r"^product/(?P\d+)/components$", views.view_product_components, + name="view_product_components"), + re_path(r"^product/(?P\d+)/engagements$", views.view_engagements, + name="view_engagements"), + re_path( + r"^product/(?P\d+)/import_scan_results$", + dojo_engagement_views.ImportScanResultsView.as_view(), + name="import_scan_results_prod"), + re_path(r"^product/(?P\d+)/metrics$", views.view_product_metrics, + name="view_product_metrics"), + re_path(r"^product/(?P\d+)/async_burndown_metrics$", views.async_burndown_metrics, + name="async_burndown_metrics"), + re_path(r"^product/(?P\d+)/edit$", views.edit_product, + name="edit_product"), + re_path(r"^product/(?P\d+)/delete$", views.delete_product, + name="delete_product"), + re_path(r"^product/add", views.new_product, name="new_product"), + re_path(r"^product/(?P\d+)/new_engagement$", views.new_eng_for_app, + name="new_eng_for_prod"), + re_path(r"^product/(?P\d+)/new_technology$", views.new_tech_for_prod, + name="new_tech_for_prod"), + re_path(r"^technology/(?P\d+)/edit$", views.edit_technology, + name="edit_technology"), + re_path(r"^technology/(?P\d+)/delete$", views.delete_technology, + name="delete_technology"), + re_path(r"^product/(?P\d+)/new_engagement/cicd$", views.new_eng_for_app_cicd, + name="new_eng_for_prod_cicd"), + re_path(r"^product/(?P\d+)/add_meta_data$", views.add_meta_data, + name="add_meta_data"), + re_path(r"^product/(?P\d+)/edit_notifications$", views.edit_notifications, + name="edit_notifications"), + re_path(r"^product/(?P\d+)/edit_meta_data$", views.edit_meta_data, + name="edit_meta_data"), + re_path( + r"^product/(?P\d+)/ad_hoc_finding$", + views.AdHocFindingView.as_view(), + name="ad_hoc_finding"), + re_path(r"^product/(?P\d+)/engagement_presets$", views.engagement_presets, + name="engagement_presets"), + re_path(r"^product/(?P\d+)/engagement_presets/(?P\d+)/edit$", views.edit_engagement_presets, + name="edit_engagement_presets"), + re_path(r"^product/(?P\d+)/engagement_presets/add$", views.add_engagement_presets, + name="add_engagement_presets"), + re_path(r"^product/(?P\d+)/engagement_presets/(?P\d+)/delete$", views.delete_engagement_presets, + name="delete_engagement_presets"), + re_path(r"^product/(?P\d+)/add_member$", views.add_product_member, + name="add_product_member"), + re_path(r"^product/member/(?P\d+)/edit$", views.edit_product_member, + name="edit_product_member"), + re_path(r"^product/member/(?P\d+)/delete$", views.delete_product_member, + name="delete_product_member"), + re_path(r"^product/(?P\d+)/add_api_scan_configuration$", views.add_api_scan_configuration, + name="add_api_scan_configuration"), + re_path(r"^product/(?P\d+)/view_api_scan_configurations$", views.view_api_scan_configurations, + name="view_api_scan_configurations"), + re_path(r"^product/(?P\d+)/edit_api_scan_configuration/(?P\d+)$", + views.edit_api_scan_configuration, + name="edit_api_scan_configuration"), + re_path(r"^product/(?P\d+)/delete_api_scan_configuration/(?P\d+)$", + views.delete_api_scan_configuration, + name="delete_api_scan_configuration"), + re_path(r"^product/(?P\d+)/add_group$", views.add_product_group, + name="add_product_group"), + re_path(r"^product/group/(?P\d+)/edit$", views.edit_product_group, + name="edit_product_group"), + re_path(r"^product/group/(?P\d+)/delete$", views.delete_product_group, + name="delete_product_group"), + # Forward compatibility + re_path(r"^asset$", redirect_view("product")), + re_path(r"^asset/(?P\d+)$", redirect_view("view_product")), + re_path(r"^asset/(?P\d+)/components$", redirect_view("view_product_components")), + re_path(r"^asset/(?P\d+)/engagements$", redirect_view("view_engagements")), + re_path(r"^asset/(?P\d+)/import_scan_results$", redirect_view("import_scan_results_prod")), + re_path(r"^asset/(?P\d+)/metrics$", redirect_view("view_product_metrics")), + re_path(r"^asset/(?P\d+)/async_burndown_metrics$", redirect_view("async_burndown_metrics")), + re_path(r"^asset/(?P\d+)/edit$", redirect_view("edit_product")), + re_path(r"^asset/(?P\d+)/delete$", redirect_view("delete_product")), + re_path(r"^asset/add", redirect_view("new_product")), + re_path(r"^asset/(?P\d+)/new_engagement$", redirect_view("new_eng_for_prod")), + re_path(r"^asset/(?P\d+)/new_technology$", redirect_view("new_tech_for_prod")), + re_path(r"^asset/(?P\d+)/new_engagement/cicd$", redirect_view("new_eng_for_prod_cicd")), + re_path(r"^asset/(?P\d+)/add_meta_data$", redirect_view("add_meta_data")), + re_path(r"^asset/(?P\d+)/edit_notifications$", redirect_view("edit_notifications")), + re_path(r"^asset/(?P\d+)/edit_meta_data$", redirect_view("edit_meta_data")), + re_path(r"^asset/(?P\d+)/ad_hoc_finding$", redirect_view("ad_hoc_finding")), + re_path(r"^asset/(?P\d+)/engagement_presets$", redirect_view("engagement_presets")), + re_path(r"^asset/(?P\d+)/engagement_presets/(?P\d+)/edit$", redirect_view("edit_engagement_presets")), + re_path(r"^asset/(?P\d+)/engagement_presets/add$", redirect_view("add_engagement_presets")), + re_path(r"^asset/(?P\d+)/engagement_presets/(?P\d+)/delete$", + redirect_view("delete_engagement_presets")), + re_path(r"^asset/(?P\d+)/add_member$", redirect_view("add_product_member")), + re_path(r"^asset/member/(?P\d+)/edit$", redirect_view("edit_product_member")), + re_path(r"^asset/member/(?P\d+)/delete$", redirect_view("delete_product_member")), + re_path(r"^asset/(?P\d+)/add_api_scan_configuration$", redirect_view("add_api_scan_configuration")), + re_path(r"^asset/(?P\d+)/view_api_scan_configurations$", redirect_view("view_api_scan_configurations")), + re_path(r"^asset/(?P\d+)/edit_api_scan_configuration/(?P\d+)$", + redirect_view("edit_api_scan_configuration")), + re_path(r"^asset/(?P\d+)/delete_api_scan_configuration/(?P\d+)$", + redirect_view("delete_api_scan_configuration")), + re_path(r"^asset/(?P\d+)/add_group$", redirect_view("add_product_group")), + re_path(r"^asset/group/(?P\d+)/edit$", redirect_view("edit_product_group")), + re_path(r"^asset/group/(?P\d+)/delete$", redirect_view("delete_product_group")), + ] diff --git a/dojo/context_processors.py b/dojo/context_processors.py index a653eae3096..409851e2458 100644 --- a/dojo/context_processors.py +++ b/dojo/context_processors.py @@ -4,6 +4,7 @@ # import the settings file from django.conf import settings +from dojo.labels import get_labels from dojo.models import Alerts, System_Settings, UserAnnouncement @@ -74,3 +75,9 @@ def session_expiry_notification(request): return { "session_notify_time": notify_time, } + + +def labels(request): + return { + "labels": get_labels(), + } diff --git a/dojo/filters.py b/dojo/filters.py index 69ebe7cbae4..48dd5cfc824 100644 --- a/dojo/filters.py +++ b/dojo/filters.py @@ -54,6 +54,7 @@ ) from dojo.finding.queries import get_authorized_findings from dojo.finding_group.queries import get_authorized_finding_groups +from dojo.labels import get_labels from dojo.models import ( EFFORT_FOR_FIXING_CHOICES, ENGAGEMENT_STATUS_CHOICES, @@ -96,6 +97,8 @@ logger = logging.getLogger(__name__) +labels = get_labels() + BOOLEAN_CHOICES = (("false", "No"), ("true", "Yes")) EARLIEST_FINDING = None @@ -367,6 +370,10 @@ def get_tags_model_from_field_name(field): def get_tags_label_from_model(model): if model: + if model is Product_Type: + return labels.ORG_FILTERS_TAGS_LABEL + if model is Product: + return labels.ASSET_FILTERS_TAGS_LABEL return f"Tags ({model.__name__.title()})" return "Tags (Unknown)" @@ -512,7 +519,8 @@ class FindingTagFilter(DojoFilter): field_name="test__engagement__product__tags__name", to_field_name="name", queryset=Product.tags.tag_model.objects.all().order_by("name"), - help_text="Filter Products by the selected tags") + label=labels.ASSET_FILTERS_TAGS_FILTER_LABEL, + help_text=labels.ASSET_FILTERS_TAGS_FILTER_HELP) not_tags = ModelMultipleChoiceFilter( field_name="tags__name", @@ -537,9 +545,9 @@ class FindingTagFilter(DojoFilter): not_test__engagement__product__tags = ModelMultipleChoiceFilter( field_name="test__engagement__product__tags__name", to_field_name="name", - label="Product without tags", + label=labels.ASSET_FILTERS_ASSETS_WITHOUT_TAGS_LABEL, queryset=Product.tags.tag_model.objects.all().order_by("name"), - help_text="Search for tags on a Product that contain a given pattern, and exclude them", + help_text=labels.ASSET_FILTERS_ASSETS_WITHOUT_TAGS_HELP, exclude=True) def __init__(self, *args, **kwargs): @@ -578,15 +586,15 @@ class FindingTagStringFilter(FilterSet): lookup_expr="iexact", help_text="Search for tags on a Finding that are an exact match") test__engagement__product__tags_contains = CharFilter( - label="Product Tag Contains", + label=labels.ASSET_FILTERS_TAG_ASSET_CONTAINS_LABEL, field_name="test__engagement__product__tags__name", lookup_expr="icontains", - help_text="Search for tags on a Finding that contain a given pattern") + help_text=labels.ASSET_FILTERS_TAG_ASSET_CONTAINS_HELP) test__engagement__product__tags = CharFilter( - label="Product Tag", + label=labels.ASSET_FILTERS_TAG_ASSET_LABEL, field_name="test__engagement__product__tags__name", lookup_expr="iexact", - help_text="Search for tags on a Finding that are an exact match") + help_text=labels.ASSET_FILTERS_TAG_ASSET_HELP) not_tags_contains = CharFilter( label="Finding Tag Does Not Contain", @@ -625,16 +633,16 @@ class FindingTagStringFilter(FilterSet): help_text="Search for tags on a Engagement that are an exact match, and exclude them", exclude=True) not_test__engagement__product__tags_contains = CharFilter( - label="Product Tag Does Not Contain", + label=labels.ASSET_FILTERS_TAG_NOT_CONTAIN_LABEL, field_name="test__engagement__product__tags__name", lookup_expr="icontains", - help_text="Search for tags on a Product that contain a given pattern, and exclude them", + help_text=labels.ASSET_FILTERS_TAG_NOT_CONTAIN_HELP, exclude=True) not_test__engagement__product__tags = CharFilter( - label="Not Product Tag", + label=labels.ASSET_FILTERS_TAG_NOT_LABEL, field_name="test__engagement__product__tags__name", lookup_expr="iexact", - help_text="Search for tags on a Product that are an exact match, and exclude them", + help_text=labels.ASSET_FILTERS_TAG_NOT_HELP, exclude=True) def delete_tags_from_form(self, tag_list: list): @@ -919,32 +927,32 @@ class ComponentFilterWithoutObjectLookups(ProductComponentFilter): test__engagement__product__prod_type__name = CharFilter( field_name="test__engagement__product__prod_type__name", lookup_expr="iexact", - label="Product Type Name", - help_text="Search for Product Type names that are an exact match") + label=labels.ORG_FILTERS_NAME_LABEL, + help_text=labels.ORG_FILTERS_NAME_HELP) test__engagement__product__prod_type__name_contains = CharFilter( field_name="test__engagement__product__prod_type__name", lookup_expr="icontains", - label="Product Type Name Contains", - help_text="Search for Product Type names that contain a given pattern") + label=labels.ORG_FILTERS_NAME_CONTAINS_LABEL, + help_text=labels.ORG_FILTERS_NAME_CONTAINS_HELP) test__engagement__product__name = CharFilter( field_name="test__engagement__product__name", lookup_expr="iexact", - label="Product Name", - help_text="Search for Product names that are an exact match") + label=labels.ASSET_FILTERS_NAME_LABEL, + help_text=labels.ASSET_FILTERS_NAME_HELP) test__engagement__product__name_contains = CharFilter( field_name="test__engagement__product__name", lookup_expr="icontains", - label="Product Name Contains", - help_text="Search for Product names that contain a given pattern") + label=labels.ASSET_FILTERS_NAME_CONTAINS_LABEL, + help_text=labels.ASSET_FILTERS_NAME_CONTAINS_HELP) class ComponentFilter(ProductComponentFilter): test__engagement__product__prod_type = ModelMultipleChoiceFilter( queryset=Product_Type.objects.none(), - label="Product Type") + label=labels.ORG_FILTERS_LABEL) test__engagement__product = ModelMultipleChoiceFilter( queryset=Product.objects.none(), - label="Product") + label=labels.ASSET_FILTERS_LABEL) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -958,7 +966,7 @@ class EngagementDirectFilterHelper(FilterSet): name = CharFilter(lookup_expr="icontains", label="Engagement name contains") version = CharFilter(field_name="version", lookup_expr="icontains", label="Engagement version") test__version = CharFilter(field_name="test__version", lookup_expr="icontains", label="Test version") - product__name = CharFilter(lookup_expr="icontains", label="Product name contains") + product__name = CharFilter(lookup_expr="icontains", label=labels.ASSET_FILTERS_NAME_CONTAINS_LABEL) status = MultipleChoiceFilter(choices=ENGAGEMENT_STATUS_CHOICES, label="Status") tag = CharFilter(field_name="tags__name", lookup_expr="icontains", label="Tag name contains") not_tag = CharFilter(field_name="tags__name", lookup_expr="icontains", label="Not tag name contains", exclude=True) @@ -967,7 +975,7 @@ class EngagementDirectFilterHelper(FilterSet): target_end = DateRangeFilter() test__engagement__product__lifecycle = MultipleChoiceFilter( choices=Product.LIFECYCLE_CHOICES, - label="Product lifecycle", + label=labels.ASSET_LIFECYCLE_LABEL, null_label="Empty") o = OrderingFilter( # tuple-mapping retains order @@ -981,8 +989,8 @@ class EngagementDirectFilterHelper(FilterSet): field_labels={ "target_start": "Start date", "name": "Engagement", - "product__name": "Product Name", - "product__prod_type__name": "Product Type", + "product__name": labels.ASSET_FILTERS_NAME_LABEL, + "product__prod_type__name": labels.ORG_FILTERS_LABEL, "lead__first_name": "Lead", }, ) @@ -992,7 +1000,7 @@ class EngagementDirectFilter(EngagementDirectFilterHelper, DojoFilter): lead = ModelChoiceFilter(queryset=Dojo_User.objects.none(), label="Lead") product__prod_type = ModelMultipleChoiceFilter( queryset=Product_Type.objects.none(), - label="Product Type") + label=labels.ORG_FILTERS_LABEL) tags = ModelMultipleChoiceFilter( field_name="tags__name", to_field_name="name", @@ -1028,13 +1036,13 @@ class EngagementDirectFilterWithoutObjectLookups(EngagementDirectFilterHelper): product__prod_type__name = CharFilter( field_name="product__prod_type__name", lookup_expr="iexact", - label="Product Type Name", - help_text="Search for Product Type names that are an exact match") + label=labels.ORG_FILTERS_NAME_LABEL, + help_text=labels.ORG_FILTERS_NAME_HELP) product__prod_type__name_contains = CharFilter( field_name="product__prod_type__name", lookup_expr="icontains", - label="Product Type Name Contains", - help_text="Search for Product Type names that contain a given pattern") + label=labels.ORG_FILTERS_NAME_CONTAINS_LABEL, + help_text=labels.ORG_FILTERS_NAME_CONTAINS_HELP) class Meta: model = Engagement @@ -1042,7 +1050,7 @@ class Meta: class EngagementFilterHelper(FilterSet): - name = CharFilter(lookup_expr="icontains", label="Product name contains") + name = CharFilter(lookup_expr="icontains", label=labels.ASSET_FILTERS_NAME_CONTAINS_LABEL) tag = CharFilter(field_name="tags__name", lookup_expr="icontains", label="Tag name contains") not_tag = CharFilter(field_name="tags__name", lookup_expr="icontains", label="Not tag name contains", exclude=True) has_tags = BooleanFilter(field_name="tags", lookup_expr="isnull", exclude=True, label="Has tags") @@ -1051,7 +1059,7 @@ class EngagementFilterHelper(FilterSet): engagement__test__version = CharFilter(field_name="engagement__test__version", lookup_expr="icontains", label="Test version") engagement__product__lifecycle = MultipleChoiceFilter( choices=Product.LIFECYCLE_CHOICES, - label="Product lifecycle", + label=labels.ASSET_LIFECYCLE_LABEL, null_label="Empty") engagement__status = MultipleChoiceFilter( choices=ENGAGEMENT_STATUS_CHOICES, @@ -1063,8 +1071,8 @@ class EngagementFilterHelper(FilterSet): ("prod_type__name", "prod_type__name"), ), field_labels={ - "name": "Product Name", - "prod_type__name": "Product Type", + "name": labels.ASSET_FILTERS_NAME_LABEL, + "prod_type__name": labels.ORG_FILTERS_LABEL, }, ) @@ -1075,7 +1083,7 @@ class EngagementFilter(EngagementFilterHelper, DojoFilter): label="Lead") prod_type = ModelMultipleChoiceFilter( queryset=Product_Type.objects.none(), - label="Product Type") + label=labels.ORG_FILTERS_LABEL) tags = ModelMultipleChoiceFilter( field_name="tags__name", to_field_name="name", @@ -1091,6 +1099,8 @@ def __init__(self, *args, **kwargs): self.form.fields["prod_type"].queryset = get_authorized_product_types(Permissions.Product_Type_View) self.form.fields["engagement__lead"].queryset = get_authorized_users(Permissions.Product_Type_View) \ .filter(engagement__lead__isnull=False).distinct() + self.form.fields["tags"].help_text = labels.ASSET_FILTERS_TAGS_HELP + self.form.fields["not_tags"].help_text = labels.ASSET_FILTERS_NOT_TAGS_HELP class Meta: model = Product @@ -1137,13 +1147,13 @@ class EngagementFilterWithoutObjectLookups(EngagementFilterHelper): prod_type__name = CharFilter( field_name="prod_type__name", lookup_expr="iexact", - label="Product Type Name", - help_text="Search for Product Type names that are an exact match") + label=labels.ORG_FILTERS_LABEL, + help_text=labels.ORG_FILTERS_LABEL_HELP) prod_type__name_contains = CharFilter( field_name="prod_type__name", lookup_expr="icontains", - label="Product Type Name Contains", - help_text="Search for Product Type names that contain a given pattern") + label=labels.ORG_FILTERS_NAME_CONTAINS_LABEL, + help_text=labels.ORG_FILTERS_NAME_CONTAINS_HELP) class Meta: model = Product @@ -1223,17 +1233,17 @@ class ApiEngagementFilter(DojoFilter): product__tags = CharFieldInFilter( field_name="product__tags__name", lookup_expr="in", - help_text="Comma separated list of exact tags present on product (uses OR for multiple values)") + help_text=labels.ASSET_FILTERS_CSV_TAGS_OR_HELP) product__tags__and = CharFieldFilterANDExpression( field_name="product__tags__name", - help_text="Comma separated list of exact tags to match with an AND expression present on product") + help_text=labels.ASSET_FILTERS_CSV_TAGS_AND_HELP) not_tag = CharFilter(field_name="tags__name", lookup_expr="icontains", help_text="Not Tag name contains", exclude="True") not_tags = CharFieldInFilter(field_name="tags__name", lookup_expr="in", help_text="Comma separated list of exact tags not present on model", exclude="True") not_product__tags = CharFieldInFilter(field_name="product__tags__name", lookup_expr="in", - help_text="Comma separated list of exact tags not present on product", + help_text=labels.ASSET_FILTERS_CSV_TAGS_NOT_HELP, exclude="True") has_tags = BooleanFilter(field_name="tags", lookup_expr="isnull", exclude=True, label="Has tags") @@ -1264,8 +1274,8 @@ class Meta: class ProductFilterHelper(FilterSet): - name = CharFilter(lookup_expr="icontains", label="Product Name") - name_exact = CharFilter(field_name="name", lookup_expr="iexact", label="Exact Product Name") + name = CharFilter(lookup_expr="icontains", label=labels.ASSET_FILTERS_NAME_LABEL) + name_exact = CharFilter(field_name="name", lookup_expr="iexact", label=labels.ASSET_FILTERS_NAME_EXACT_LABEL) business_criticality = MultipleChoiceFilter(choices=Product.BUSINESS_CRITICALITY_CHOICES, null_label="Empty") platform = MultipleChoiceFilter(choices=Product.PLATFORM_CHOICES, null_label="Empty") lifecycle = MultipleChoiceFilter(choices=Product.LIFECYCLE_CHOICES, null_label="Empty") @@ -1291,9 +1301,9 @@ class ProductFilterHelper(FilterSet): ("findings_count", "findings_count"), ), field_labels={ - "name": "Product Name", - "name_exact": "Exact Product Name", - "prod_type__name": "Product Type", + "name": labels.ASSET_FILTERS_NAME_LABEL, + "name_exact": labels.ASSET_FILTERS_NAME_EXACT_LABEL, + "prod_type__name": labels.ORG_FILTERS_LABEL, "business_criticality": "Business Criticality", "platform": "Platform ", "lifecycle": "Lifecycle ", @@ -1308,7 +1318,7 @@ class ProductFilterHelper(FilterSet): class ProductFilter(ProductFilterHelper, DojoFilter): prod_type = ModelMultipleChoiceFilter( queryset=Product_Type.objects.none(), - label="Product Type") + label=labels.ORG_FILTERS_LABEL) tags = ModelMultipleChoiceFilter( field_name="tags__name", to_field_name="name", @@ -1325,6 +1335,8 @@ def __init__(self, *args, **kwargs): self.user = kwargs.pop("user") super().__init__(*args, **kwargs) self.form.fields["prod_type"].queryset = get_authorized_product_types(Permissions.Product_Type_View) + self.form.fields["tags"].help_text = labels.ASSET_FILTERS_TAGS_HELP + self.form.fields["not_tags"].help_text = labels.ASSET_FILTERS_NOT_TAGS_HELP class Meta: model = Product @@ -1339,13 +1351,13 @@ class ProductFilterWithoutObjectLookups(ProductFilterHelper): prod_type__name = CharFilter( field_name="prod_type__name", lookup_expr="iexact", - label="Product Type Name", - help_text="Search for Product Type names that are an exact match") + label=labels.ORG_FILTERS_NAME_LABEL, + help_text=labels.ORG_FILTERS_NAME_HELP) prod_type__name_contains = CharFilter( field_name="prod_type__name", lookup_expr="icontains", - label="Product Type Name Contains", - help_text="Search for Product Type names that contain a given pattern") + label=labels.ORG_FILTERS_NAME_CONTAINS_LABEL, + help_text=labels.ORG_FILTERS_NAME_CONTAINS_HELP) def __init__(self, *args, **kwargs): kwargs.pop("user", None) @@ -1408,7 +1420,7 @@ class ApiProductFilter(DojoFilter): help_text="Comma separated list of exact tags to match with an AND expression") not_tag = CharFilter(field_name="tags__name", lookup_expr="icontains", help_text="Not Tag name contains", exclude="True") not_tags = CharFieldInFilter(field_name="tags__name", lookup_expr="in", - help_text="Comma separated list of exact tags not present on product", exclude="True") + help_text=labels.ASSET_FILTERS_CSV_TAGS_NOT_HELP, exclude="True") has_tags = BooleanFilter(field_name="tags", lookup_expr="isnull", exclude=True, label="Has tags") outside_of_sla = extend_schema_field(OpenApiTypes.NUMBER)(ProductSLAFilter()) @@ -1491,10 +1503,10 @@ class ApiFindingFilter(DojoFilter): steps_to_reproduce = CharFilter(lookup_expr="icontains") unique_id_from_tool = CharFilter(lookup_expr="icontains") title = CharFilter(lookup_expr="icontains") - product_name = CharFilter(lookup_expr="engagement__product__name__iexact", field_name="test", label="exact product name") - product_name_contains = CharFilter(lookup_expr="engagement__product__name__icontains", field_name="test", label="exact product name") + product_name = CharFilter(lookup_expr="engagement__product__name__iexact", field_name="test", label=labels.ASSET_FILTERS_NAME_EXACT_LABEL) + product_name_contains = CharFilter(lookup_expr="engagement__product__name__icontains", field_name="test", label=labels.ASSET_FILTERS_NAME_CONTAINS_LABEL) product_lifecycle = CharFilter(method=custom_filter, lookup_expr="engagement__product__lifecycle", - field_name="test__engagement__product__lifecycle", label="Comma separated list of exact product lifecycles") + field_name="test__engagement__product__lifecycle", label=labels.ASSET_FILTERS_CSV_LIFECYCLES_LABEL) # DateRangeFilter created = DateRangeFilter() date = DateRangeFilter() @@ -1574,10 +1586,10 @@ class ApiFindingFilter(DojoFilter): test__engagement__product__tags = CharFieldInFilter( field_name="test__engagement__product__tags__name", lookup_expr="in", - help_text="Comma separated list of exact tags present on product (uses OR for multiple values)") + help_text=labels.ASSET_FILTERS_CSV_TAGS_OR_HELP) test__engagement__product__tags__and = CharFieldFilterANDExpression( field_name="test__engagement__product__tags__name", - help_text="Comma separated list of exact tags to match with an AND expression present on product") + help_text=labels.ASSET_FILTERS_CSV_TAGS_AND_HELP) not_tag = CharFilter(field_name="tags__name", lookup_expr="icontains", help_text="Not Tag name contains", exclude="True") not_tags = CharFieldInFilter(field_name="tags__name", lookup_expr="in", help_text="Comma separated list of exact tags not present on model", exclude="True") @@ -1588,7 +1600,7 @@ class ApiFindingFilter(DojoFilter): not_test__engagement__product__tags = CharFieldInFilter( field_name="test__engagement__product__tags__name", lookup_expr="in", - help_text="Comma separated list of exact tags not present on product", + help_text=labels.ASSET_FILTERS_CSV_TAGS_NOT_HELP, exclude="True") has_tags = BooleanFilter(field_name="tags", lookup_expr="isnull", exclude=True, label="Has tags") outside_of_sla = extend_schema_field(OpenApiTypes.NUMBER)(FindingSLAFilter()) @@ -1697,7 +1709,7 @@ class FindingFilterHelper(FilterSet): status = FindingStatusFilter(label="Status") test__engagement__product__lifecycle = MultipleChoiceFilter( choices=Product.LIFECYCLE_CHOICES, - label="Product lifecycle") + label=labels.ASSET_LIFECYCLE_LABEL) has_component = BooleanFilter( field_name="component_name", @@ -1787,7 +1799,7 @@ class FindingFilterHelper(FilterSet): "mitigated": "Mitigated Date", "fix_available": "Fix Available", "title": "Finding Name", - "test__engagement__product__name": "Product Name", + "test__engagement__product__name": labels.ASSET_FILTERS_NAME_LABEL, "epss_score": "EPSS Score", "epss_percentile": "EPSS Percentile", "known_exploited": "Known Exploited", @@ -1853,23 +1865,23 @@ class FindingFilterWithoutObjectLookups(FindingFilterHelper, FindingTagStringFil test__engagement__product__prod_type__name = CharFilter( field_name="test__engagement__product__prod_type__name", lookup_expr="iexact", - label="Product Type Name", - help_text="Search for Product Type names that are an exact match") + label=labels.ORG_FILTERS_NAME_LABEL, + help_text=labels.ORG_FILTERS_NAME_HELP) test__engagement__product__prod_type__name_contains = CharFilter( field_name="test__engagement__product__prod_type__name", lookup_expr="icontains", - label="Product Type Name Contains", - help_text="Search for Product Type names that contain a given pattern") + label=labels.ORG_FILTERS_NAME_CONTAINS_LABEL, + help_text=labels.ORG_FILTERS_NAME_CONTAINS_HELP) test__engagement__product__name = CharFilter( field_name="test__engagement__product__name", lookup_expr="iexact", - label="Product Name", - help_text="Search for Product names that are an exact match") + label=labels.ASSET_FILTERS_NAME_LABEL, + help_text=labels.ASSET_FILTERS_NAME_HELP) test__engagement__product__name_contains = CharFilter( field_name="test__engagement__product__name", lookup_expr="icontains", - label="Product name Contains", - help_text="Search for Product Typ names that contain a given pattern") + label=labels.ASSET_FILTERS_NAME_CONTAINS_LABEL, + help_text=labels.ASSET_FILTERS_NAME_CONTAINS_HELP) test__engagement__name = CharFilter( field_name="test__engagement__name", lookup_expr="iexact", @@ -1942,10 +1954,10 @@ class FindingFilter(FindingFilterHelper, FindingTagFilter): reviewers = ModelMultipleChoiceFilter(queryset=Dojo_User.objects.none()) test__engagement__product__prod_type = ModelMultipleChoiceFilter( queryset=Product_Type.objects.none(), - label="Product Type") + label=labels.ORG_FILTERS_LABEL) test__engagement__product = ModelMultipleChoiceFilter( queryset=Product.objects.none(), - label="Product") + label=labels.ASSET_FILTERS_LABEL) test__engagement = ModelMultipleChoiceFilter( queryset=Engagement.objects.none(), label="Engagement") @@ -2021,7 +2033,7 @@ class FindingGroupsFilter(FilterSet): label="Min Severity", ) engagement = ModelMultipleChoiceFilter(queryset=Engagement.objects.none(), label="Engagement") - product = ModelMultipleChoiceFilter(queryset=Product.objects.none(), label="Product") + product = ModelMultipleChoiceFilter(queryset=Product.objects.none(), label=labels.ASSET_LABEL) class Meta: model = Finding @@ -2214,7 +2226,7 @@ class Meta: field_name="test__engagement__product__tags__name", to_field_name="name", exclude=True, - label="Product without tags", + label=labels.ASSET_FILTERS_WITHOUT_TAGS_LABEL, queryset=Product.tags.tag_model.objects.all().order_by("name"), # label='tags', # doesn't work with tagulous, need to set in __init__ below ) @@ -2326,7 +2338,7 @@ class MetricsEndpointFilterHelper(FilterSet): class MetricsEndpointFilter(MetricsEndpointFilterHelper): finding__test__engagement__product__prod_type = ModelMultipleChoiceFilter( queryset=Product_Type.objects.none(), - label="Product Type") + label=labels.ORG_FILTERS_LABEL) finding__test__engagement = ModelMultipleChoiceFilter( queryset=Engagement.objects.none(), label="Engagement") @@ -2353,7 +2365,7 @@ class MetricsEndpointFilter(MetricsEndpointFilterHelper): finding__test__engagement__product__tags = ModelMultipleChoiceFilter( field_name="finding__test__engagement__product__tags__name", to_field_name="name", - label="Product tags", + label=labels.ASSET_FILTERS_TAGS_ASSET_LABEL, queryset=Product.tags.tag_model.objects.all().order_by("name")) not_endpoint__tags = ModelMultipleChoiceFilter( field_name="endpoint__tags__name", @@ -2383,7 +2395,7 @@ class MetricsEndpointFilter(MetricsEndpointFilterHelper): field_name="finding__test__engagement__product__tags__name", to_field_name="name", exclude=True, - label="Product without tags", + label=labels.ASSET_FILTERS_WITHOUT_TAGS_LABEL, queryset=Product.tags.tag_model.objects.all().order_by("name")) def __init__(self, *args, **kwargs): @@ -2419,13 +2431,13 @@ class MetricsEndpointFilterWithoutObjectLookups(MetricsEndpointFilterHelper, Fin finding__test__engagement__product__prod_type = CharFilter( field_name="finding__test__engagement__product__prod_type", lookup_expr="iexact", - label="Product Type Name", - help_text="Search for Product Type names that are an exact match") + label=labels.ORG_FILTERS_NAME_LABEL, + help_text=labels.ORG_FILTERS_NAME_HELP) finding__test__engagement__product__prod_type_contains = CharFilter( field_name="finding__test__engagement__product__prod_type", lookup_expr="icontains", - label="Product Type Name Contains", - help_text="Search for Product Type names that contain a given pattern") + label=labels.ORG_FILTERS_NAME_CONTAINS_LABEL, + help_text=labels.ORG_FILTERS_NAME_CONTAINS_HELP) finding__test__engagement = CharFilter( field_name="finding__test__engagement", lookup_expr="iexact", @@ -2477,15 +2489,15 @@ class MetricsEndpointFilterWithoutObjectLookups(MetricsEndpointFilterHelper, Fin lookup_expr="iexact", help_text="Search for tags on a Finding that are an exact match") finding__test__engagement__product__tags_contains = CharFilter( - label="Product Tag Contains", + label=labels.ASSET_FILTERS_TAG_ASSET_CONTAINS_LABEL, field_name="finding__test__engagement__product__tags__name", lookup_expr="icontains", - help_text="Search for tags on a Finding that contain a given pattern") + help_text=labels.ASSET_FILTERS_TAG_ASSET_CONTAINS_HELP) finding__test__engagement__product__tags = CharFilter( - label="Product Tag", + label=labels.ASSET_FILTERS_TAG_ASSET_LABEL, field_name="finding__test__engagement__product__tags__name", lookup_expr="iexact", - help_text="Search for tags on a Finding that are an exact match") + help_text=labels.ASSET_FILTERS_TAG_ASSET_HELP) not_endpoint__tags_contains = CharFilter( label="Endpoint Tag Does Not Contain", @@ -2536,16 +2548,16 @@ class MetricsEndpointFilterWithoutObjectLookups(MetricsEndpointFilterHelper, Fin help_text="Search for tags on a Engagement that are an exact match, and exclude them", exclude=True) not_finding__test__engagement__product__tags_contains = CharFilter( - label="Product Tag Does Not Contain", + label=labels.ASSET_FILTERS_TAG_NOT_CONTAIN_LABEL, field_name="finding__test__engagement__product__tags__name", lookup_expr="icontains", - help_text="Search for tags on a Product that contain a given pattern, and exclude them", + help_text=labels.ASSET_FILTERS_TAG_NOT_CONTAIN_HELP, exclude=True) not_finding__test__engagement__product__tags = CharFilter( - label="Not Product Tag", + label=labels.ASSET_FILTERS_TAG_NOT_LABEL, field_name="finding__test__engagement__product__tags__name", lookup_expr="iexact", - help_text="Search for tags on a Product that are an exact match, and exclude them", + help_text=labels.ASSET_FILTERS_TAG_NOT_HELP, exclude=True) def __init__(self, *args, **kwargs): @@ -2590,7 +2602,7 @@ class EndpointFilterHelper(FilterSet): class EndpointFilter(EndpointFilterHelper, DojoFilter): product = ModelMultipleChoiceFilter( queryset=Product.objects.none(), - label="Product") + label=labels.ASSET_FILTERS_LABEL) tags = ModelMultipleChoiceFilter( field_name="tags__name", to_field_name="name", @@ -2614,7 +2626,7 @@ class EndpointFilter(EndpointFilterHelper, DojoFilter): findings__test__engagement__product__tags = ModelMultipleChoiceFilter( field_name="findings__test__engagement__product__tags__name", to_field_name="name", - label="Product Tags", + label=labels.ASSET_FILTERS_TAGS_ASSET_LABEL, queryset=Product.tags.tag_model.objects.all().order_by("name")) not_tags = ModelMultipleChoiceFilter( field_name="tags__name", @@ -2643,7 +2655,7 @@ class EndpointFilter(EndpointFilterHelper, DojoFilter): not_findings__test__engagement__product__tags = ModelMultipleChoiceFilter( field_name="findings__test__engagement__product__tags__name", to_field_name="name", - label="Not Product Tags", + label=labels.ASSET_FILTERS_NOT_TAGS_ASSET_LABEL, exclude=True, queryset=Product.tags.tag_model.objects.all().order_by("name")) @@ -2669,13 +2681,13 @@ class EndpointFilterWithoutObjectLookups(EndpointFilterHelper): product__name = CharFilter( field_name="product__name", lookup_expr="iexact", - label="Product Name", - help_text="Search for Product names that are an exact match") + label=labels.ASSET_FILTERS_NAME_LABEL, + help_text=labels.ASSET_FILTERS_NAME_HELP) product__name_contains = CharFilter( field_name="product__name", lookup_expr="icontains", - label="Product Name Contains", - help_text="Search for Product names that contain a given pattern") + label=labels.ASSET_FILTERS_NAME_CONTAINS_LABEL, + help_text=labels.ASSET_FILTERS_NAME_CONTAINS_HELP) tags_contains = CharFilter( label="Endpoint Tag Contains", @@ -2718,15 +2730,15 @@ class EndpointFilterWithoutObjectLookups(EndpointFilterHelper): lookup_expr="iexact", help_text="Search for tags on a Finding that are an exact match") findings__test__engagement__product__tags_contains = CharFilter( - label="Product Tag Contains", + label=labels.ASSET_FILTERS_TAG_ASSET_CONTAINS_LABEL, field_name="findings__test__engagement__product__tags__name", lookup_expr="icontains", - help_text="Search for tags on a Finding that contain a given pattern") + help_text=labels.ASSET_FILTERS_TAG_ASSET_CONTAINS_HELP) findings__test__engagement__product__tags = CharFilter( - label="Product Tag", + label=labels.ASSET_FILTERS_TAG_ASSET_LABEL, field_name="findings__test__engagement__product__tags__name", lookup_expr="iexact", - help_text="Search for tags on a Finding that are an exact match") + help_text=labels.ASSET_FILTERS_TAG_ASSET_HELP) not_tags_contains = CharFilter( label="Endpoint Tag Does Not Contain", @@ -2777,16 +2789,16 @@ class EndpointFilterWithoutObjectLookups(EndpointFilterHelper): help_text="Search for tags on a Engagement that are an exact match, and exclude them", exclude=True) not_findings__test__engagement__product__tags_contains = CharFilter( - label="Product Tag Does Not Contain", + label=labels.ASSET_FILTERS_TAG_NOT_CONTAIN_LABEL, field_name="findings__test__engagement__product__tags__name", lookup_expr="icontains", - help_text="Search for tags on a Product that contain a given pattern, and exclude them", + help_text=labels.ASSET_FILTERS_TAG_NOT_CONTAIN_HELP, exclude=True) not_findings__test__engagement__product__tags = CharFilter( - label="Not Product Tag", + label=labels.ASSET_FILTERS_TAG_NOT_LABEL, field_name="findings__test__engagement__product__tags__name", lookup_expr="iexact", - help_text="Search for tags on a Product that are an exact match, and exclude them", + help_text=labels.ASSET_FILTERS_TAG_NOT_HELP, exclude=True) def __init__(self, *args, **kwargs): @@ -2984,10 +2996,10 @@ class ApiTestFilter(DojoFilter): engagement__product__tags = CharFieldInFilter( field_name="engagement__product__tags__name", lookup_expr="in", - help_text="Comma separated list of exact tags present on product (uses OR for multiple values)") + help_text=labels.ASSET_FILTERS_CSV_TAGS_OR_HELP) engagement__product__tags__and = CharFieldFilterANDExpression( field_name="engagement__product__tags__name", - help_text="Comma separated list of exact tags to match with an AND expression present on product") + help_text=labels.ASSET_FILTERS_CSV_TAGS_AND_HELP) not_tag = CharFilter(field_name="tags__name", lookup_expr="icontains", help_text="Not Tag name contains", exclude="True") not_tags = CharFieldInFilter(field_name="tags__name", lookup_expr="in", @@ -2997,7 +3009,7 @@ class ApiTestFilter(DojoFilter): exclude="True") not_engagement__product__tags = CharFieldInFilter(field_name="engagement__product__tags__name", lookup_expr="in", - help_text="Comma separated list of exact tags not present on product", + help_text=labels.ASSET_FILTERS_CSV_TAGS_NOT_HELP, exclude="True") has_tags = BooleanFilter(field_name="tags", lookup_expr="isnull", exclude=True, label="Has tags") @@ -3151,11 +3163,11 @@ def qs(self): class ReportFindingFilter(ReportFindingFilterHelper, FindingTagFilter): test__engagement__product = ModelMultipleChoiceFilter( - queryset=Product.objects.none(), label="Product") + queryset=Product.objects.none(), label=labels.ASSET_FILTERS_LABEL) test__engagement__product__prod_type = ModelMultipleChoiceFilter( queryset=Product_Type.objects.none(), - label="Product Type") - test__engagement__product__lifecycle = MultipleChoiceFilter(choices=Product.LIFECYCLE_CHOICES, label="Product Lifecycle") + label=labels.ORG_FILTERS_LABEL) + test__engagement__product__lifecycle = MultipleChoiceFilter(choices=Product.LIFECYCLE_CHOICES, label=labels.ASSET_LIFECYCLE_LABEL) test__engagement = ModelMultipleChoiceFilter(queryset=Engagement.objects.none(), label="Engagement") duplicate_finding = ModelChoiceFilter(queryset=Finding.objects.filter(original_finding__isnull=False).distinct()) @@ -3266,23 +3278,23 @@ class ReportFindingFilterWithoutObjectLookups(ReportFindingFilterHelper, Finding test__engagement__product__prod_type__name = CharFilter( field_name="test__engagement__product__prod_type__name", lookup_expr="iexact", - label="Product Type Name", - help_text="Search for Product Type names that are an exact match") + label=labels.ORG_FILTERS_NAME_LABEL, + help_text=labels.ORG_FILTERS_NAME_HELP) test__engagement__product__prod_type__name_contains = CharFilter( field_name="test__engagement__product__prod_type__name", lookup_expr="icontains", - label="Product Type Name Contains", - help_text="Search for Product Type names that contain a given pattern") + label=labels.ORG_FILTERS_NAME_CONTAINS_LABEL, + help_text=labels.ORG_FILTERS_NAME_CONTAINS_HELP) test__engagement__product__name = CharFilter( field_name="test__engagement__product__name", lookup_expr="iexact", - label="Product Name", - help_text="Search for Product names that are an exact match") + label=labels.ASSET_FILTERS_NAME_LABEL, + help_text=labels.ASSET_FILTERS_NAME_HELP) test__engagement__product__name_contains = CharFilter( field_name="test__engagement__product__name", lookup_expr="icontains", - label="Product name Contains", - help_text="Search for Product names that contain a given pattern") + label=labels.ASSET_FILTERS_NAME_CONTAINS_LABEL, + help_text=labels.ASSET_FILTERS_NAME_CONTAINS_HELP) test__engagement__name = CharFilter( field_name="test__engagement__name", lookup_expr="iexact", diff --git a/dojo/forms.py b/dojo/forms.py index 25e8d279a1f..b422a4ca475 100644 --- a/dojo/forms.py +++ b/dojo/forms.py @@ -37,6 +37,7 @@ from dojo.engagement.queries import get_authorized_engagements from dojo.finding.queries import get_authorized_findings from dojo.group.queries import get_authorized_groups, get_group_member_roles +from dojo.labels import get_labels from dojo.models import ( EFFORT_FOR_FIXING_CHOICES, SEVERITY_CHOICES, @@ -118,6 +119,8 @@ logger = logging.getLogger(__name__) +labels = get_labels() + RE_DATE = re.compile(r"(\d{4})-(\d\d?)-(\d\d?)$") FINDING_STATUS = (("verified", "Verified"), @@ -244,6 +247,11 @@ class Product_TypeForm(forms.ModelForm): description = forms.CharField(widget=forms.Textarea(attrs={}), required=False) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["critical_product"].label = labels.ORG_CRITICAL_PRODUCT_LABEL + self.fields["key_product"].label = labels.ORG_KEY_PRODUCT_LABEL + class Meta: model = Product_Type fields = ["name", "description", "critical_product", "key_product"] @@ -280,6 +288,7 @@ def __init__(self, *args, **kwargs): self.fields["users"].queryset = Dojo_User.objects.exclude( Q(is_superuser=True) | Q(id__in=current_members)).exclude(is_active=False).order_by("first_name", "last_name") + self.fields["product_type"].label = labels.ORG_LABEL self.fields["product_type"].disabled = True class Meta: @@ -288,7 +297,8 @@ class Meta: class Add_Product_Type_Member_UserForm(forms.ModelForm): - product_types = forms.ModelMultipleChoiceField(queryset=Product_Type.objects.none(), required=True, label="Product Types") + product_types = forms.ModelMultipleChoiceField(queryset=Product_Type.objects.none(), required=True, + label=labels.ORG_PLURAL_LABEL) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -306,6 +316,7 @@ class Delete_Product_Type_MemberForm(Edit_Product_Type_MemberForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["role"].disabled = True + self.fields["product_type"].label = labels.ORG_LABEL class Test_TypeForm(forms.ModelForm): @@ -331,7 +342,7 @@ class ProductForm(forms.ModelForm): description = forms.CharField(widget=forms.Textarea(attrs={}), required=True) - prod_type = forms.ModelChoiceField(label="Product Type", + prod_type = forms.ModelChoiceField(label=labels.ORG_LABEL, queryset=Product_Type.objects.none(), required=True) @@ -340,13 +351,16 @@ class ProductForm(forms.ModelForm): required=True, initial="Default") - product_manager = forms.ModelChoiceField(queryset=Dojo_User.objects.exclude(is_active=False).order_by("first_name", "last_name"), required=False) + product_manager = forms.ModelChoiceField(label=labels.ASSET_MANAGER_LABEL, + queryset=Dojo_User.objects.exclude(is_active=False).order_by("first_name", "last_name"), required=False) technical_contact = forms.ModelChoiceField(queryset=Dojo_User.objects.exclude(is_active=False).order_by("first_name", "last_name"), required=False) team_manager = forms.ModelChoiceField(queryset=Dojo_User.objects.exclude(is_active=False).order_by("first_name", "last_name"), required=False) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["prod_type"].queryset = get_authorized_product_types(Permissions.Product_Type_Add_Product) + self.fields["enable_product_tag_inheritance"].label = labels.ASSET_TAG_INHERITANCE_ENABLE_LABEL + self.fields["enable_product_tag_inheritance"].help_text = labels.ASSET_TAG_INHERITANCE_ENABLE_HELP if prod_type_id := kwargs.get("instance", Product()).prod_type_id: # we are editing existing instance self.fields["prod_type"].queryset |= Product_Type.objects.filter(pk=prod_type_id) # even if user does not have permission for any other ProdType we need to add at least assign ProdType to make form submittable (otherwise empty list was here which generated invalid form) @@ -415,6 +429,7 @@ class Edit_Product_MemberForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["product"].disabled = True + self.fields["product"].label = labels.ASSET_LABEL self.fields["user"].queryset = Dojo_User.objects.order_by("first_name", "last_name") self.fields["user"].disabled = True @@ -429,6 +444,7 @@ class Add_Product_MemberForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["product"].disabled = True + self.fields["product"].label = labels.ASSET_LABEL current_members = Product_Member.objects.filter(product=self.initial["product"]).values_list("user", flat=True) self.fields["users"].queryset = Dojo_User.objects.exclude( Q(is_superuser=True) @@ -440,7 +456,8 @@ class Meta: class Add_Product_Member_UserForm(forms.ModelForm): - products = forms.ModelMultipleChoiceField(queryset=Product.objects.none(), required=True, label="Products") + products = forms.ModelMultipleChoiceField(queryset=Product.objects.none(), required=True, + label=labels.ASSET_PLURAL_LABEL) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -565,11 +582,8 @@ class ImportScanForm(forms.Form): label="Close old findings", required=False, initial=False) - close_old_findings_product_scope = forms.BooleanField(help_text="Old findings no longer present in the new report get closed as mitigated when importing. " - "If service has been set, only the findings for this service will be closed; " - "if no service is set, only findings without a service will be closed. " - "This affects findings within the same product.", - label="Close old findings within this product", + close_old_findings_product_scope = forms.BooleanField(help_text=labels.ASSET_FINDINGS_CLOSE_HELP, + label=labels.ASSET_FINDINGS_CLOSE_LABEL, required=False, initial=False) apply_tags_to_findings = forms.BooleanField( @@ -1003,9 +1017,9 @@ class EngForm(forms.ModelForm): )) description = forms.CharField(widget=forms.Textarea(attrs={}), required=False, help_text="Description of the engagement and details regarding the engagement.") - product = forms.ModelChoiceField(label="Product", - queryset=Product.objects.none(), - required=True) + product = forms.ModelChoiceField(label=labels.ASSET_LABEL, + queryset=Product.objects.none(), + required=True) target_start = forms.DateField(widget=forms.TextInput( attrs={"class": "datepicker", "autocomplete": "off"})) target_end = forms.DateField(widget=forms.TextInput( @@ -1777,8 +1791,8 @@ class AddEndpointForm(forms.Form): "Each must be valid.", widget=forms.widgets.Textarea(attrs={"rows": "15", "cols": "400"})) product = forms.CharField(required=True, - widget=forms.widgets.HiddenInput(), help_text="The product this endpoint should be " - "associated with.") + label=labels.ASSET_LABEL, help_text=labels.ASSET_ENDPOINT_HELP, + widget=forms.widgets.HiddenInput()) tags = TagField(required=False, help_text="Add tags that help describe this endpoint. " "Choose from the list or add new tags. Press Enter key to add.") @@ -1788,7 +1802,10 @@ def __init__(self, *args, **kwargs): if "product" in kwargs: product = kwargs.pop("product") super().__init__(*args, **kwargs) - self.fields["product"] = forms.ModelChoiceField(queryset=get_authorized_products(Permissions.Endpoint_Add)) + self.fields["product"] = forms.ModelChoiceField( + queryset=get_authorized_products(Permissions.Endpoint_Add), + label=labels.ASSET_LABEL, + help_text=labels.ASSET_ENDPOINT_HELP) if product is not None: self.fields["product"].initial = product.id @@ -2195,6 +2212,7 @@ class Add_Product_GroupForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["product"].disabled = True + self.fields["product"].label = labels.ASSET_LABEL current_groups = Product_Group.objects.filter(product=self.initial["product"]).values_list("group", flat=True) authorized_groups = get_authorized_groups(Permissions.Group_View) authorized_groups = authorized_groups.exclude(id__in=current_groups) @@ -2206,7 +2224,8 @@ class Meta: class Add_Product_Group_GroupForm(forms.ModelForm): - products = forms.ModelMultipleChoiceField(queryset=Product.objects.none(), required=True, label="Products") + products = forms.ModelMultipleChoiceField(queryset=Product.objects.none(), required=True, + label=labels.ASSET_PLURAL_LABEL) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -2225,6 +2244,7 @@ class Edit_Product_Group_Form(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["product"].disabled = True + self.fields["product"].label = labels.ASSET_LABEL self.fields["group"].disabled = True class Meta: @@ -2248,6 +2268,7 @@ def __init__(self, *args, **kwargs): authorized_groups = authorized_groups.exclude(id__in=current_groups) self.fields["groups"].queryset = authorized_groups self.fields["product_type"].disabled = True + self.fields["product_type"].label = labels.ORG_LABEL class Meta: model = Product_Type_Group @@ -2255,7 +2276,8 @@ class Meta: class Add_Product_Type_Group_GroupForm(forms.ModelForm): - product_types = forms.ModelMultipleChoiceField(queryset=Product_Type.objects.none(), required=True, label="Product Types") + product_types = forms.ModelMultipleChoiceField(queryset=Product_Type.objects.none(), required=True, + label=labels.ORG_PLURAL_LABEL) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -2274,6 +2296,7 @@ class Edit_Product_Type_Group_Form(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["product_type"].disabled = True + self.fields["product_type"].label = labels.ORG_LABEL self.fields["group"].disabled = True class Meta: @@ -2414,6 +2437,7 @@ class Meta: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) current_user = get_current_user() + self.fields["role"].help_text = labels.ASSET_GLOBAL_ROLE_HELP if not current_user.is_superuser: self.fields["role"].disabled = True @@ -2433,6 +2457,7 @@ class ProductCountsFormBase(forms.Form): class ProductTypeCountsForm(ProductCountsFormBase): product_type = forms.ModelChoiceField(required=True, queryset=Product_Type.objects.none(), + label=labels.ORG_LABEL, error_messages={ "required": "*"}) @@ -2444,6 +2469,7 @@ def __init__(self, *args, **kwargs): class ProductTagCountsForm(ProductCountsFormBase): product_tag = forms.ModelChoiceField(required=True, queryset=Product.tags.tag_model.objects.none().order_by("name"), + label=labels.ASSET_TAG_LABEL, error_messages={ "required": "*"}) @@ -2932,6 +2958,20 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields["default_group_role"].queryset = get_group_member_roles() + self.fields["enable_product_tracking_files"].label = labels.SETTINGS_TRACKED_FILES_ENABLE_LABEL + self.fields["enable_product_tracking_files"].help_text = labels.SETTINGS_TRACKED_FILES_ENABLE_HELP + + self.fields[ + "enforce_verified_status_product_grading"].label = labels.SETTINGS_ASSET_GRADING_ENFORCE_VERIFIED_LABEL + self.fields[ + "enforce_verified_status_product_grading"].help_text = labels.SETTINGS_ASSET_GRADING_ENFORCE_VERIFIED_HELP + + self.fields["enable_product_grade"].label = labels.SETTINGS_ASSET_GRADING_ENABLE_LABEL + self.fields["enable_product_grade"].help_text = labels.SETTINGS_ASSET_GRADING_ENABLE_HELP + + self.fields["enable_product_tag_inheritance"].label = labels.SETTINGS_ASSET_TAG_INHERITANCE_ENABLE_LABEL + self.fields["enable_product_tag_inheritance"].help_text = labels.SETTINGS_ASSET_TAG_INHERITANCE_ENABLE_HELP + def clean(self): cleaned_data = super().clean() enable_jira_value = cleaned_data.get("enable_jira") diff --git a/dojo/group/views.py b/dojo/group/views.py index fa2fd1e65b1..df1e6e815b2 100644 --- a/dojo/group/views.py +++ b/dojo/group/views.py @@ -39,6 +39,7 @@ get_product_type_groups_for_group, ) from dojo.group.utils import get_auth_group_name +from dojo.labels import get_labels from dojo.models import Dojo_Group, Dojo_Group_Member, Global_Role, Product_Group, Product_Type_Group from dojo.utils import ( add_breadcrumb, @@ -51,6 +52,9 @@ logger = logging.getLogger(__name__) +labels = get_labels() + + class ListGroups(View): def get_groups(self): return get_authorized_groups(Permissions.Group_View) @@ -514,6 +518,7 @@ def delete_group_member(request, mid): def add_product_group(request, gid): group = get_object_or_404(Dojo_Group, id=gid) group_form = Add_Product_Group_GroupForm(initial={"group": group.id}) + page_name = str(labels.ASSET_GROUPS_ADD_LABEL) if request.method == "POST": group_form = Add_Product_Group_GroupForm(request.POST, initial={"group": group.id}) @@ -529,12 +534,13 @@ def add_product_group(request, gid): product_group.save() messages.add_message(request, messages.SUCCESS, - "Product groups added successfully.", + labels.ASSET_GROUPS_ADD_SUCCESS_MESSAGE, extra_tags="alert-success") return HttpResponseRedirect(reverse("view_group", args=(gid, ))) - add_breadcrumb(title="Add Product Group", top_level=False, request=request) + add_breadcrumb(title=page_name, top_level=False, request=request) return render(request, "dojo/new_product_group_group.html", { + "name": page_name, "group": group, "form": group_form, }) @@ -544,6 +550,7 @@ def add_product_group(request, gid): def add_product_type_group(request, gid): group = get_object_or_404(Dojo_Group, id=gid) group_form = Add_Product_Type_Group_GroupForm(initial={"group": group.id}) + page_name = str(labels.ORG_GROUPS_ADD_LABEL) if request.method == "POST": group_form = Add_Product_Type_Group_GroupForm(request.POST, initial={"group": group.id}) @@ -559,12 +566,13 @@ def add_product_type_group(request, gid): product_type_group.save() messages.add_message(request, messages.SUCCESS, - "Product type groups added successfully.", + labels.ORG_GROUPS_ADD_SUCCESS_MESSAGE, extra_tags="alert-success") return HttpResponseRedirect(reverse("view_group", args=(gid, ))) - add_breadcrumb(title="Add Product Type Group", top_level=False, request=request) + add_breadcrumb(title=page_name, top_level=False, request=request) return render(request, "dojo/new_product_type_group_group.html", { + "name": page_name, "group": group, "form": group_form, }) diff --git a/dojo/labels.py b/dojo/labels.py new file mode 100644 index 00000000000..ce5ea520c5d --- /dev/null +++ b/dojo/labels.py @@ -0,0 +1,85 @@ +""" +This module provides centralized access to application text copy. For the time being, this centralization is necessary +as some elements (forms.py, templates) require access to labels from across different model packages. + +Each model package that needs to support text copy can provide its own 'labels.py' that can be registered here. That +module should provide a set of stable dictionary keys that can be used to reference text copy within the app, as well as +a dictionary that maps these keys to the text copy. + +In this file, the sets of keys and the text copy dictionaries for all such model packages should be imported and added +to the corresponding structures: LabelsProxy should extend the set of keys, and the 'labels' variable should have the +text copy dictionary added to it. The LabelsProxy serves to provide easy autocomplete/linter compatibility with the +full list of text copy keys that exist over the program, until things are more modularized on a per-model basis. + +For templates, a `label` context processor has been added, so developers can just use labels.ATTRIBUTE_NAME. + +In views/Python code, developers should first import get_labels() and set it to a variable, e.g., labels = get_labels(). +Then they can simply use labels.ATTRIBUTE_NAME. + +For the stable keys, some conventions used: + Each copy attribute name starts with a noun representing the overarching model/object type the label is for. + Attribute suffixes are as follows: + _LABEL -> short label, used for UI/API fields + _MESSAGE -> a longer message displayed as a toast or displayed on the page + _HELP -> helptext (for help_text kwargs/popover content) +""" +import logging + +from dojo.asset.labels import AssetLabelsKeys +from dojo.asset.labels import labels as asset_labels +from dojo.organization.labels import OrganizationLabelsKeys +from dojo.organization.labels import labels as organization_labels +from dojo.system_settings.labels import SystemSettingsLabelsKeys +from dojo.system_settings.labels import labels as system_settings_labels + +logger = logging.getLogger(__name__) + + +class LabelsProxy( + AssetLabelsKeys, + OrganizationLabelsKeys, + SystemSettingsLabelsKeys, +): + + """ + Proxy class for text copy. The purpose of this is to allow easy access to the copy from within templates, and to + allow for IDE code completion. This inherits from the various copy key classes so IDEs can statically determine what + attributes ("labels") are available. After initialization, all attributes defined on this class are set to the value + of the appropriate text. + """ + + def _get_label_entries(self): + """Returns a dict of all "label" entries from this class.""" + cl = self.__class__ + return { + name: getattr(cl, name) for name in dir(cl) if not name.startswith("_")} + + def __init__(self, label_set: dict[str, str]): + """ + The initializer takes a dict set of labels and sets the corresponding attribute defined in this class to the + value specified in the dict (e.g., self.ASSET_GROUPS_DELETE_SUCCESS_MESSAGE is set to + labels[K.ASSET_GROUPS_DELETE_SUCCESS_MESSAGE]). + + As a side benefit, this will explode if any label defined on this class is not present in the given dict: a + runtime check that a labels dict must be complete. + """ + for _l, _v in self._get_label_entries().items(): + try: + setattr(self, _l, label_set[_v]) + except KeyError: + error_message = f"Supplied copy dictionary does not provide entry for {_l}" + logger.error(error_message) + raise ValueError(error_message) + + +# The full set of text copy, mapping the stable key entries to their respective text copy values +labels: dict[str, str] = asset_labels | organization_labels | system_settings_labels + + +# The labels proxy object +labels_proxy = LabelsProxy(labels) + + +def get_labels() -> LabelsProxy: + """Method for getting a LabelsProxy initialized with the correct set of labels.""" + return labels_proxy diff --git a/dojo/metrics/urls.py b/dojo/metrics/urls.py index a121403cc1d..f0643322f1c 100644 --- a/dojo/metrics/urls.py +++ b/dojo/metrics/urls.py @@ -1,27 +1,101 @@ +from django.conf import settings from django.urls import re_path from dojo.metrics import views +from dojo.utils import redirect_view -urlpatterns = [ - # metrics - re_path(r"^metrics$", views.metrics, {"mtype": "All"}, - name="metrics"), - re_path(r"^critical_product_metrics$", views.critical_product_metrics, {"mtype": "All"}, - name="critical_product_metrics"), - re_path(r"^metrics/all$", views.metrics, {"mtype": "All"}, - name="metrics_all"), - re_path(r"^metrics/product/type$", views.metrics, {"mtype": "All"}, - name="metrics_product_type"), - re_path(r"^metrics/simple$", views.simple_metrics, - name="simple_metrics"), - re_path(r"^metrics/product/type/(?P\d+)$", - views.metrics, name="product_type_metrics"), - re_path(r"^metrics/product/type/counts$", - views.product_type_counts, name="product_type_counts"), - re_path(r"^metrics/product/tag/counts$", - views.product_tag_counts, name="product_tag_counts"), - re_path(r"^metrics/engineer$", views.engineer_metrics, - name="engineer_metrics"), - re_path(r"^metrics/engineer/(?P\d+)$", views.view_engineer, - name="view_engineer"), -] +# TODO: remove the else: branch once v3 migration is complete +if settings.ENABLE_V3_ORGANIZATION_ASSET_RELABEL: + urlpatterns = [ + # metrics + re_path( + r"^metrics$", + views.metrics, + {"mtype": "All"}, + name="metrics", + ), + re_path( + r"^critical_asset_metrics$", + views.critical_product_metrics, + {"mtype": "All"}, + name="critical_product_metrics", + ), + re_path( + r"^metrics/all$", + views.metrics, + {"mtype": "All"}, + name="metrics_all", + ), + re_path( + r"^metrics/organization$", + views.metrics, + {"mtype": "All"}, + name="metrics_product_type", + ), + re_path( + r"^metrics/simple$", + views.simple_metrics, + name="simple_metrics", + ), + re_path( + r"^metrics/organization/(?P\d+)$", + views.metrics, + name="product_type_metrics", + ), + re_path( + r"^metrics/organization/counts$", + views.product_type_counts, + name="product_type_counts", + ), + re_path( + r"^metrics/asset/tag/counts$", + views.product_tag_counts, + name="product_tag_counts", + ), + re_path( + r"^metrics/engineer$", + views.engineer_metrics, + name="engineer_metrics", + ), + re_path( + r"^metrics/engineer/(?P\d+)$", + views.view_engineer, + name="view_engineer", + ), + # TODO: Backwards compatibility; remove after v3 migration is complete + re_path(r"^critical_product_metrics$", redirect_view("critical_product_metrics")), + re_path(r"^metrics/product/type$", redirect_view("metrics_product_type")), + re_path(r"^metrics/product/type/(?P\d+)$", redirect_view("product_type_metrics")), + re_path(r"^metrics/product/type/counts$", redirect_view("product_type_counts")), + re_path(r"^metrics/product/tag/counts$", redirect_view("product_tag_counts")), + ] +else: + urlpatterns = [ + # metrics + re_path(r"^metrics$", views.metrics, {"mtype": "All"}, + name="metrics"), + re_path(r"^critical_product_metrics$", views.critical_product_metrics, {"mtype": "All"}, + name="critical_product_metrics"), + re_path(r"^metrics/all$", views.metrics, {"mtype": "All"}, + name="metrics_all"), + re_path(r"^metrics/product/type$", views.metrics, {"mtype": "All"}, + name="metrics_product_type"), + re_path(r"^metrics/simple$", views.simple_metrics, + name="simple_metrics"), + re_path(r"^metrics/product/type/(?P\d+)$", + views.metrics, name="product_type_metrics"), + re_path(r"^metrics/product/type/counts$", + views.product_type_counts, name="product_type_counts"), + re_path(r"^metrics/product/tag/counts$", + views.product_tag_counts, name="product_tag_counts"), + re_path(r"^metrics/engineer$", views.engineer_metrics, + name="engineer_metrics"), + re_path(r"^metrics/engineer/(?P\d+)$", views.view_engineer, + name="view_engineer"), + # Forward compatibility + re_path(r"^critical_asset_metrics$", redirect_view("critical_product_metrics")), + re_path(r"^metrics/organization$", redirect_view("metrics_product_type")), + re_path(r"^metrics/organization/(?P\d+)$", redirect_view("product_type_metrics")), + re_path(r"^metrics/organization/counts$", redirect_view("product_type_counts")), + re_path(r"^metrics/asset/tag/counts$", redirect_view("product_tag_counts")), + ] diff --git a/dojo/metrics/views.py b/dojo/metrics/views.py index 24c68381806..42e045eeb98 100644 --- a/dojo/metrics/views.py +++ b/dojo/metrics/views.py @@ -22,6 +22,7 @@ from dojo.authorization.roles_permissions import Permissions from dojo.filters import UserFilter from dojo.forms import ProductTagCountsForm, ProductTypeCountsForm, SimpleMetricsForm +from dojo.labels import get_labels from dojo.metrics.utils import ( endpoint_queries, finding_queries, @@ -49,6 +50,9 @@ logger = logging.getLogger(__name__) +labels = get_labels() + + """ Greg, Jay status: in production @@ -58,7 +62,7 @@ def critical_product_metrics(request, mtype): template = "dojo/metrics.html" - page_name = _("Critical Product Metrics") + page_name = str(labels.ASSET_METRICS_CRITICAL_LABEL) critical_products = get_authorized_product_types(Permissions.Product_Type_View) critical_products = critical_products.filter(critical_product=True) add_breadcrumb(title=page_name, top_level=not len(request.GET), request=request) @@ -94,10 +98,10 @@ def metrics(request, mtype): filters = {} if view == "Finding": - page_name = _("Product Type Metrics by Findings") + page_name = str(labels.ORG_METRICS_BY_FINDINGS_LABEL) filters = finding_queries(prod_type, request) elif view == "Endpoint": - page_name = _("Product Type Metrics by Affected Endpoints") + page_name = str(labels.ORG_METRICS_BY_ENDPOINTS_LABEL) filters = endpoint_queries(prod_type, request) all_findings = findings_queryset(queryset_check(filters["all"])) @@ -425,7 +429,7 @@ def product_type_counts(request): for o in overall_in_pt: aip[o["numerical_severity"]] = o["numerical_severity__count"] else: - messages.add_message(request, messages.ERROR, _("Please choose month and year and the Product Type."), + messages.add_message(request, messages.ERROR, labels.ORG_METRICS_TYPE_COUNTS_ERROR_MESSAGE, extra_tags="alert-danger") add_breadcrumb(title=_("Bi-Weekly Metrics"), top_level=True, request=request) @@ -630,8 +634,7 @@ def product_tag_counts(request): for o in overall_in_pt: aip[o["numerical_severity"]] = o["numerical_severity__count"] else: - messages.add_message(request, messages.ERROR, _("Please choose month and year and the Product Tag."), - extra_tags="alert-danger") + messages.add_message(request, messages.ERROR, labels.ASSET_METRICS_TAG_COUNTS_ERROR_MESSAGE, extra_tags="alert-danger") add_breadcrumb(title=_("Bi-Weekly Metrics"), top_level=True, request=request) diff --git a/dojo/notifications/helper.py b/dojo/notifications/helper.py index c525bb4fb8c..f59060331d1 100644 --- a/dojo/notifications/helper.py +++ b/dojo/notifications/helper.py @@ -19,6 +19,7 @@ from dojo.authorization.roles_permissions import Permissions from dojo.celery import app from dojo.decorators import dojo_async_task, we_want_async +from dojo.labels import get_labels from dojo.models import ( Alerts, Dojo_User, @@ -41,6 +42,9 @@ logger = logging.getLogger(__name__) +labels = get_labels() + + def create_notification( event: str | None = None, title: str | None = None, @@ -120,9 +124,9 @@ def _get_system_settings(self) -> System_Settings: def _create_description(self, event: str, kwargs: dict) -> str: if kwargs.get("description") is None: if event == "product_added": - kwargs["description"] = _("Product %s has been created successfully.") % kwargs["title"] + kwargs["description"] = labels.ASSET_NOTIFICATION_WITH_NAME_CREATED_MESSAGE % {"name": kwargs["title"]} elif event == "product_type_added": - kwargs["description"] = _("Product Type %s has been created successfully.") % kwargs["title"] + kwargs["description"] = labels.ORG_NOTIFICATION_WITH_NAME_CREATED_MESSAGE % {"name": kwargs["title"]} else: kwargs["description"] = _("Event %s has occurred.") % str(event) diff --git a/dojo/object/views.py b/dojo/object/views.py index ad649885bf1..96616c556a8 100644 --- a/dojo/object/views.py +++ b/dojo/object/views.py @@ -9,14 +9,18 @@ from dojo.authorization.authorization_decorators import user_is_authorized from dojo.authorization.roles_permissions import Permissions from dojo.forms import DeleteObjectsSettingsForm, ObjectSettingsForm +from dojo.labels import get_labels from dojo.models import Objects_Product, Product from dojo.utils import Product_Tab logger = logging.getLogger(__name__) +labels = get_labels() + @user_is_authorized(Product, Permissions.Product_Tracking_Files_Add, "pid") def new_object(request, pid): + page_name = labels.ASSET_TRACKED_FILES_ADD_LABEL prod = get_object_or_404(Product, id=pid) if request.method == "POST": tform = ObjectSettingsForm(request.POST) @@ -27,15 +31,17 @@ def new_object(request, pid): messages.add_message(request, messages.SUCCESS, - "Added Tracked File to a Product", + labels.ASSET_TRACKED_FILES_ADD_SUCCESS_MESSAGE, extra_tags="alert-success") return HttpResponseRedirect(reverse("view_objects", args=(pid,))) return None tform = ObjectSettingsForm() - product_tab = Product_Tab(prod, title="Add Tracked Files to a Product", tab="settings") + product_tab = Product_Tab(prod, title=str(page_name), tab="settings") return render(request, "dojo/new_object.html", - {"tform": tform, + { + "name": page_name, + "tform": tform, "product_tab": product_tab, "pid": prod.id}) @@ -45,7 +51,7 @@ def view_objects(request, pid): product = get_object_or_404(Product, id=pid) object_queryset = Objects_Product.objects.filter(product=pid).order_by("path", "folder", "artifact") - product_tab = Product_Tab(product, title="Tracked Product Files, Paths and Artifacts", tab="settings") + product_tab = Product_Tab(product, title="Tracked Files, Paths and Artifacts", tab="settings") return render(request, "dojo/view_objects.html", { @@ -60,7 +66,8 @@ def edit_object(request, pid, ttid): object_prod = Objects_Product.objects.get(pk=ttid) product = get_object_or_404(Product, id=pid) if object_prod.product != product: - msg = f"Product {pid} does not fit to product of Object {object_prod.product.id}" + msg = labels.ASSET_TRACKED_FILES_ID_MISMATCH_ERROR_MESSAGE % {"asset_id": pid, + "object_asset_id": object_prod.product.id} raise BadRequest(msg) if request.method == "POST": @@ -70,7 +77,7 @@ def edit_object(request, pid, ttid): messages.add_message(request, messages.SUCCESS, - "Tool Product Configuration Successfully Updated.", + "Tracked File Successfully Updated.", extra_tags="alert-success") return HttpResponseRedirect(reverse("view_objects", args=(pid,))) else: @@ -90,7 +97,8 @@ def delete_object(request, pid, ttid): object_prod = Objects_Product.objects.get(pk=ttid) product = get_object_or_404(Product, id=pid) if object_prod.product != product: - msg = f"Product {pid} does not fit to product of Object {object_prod.product.id}" + msg = labels.ASSET_TRACKED_FILES_ID_MISMATCH_ERROR_MESSAGE % {"asset_id": pid, + "object_asset_id": object_prod.product.id} raise BadRequest(msg) if request.method == "POST": @@ -98,12 +106,12 @@ def delete_object(request, pid, ttid): object_prod.delete() messages.add_message(request, messages.SUCCESS, - "Tracked Product Files Deleted.", + "Tracked Files Deleted.", extra_tags="alert-success") return HttpResponseRedirect(reverse("view_objects", args=(pid,))) tform = DeleteObjectsSettingsForm(instance=object_prod) - product_tab = Product_Tab(product, title="Delete Product Tool Configuration", tab="settings") + product_tab = Product_Tab(product, title="Delete Tracked File", tab="settings") return render(request, "dojo/delete_object.html", { diff --git a/dojo/organization/__init__.py b/dojo/organization/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dojo/organization/api/__init__.py b/dojo/organization/api/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dojo/organization/api/filters.py b/dojo/organization/api/filters.py new file mode 100644 index 00000000000..14e282e2ec9 --- /dev/null +++ b/dojo/organization/api/filters.py @@ -0,0 +1,36 @@ +from django_filters import BooleanFilter, NumberFilter +from django_filters.rest_framework import FilterSet + +from dojo.labels import get_labels +from dojo.models import ( + Product_Type, + Product_Type_Group, + Product_Type_Member, +) + +labels = get_labels() + + +class OrganizationFilterSet(FilterSet): + critical_asset = BooleanFilter(field_name="critical_product") + key_asset = BooleanFilter(field_name="key_product") + + class Meta: + model = Product_Type + fields = ("id", "name", "created", "updated") + + +class OrganizationMemberFilterSet(FilterSet): + organization_id = NumberFilter(field_name="product_type_id") + + class Meta: + model = Product_Type_Member + fields = ("id", "user_id") + + +class OrganizationGroupFilterSet(FilterSet): + asset_type_id = NumberFilter(field_name="product_type_id") + + class Meta: + model = Product_Type_Group + fields = ("id", "group_id") diff --git a/dojo/organization/api/serializers.py b/dojo/organization/api/serializers.py new file mode 100644 index 00000000000..d624c72524d --- /dev/null +++ b/dojo/organization/api/serializers.py @@ -0,0 +1,123 @@ +from rest_framework import serializers +from rest_framework.exceptions import PermissionDenied, ValidationError + +from dojo.authorization.authorization import user_has_permission +from dojo.authorization.roles_permissions import Permissions +from dojo.models import ( + Product_Type, + Product_Type_Group, + Product_Type_Member, +) +from dojo.product_type.queries import get_authorized_product_types + + +class RelatedOrganizationField(serializers.PrimaryKeyRelatedField): + def get_queryset(self): + return get_authorized_product_types(Permissions.Product_Type_View) + + +class OrganizationMemberSerializer(serializers.ModelSerializer): + organization = RelatedOrganizationField(source="product_type") + + class Meta: + model = Product_Type_Member + exclude = ("product_type",) + + def validate(self, data): + if ( + self.instance is not None + and data.get("organization") != self.instance.product_type + and not user_has_permission( + self.context["request"].user, + data.get("organization"), + Permissions.Product_Type_Manage_Members, + ) + ): + msg = "You are not permitted to add a member to this Organization" + raise PermissionDenied(msg) + + if ( + self.instance is None + or data.get("organization") != self.instance.product_type + or data.get("user") != self.instance.user + ): + members = Product_Type_Member.objects.filter( + product_type=data.get("organization"), user=data.get("user"), + ) + if members.count() > 0: + msg = "Organization Member already exists" + raise ValidationError(msg) + + if self.instance is not None and not data.get("role").is_owner: + owners = ( + Product_Type_Member.objects.filter( + product_type=data.get("organization"), role__is_owner=True, + ) + .exclude(id=self.instance.id) + .count() + ) + if owners < 1: + msg = "There must be at least one owner" + raise ValidationError(msg) + + if data.get("role").is_owner and not user_has_permission( + self.context["request"].user, + data.get("organization"), + Permissions.Product_Type_Member_Add_Owner, + ): + msg = "You are not permitted to add a member as Owner to this Organization" + raise PermissionDenied(msg) + + return data + + +class OrganizationGroupSerializer(serializers.ModelSerializer): + organization = RelatedOrganizationField(source="product_type") + + class Meta: + model = Product_Type_Group + exclude = ("product_type",) + + def validate(self, data): + if ( + self.instance is not None + and data.get("organization") != self.instance.product_type + and not user_has_permission( + self.context["request"].user, + data.get("organization"), + Permissions.Product_Type_Group_Add, + ) + ): + msg = "You are not permitted to add a group to this Organization" + raise PermissionDenied(msg) + + if ( + self.instance is None + or data.get("organization") != self.instance.product_type + or data.get("group") != self.instance.group + ): + members = Product_Type_Group.objects.filter( + product_type=data.get("organization"), group=data.get("group"), + ) + if members.count() > 0: + msg = "Organization Group already exists" + raise ValidationError(msg) + + if data.get("role").is_owner and not user_has_permission( + self.context["request"].user, + data.get("organization"), + Permissions.Product_Type_Group_Add_Owner, + ): + msg = "You are not permitted to add a group as Owner to this Organization" + raise PermissionDenied(msg) + + return data + + +class OrganizationSerializer(serializers.ModelSerializer): + critical_asset = serializers.BooleanField(source="critical_product") + key_asset = serializers.BooleanField(source="key_product") + + class Meta: + model = Product_Type + exclude = ("critical_product", "key_product") diff --git a/dojo/organization/api/urls.py b/dojo/organization/api/urls.py new file mode 100644 index 00000000000..a0bec88cb2d --- /dev/null +++ b/dojo/organization/api/urls.py @@ -0,0 +1,12 @@ +from dojo.organization.api.views import ( + OrganizationGroupViewSet, + OrganizationMemberViewSet, + OrganizationViewSet, +) + + +def add_organization_urls(router): + router.register(r"organizations", OrganizationViewSet, basename="organization") + router.register(r"organization_members", OrganizationMemberViewSet, basename="organization_member") + router.register(r"organization_groups", OrganizationGroupViewSet, basename="organization_group") + return router diff --git a/dojo/organization/api/views.py b/dojo/organization/api/views.py new file mode 100644 index 00000000000..dc9f3fc0cc2 --- /dev/null +++ b/dojo/organization/api/views.py @@ -0,0 +1,183 @@ +from django_filters.rest_framework import DjangoFilterBackend +from drf_spectacular.utils import extend_schema, extend_schema_view +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from dojo.api_v2 import permissions +from dojo.api_v2.serializers import ReportGenerateOptionSerializer, ReportGenerateSerializer +from dojo.api_v2.views import PrefetchDojoModelViewSet, report_generate, schema_with_prefetch +from dojo.authorization.roles_permissions import Permissions +from dojo.models import ( + Product_Type, + Product_Type_Group, + Product_Type_Member, + Role, +) +from dojo.organization.api import serializers +from dojo.organization.api.filters import ( + OrganizationGroupFilterSet, + OrganizationMemberFilterSet, +) +from dojo.product_type.queries import ( + get_authorized_product_type_groups, + get_authorized_product_type_members, + get_authorized_product_types, +) +from dojo.utils import async_delete, get_setting + + +# Authorization: object-based +@extend_schema_view(**schema_with_prefetch()) +class OrganizationViewSet( + PrefetchDojoModelViewSet, +): + serializer_class = serializers.OrganizationSerializer + queryset = Product_Type.objects.none() + filter_backends = (DjangoFilterBackend,) + filterset_fields = [ + "id", + "name", + "critical_product", + "key_product", + "created", + "updated", + ] + permission_classes = ( + IsAuthenticated, + permissions.UserHasProductTypePermission, + ) + + def get_queryset(self): + return get_authorized_product_types( + Permissions.Product_Type_View, + ).distinct() + + # Overwrite perfom_create of CreateModelMixin to add current user as owner + def perform_create(self, serializer): + serializer.save() + product_type_data = serializer.data + product_type_data.pop("authorization_groups") + product_type_data.pop("members") + member = Product_Type_Member() + member.user = self.request.user + member.product_type = Product_Type(**product_type_data) + member.role = Role.objects.get(is_owner=True) + member.save() + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + if get_setting("ASYNC_OBJECT_DELETE"): + async_del = async_delete() + async_del.delete(instance) + else: + instance.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + + @extend_schema( + request=ReportGenerateOptionSerializer, + responses={status.HTTP_200_OK: ReportGenerateSerializer}, + ) + @action( + detail=True, methods=["post"], permission_classes=[IsAuthenticated], + ) + def generate_report(self, request, pk=None): + product_type = self.get_object() + + options = {} + # prepare post data + report_options = ReportGenerateOptionSerializer( + data=request.data, + ) + if report_options.is_valid(): + options["include_finding_notes"] = report_options.validated_data[ + "include_finding_notes" + ] + options["include_finding_images"] = report_options.validated_data[ + "include_finding_images" + ] + options[ + "include_executive_summary" + ] = report_options.validated_data["include_executive_summary"] + options[ + "include_table_of_contents" + ] = report_options.validated_data["include_table_of_contents"] + else: + return Response( + report_options.errors, status=status.HTTP_400_BAD_REQUEST, + ) + + data = report_generate(request, product_type, options) + report = ReportGenerateSerializer(data) + return Response(report.data) + + +# Authorization: object-based +@extend_schema_view(**schema_with_prefetch()) +class OrganizationMemberViewSet( + PrefetchDojoModelViewSet, +): + serializer_class = serializers.OrganizationMemberSerializer + queryset = Product_Type_Member.objects.none() + filter_backends = (DjangoFilterBackend,) + filterset_class = OrganizationMemberFilterSet + permission_classes = ( + IsAuthenticated, + permissions.UserHasProductTypeMemberPermission, + ) + + def get_queryset(self): + return get_authorized_product_type_members( + Permissions.Product_Type_View, + ).distinct() + + def destroy(self, request, *args, **kwargs): + instance = self.get_object() + if instance.role.is_owner: + owners = Product_Type_Member.objects.filter( + product_type=instance.product_type, role__is_owner=True, + ).count() + if owners <= 1: + return Response( + "There must be at least one owner", + status=status.HTTP_400_BAD_REQUEST, + ) + self.perform_destroy(instance) + return Response(status=status.HTTP_204_NO_CONTENT) + + @extend_schema( + exclude=True, + ) + def partial_update(self, request, pk=None): + # Object authorization won't work if not all data is provided + response = {"message": "Patch function is not offered in this path."} + return Response(response, status=status.HTTP_405_METHOD_NOT_ALLOWED) + + +# Authorization: object-based +@extend_schema_view(**schema_with_prefetch()) +class OrganizationGroupViewSet( + PrefetchDojoModelViewSet, +): + serializer_class = serializers.OrganizationGroupSerializer + queryset = Product_Type_Group.objects.none() + filter_backends = (DjangoFilterBackend,) + filterset_class = OrganizationGroupFilterSet + permission_classes = ( + IsAuthenticated, + permissions.UserHasProductTypeGroupPermission, + ) + + def get_queryset(self): + return get_authorized_product_type_groups( + Permissions.Product_Type_Group_View, + ).distinct() + + @extend_schema( + exclude=True, + ) + def partial_update(self, request, pk=None): + # Object authorization won't work if not all data is provided + response = {"message": "Patch function is not offered in this path."} + return Response(response, status=status.HTTP_405_METHOD_NOT_ALLOWED) diff --git a/dojo/organization/labels.py b/dojo/organization/labels.py new file mode 100644 index 00000000000..911695f5866 --- /dev/null +++ b/dojo/organization/labels.py @@ -0,0 +1,196 @@ +from django.conf import settings +from django.utils.translation import gettext_lazy as _ + + +class OrganizationLabelsKeys: + + """Directory of text copy used by the Organization model.""" + + ORG_LABEL = "org.label" + ORG_PLURAL_LABEL = "org.plural_label" + ORG_ALL_LABEL = "org.all_label" + ORG_WITH_NAME_LABEL = "org.with_name_label" + ORG_NONE_FOUND_MESSAGE = "org.none_found_label" + ORG_REPORT_LABEL = "org.report_label" + ORG_REPORT_TITLE = "org.report_title" + ORG_REPORT_WITH_NAME_TITLE = "org.report_with_name_title" + ORG_METRICS_LABEL = "org.metrics.label" + ORG_METRICS_COUNTS_LABEL = "org.metrics.counts_label" + ORG_METRICS_BY_FINDINGS_LABEL = "org.metrics_by_findings_label" + ORG_METRICS_BY_ENDPOINTS_LABEL = "org.metrics_by_endpoints_label" + ORG_METRICS_TYPE_COUNTS_ERROR_MESSAGE = "org.metrics_type_counts_error_message" + ORG_OPTIONS_LABEL = "org.options_label" + ORG_NOTIFICATION_WITH_NAME_CREATED_MESSAGE = "org.notification_with_name_created_message" + ORG_CRITICAL_PRODUCT_LABEL = "org.critical_product_label" + ORG_KEY_PRODUCT_LABEL = "org.key_product_label" + ORG_FILTERS_LABEL = "org.filters.label" + ORG_FILTERS_LABEL_HELP = "org.filters.label_help" + ORG_FILTERS_NAME_LABEL = "org.filters.name_label" + ORG_FILTERS_NAME_HELP = "org.filters.name_help" + ORG_FILTERS_NAME_EXACT_LABEL = "org.filters.name_exact_label" + ORG_FILTERS_NAME_CONTAINS_LABEL = "org.filters.name_contains_label" + ORG_FILTERS_NAME_CONTAINS_HELP = "org.filters.name_contains_help" + ORG_FILTERS_TAGS_LABEL = "org.filters.tags_label" + ORG_USERS_LABEL = "org.users.label" + ORG_USERS_NO_ACCESS_MESSAGE = "org.users.no_access_message" + ORG_USERS_ADD_ORGANIZATIONS_LABEL = "org.users.add_organizations_label" + ORG_USERS_DELETE_LABEL = "org.users.delete_label" + ORG_USERS_DELETE_SUCCESS_MESSAGE = "org.users.delete_success_message" + ORG_USERS_ADD_LABEL = "org.users.add_label" + ORG_USERS_ADD_SUCCESS_MESSAGE = "org.users.add_success_message" + ORG_USERS_UPDATE_LABEL = "org.users.update_label" + ORG_USERS_UPDATE_SUCCESS_MESSAGE = "org.users.update_success_message" + ORG_USERS_MINIMUM_NUMBER_WITH_NAME_MESSAGE = "org.users.minimum_number_with_name_message" + ORG_GROUPS_LABEL = "org.groups.label" + ORG_GROUPS_NO_ACCESS_MESSAGE = "org.groups.no_access_message" + ORG_GROUPS_ADD_ORGANIZATIONS_LABEL = "org.groups.add_organizations_label" + ORG_GROUPS_NUM_ORGANIZATIONS_LABEL = "org.groups.num_organizations_label" + ORG_GROUPS_ADD_LABEL = "org.groups.add_label" + ORG_GROUPS_ADD_SUCCESS_MESSAGE = "org.groups.add_success_message" + ORG_GROUPS_UPDATE_LABEL = "org.groups.update_label" + ORG_GROUPS_UPDATE_SUCCESS_MESSAGE = "org.groups.update_success_message" + ORG_GROUPS_DELETE_LABEL = "org.groups.delete_label" + ORG_GROUPS_DELETE_SUCCESS_MESSAGE = "org.groups.delete_success_message" + ORG_CREATE_LABEL = "org.create.label" + ORG_CREATE_SUCCESS_MESSAGE = "org.create.success_message" + ORG_READ_LABEL = "org.read.label" + ORG_READ_LIST_LABEL = "org.read.list_label" + ORG_UPDATE_LABEL = "org.update.label" + ORG_UPDATE_WITH_NAME_LABEL = "org.update.with_name_label" + ORG_UPDATE_SUCCESS_MESSAGE = "org.update.success_message" + ORG_DELETE_LABEL = "org.delete.label" + ORG_DELETE_WITH_NAME_LABEL = "org.delete.with_name_label" + ORG_DELETE_CONFIRM_MESSAGE = "org.delete.confirm_message" + ORG_DELETE_SUCCESS_MESSAGE = "org.delete.success_message" + ORG_DELETE_SUCCESS_ASYNC_MESSAGE = "org.delete.success_async_message" + ORG_DELETE_WITH_NAME_SUCCESS_MESSAGE = "org.delete.with_name_success_message" + ORG_DELETE_WITH_NAME_WITH_USER_SUCCESS_MESSAGE = "org.delete.with_name_with_user_success_message" + + +# TODO: remove the else: branch once v3 migration is complete +if settings.ENABLE_V3_ORGANIZATION_ASSET_RELABEL: + labels = { + OrganizationLabelsKeys.ORG_LABEL: _("Organization"), + OrganizationLabelsKeys.ORG_PLURAL_LABEL: _("Organizations"), + OrganizationLabelsKeys.ORG_ALL_LABEL: _("All Organizations"), + OrganizationLabelsKeys.ORG_WITH_NAME_LABEL: _("Organization '%(name)s'"), + OrganizationLabelsKeys.ORG_NONE_FOUND_MESSAGE: _("No Organizations found"), + OrganizationLabelsKeys.ORG_REPORT_LABEL: _("Organization Report"), + OrganizationLabelsKeys.ORG_REPORT_TITLE: _("Organization Report"), + OrganizationLabelsKeys.ORG_REPORT_WITH_NAME_TITLE: _("Organization Report: %(name)s"), + OrganizationLabelsKeys.ORG_METRICS_LABEL: _("Organization Metrics"), + OrganizationLabelsKeys.ORG_METRICS_COUNTS_LABEL: _("Organization Counts"), + OrganizationLabelsKeys.ORG_METRICS_BY_FINDINGS_LABEL: _("Organization Metrics by Findings"), + OrganizationLabelsKeys.ORG_METRICS_BY_ENDPOINTS_LABEL: _("Organization Metrics by Affected Endpoints"), + OrganizationLabelsKeys.ORG_METRICS_TYPE_COUNTS_ERROR_MESSAGE: _("Please choose month and year and the Organization."), + OrganizationLabelsKeys.ORG_OPTIONS_LABEL: _("Organization Options"), + OrganizationLabelsKeys.ORG_NOTIFICATION_WITH_NAME_CREATED_MESSAGE: _("Organization %(name)s has been created successfully."), + OrganizationLabelsKeys.ORG_CRITICAL_PRODUCT_LABEL: _("Critical Asset"), + OrganizationLabelsKeys.ORG_KEY_PRODUCT_LABEL: _("Key Asset"), + OrganizationLabelsKeys.ORG_FILTERS_LABEL: _("Organization"), + OrganizationLabelsKeys.ORG_FILTERS_LABEL_HELP: _("Search for Organization names that are an exact match"), + OrganizationLabelsKeys.ORG_FILTERS_NAME_LABEL: _("Organization Name"), + OrganizationLabelsKeys.ORG_FILTERS_NAME_HELP: _("Search for Organization names that are an exact match"), + OrganizationLabelsKeys.ORG_FILTERS_NAME_EXACT_LABEL: _("Exact Organization Name"), + OrganizationLabelsKeys.ORG_FILTERS_NAME_CONTAINS_LABEL: _("Organization Name Contains"), + OrganizationLabelsKeys.ORG_FILTERS_NAME_CONTAINS_HELP: _("Search for Organization names that contain a given pattern"), + OrganizationLabelsKeys.ORG_FILTERS_TAGS_LABEL: _("Tags (Organization)"), + OrganizationLabelsKeys.ORG_USERS_LABEL: _("Organizations this User can access"), + OrganizationLabelsKeys.ORG_USERS_NO_ACCESS_MESSAGE: _("This User is not assigned to any Organizations."), + OrganizationLabelsKeys.ORG_USERS_ADD_ORGANIZATIONS_LABEL: _("Add Organizations"), + OrganizationLabelsKeys.ORG_USERS_DELETE_LABEL: _("Delete Organization Member"), + OrganizationLabelsKeys.ORG_USERS_DELETE_SUCCESS_MESSAGE: _("Organization member deleted successfully."), + OrganizationLabelsKeys.ORG_USERS_ADD_LABEL: _("Add Organization Member"), + OrganizationLabelsKeys.ORG_USERS_ADD_SUCCESS_MESSAGE: _("Organization members added successfully."), + OrganizationLabelsKeys.ORG_USERS_UPDATE_LABEL: _("Edit Organization Member"), + OrganizationLabelsKeys.ORG_USERS_UPDATE_SUCCESS_MESSAGE: _("Organization member updated successfully."), + OrganizationLabelsKeys.ORG_USERS_MINIMUM_NUMBER_WITH_NAME_MESSAGE: _("There must be at least one owner for Organization %(name)s."), + OrganizationLabelsKeys.ORG_GROUPS_LABEL: _("Organizations this Group can access"), + OrganizationLabelsKeys.ORG_GROUPS_NO_ACCESS_MESSAGE: _("This Group cannot access any Organizations."), + OrganizationLabelsKeys.ORG_GROUPS_ADD_ORGANIZATIONS_LABEL: _("Add Organizations"), + OrganizationLabelsKeys.ORG_GROUPS_NUM_ORGANIZATIONS_LABEL: _("Number of Organizations"), + OrganizationLabelsKeys.ORG_GROUPS_ADD_LABEL: _("Add Organization Group"), + OrganizationLabelsKeys.ORG_GROUPS_ADD_SUCCESS_MESSAGE: _("Organization groups added successfully."), + OrganizationLabelsKeys.ORG_GROUPS_UPDATE_LABEL: _("Edit Organization Group"), + OrganizationLabelsKeys.ORG_GROUPS_UPDATE_SUCCESS_MESSAGE: _("Organization group updated successfully."), + OrganizationLabelsKeys.ORG_GROUPS_DELETE_LABEL: _("Delete Organization Group"), + OrganizationLabelsKeys.ORG_GROUPS_DELETE_SUCCESS_MESSAGE: _("Organization group deleted successfully."), + OrganizationLabelsKeys.ORG_CREATE_LABEL: _("Add Organization"), + OrganizationLabelsKeys.ORG_CREATE_SUCCESS_MESSAGE: _("Organization added successfully."), + OrganizationLabelsKeys.ORG_READ_LABEL: _("View Organization"), + OrganizationLabelsKeys.ORG_READ_LIST_LABEL: _("List Organizations"), + OrganizationLabelsKeys.ORG_UPDATE_LABEL: _("Edit Organization"), + OrganizationLabelsKeys.ORG_UPDATE_WITH_NAME_LABEL: _("Edit Organization %(name)s"), + OrganizationLabelsKeys.ORG_UPDATE_SUCCESS_MESSAGE: _("Organization updated successfully."), + OrganizationLabelsKeys.ORG_DELETE_LABEL: _("Delete Organization"), + OrganizationLabelsKeys.ORG_DELETE_WITH_NAME_LABEL: _("Delete Organization %(name)s"), + OrganizationLabelsKeys.ORG_DELETE_CONFIRM_MESSAGE: _( + "Deleting this Organization will remove any related objects associated with it. These relationships are listed below:"), + OrganizationLabelsKeys.ORG_DELETE_SUCCESS_MESSAGE: _("Organization and relationships removed."), + OrganizationLabelsKeys.ORG_DELETE_SUCCESS_ASYNC_MESSAGE: _("Organization and relationships will be removed in the background."), + OrganizationLabelsKeys.ORG_DELETE_WITH_NAME_SUCCESS_MESSAGE: _('The Organization "%(name)s" was deleted'), + OrganizationLabelsKeys.ORG_DELETE_WITH_NAME_WITH_USER_SUCCESS_MESSAGE: _('The Organization "%(name)s" was deleted by %(user)s'), + } +else: + labels = { + OrganizationLabelsKeys.ORG_LABEL: _("Product Type"), + OrganizationLabelsKeys.ORG_PLURAL_LABEL: _("Product Types"), + OrganizationLabelsKeys.ORG_ALL_LABEL: _("All Product Types"), + OrganizationLabelsKeys.ORG_WITH_NAME_LABEL: _("Product Type '%(name)s'"), + OrganizationLabelsKeys.ORG_NONE_FOUND_MESSAGE: _("No Product Types found"), + OrganizationLabelsKeys.ORG_REPORT_LABEL: _("Product Type Report"), + OrganizationLabelsKeys.ORG_REPORT_TITLE: _("Product Type Report"), + OrganizationLabelsKeys.ORG_REPORT_WITH_NAME_TITLE: _("Product Type Report: %(name)s"), + OrganizationLabelsKeys.ORG_METRICS_LABEL: _("Product Type Metrics"), + OrganizationLabelsKeys.ORG_METRICS_COUNTS_LABEL: _("Product Type Counts"), + OrganizationLabelsKeys.ORG_METRICS_BY_FINDINGS_LABEL: _("Product Type Metrics by Findings"), + OrganizationLabelsKeys.ORG_METRICS_BY_ENDPOINTS_LABEL: _("Product Type Metrics by Affected Endpoints"), + OrganizationLabelsKeys.ORG_METRICS_TYPE_COUNTS_ERROR_MESSAGE: _("Please choose month and year and the Product Type."), + OrganizationLabelsKeys.ORG_OPTIONS_LABEL: _("Product Type Options"), + OrganizationLabelsKeys.ORG_NOTIFICATION_WITH_NAME_CREATED_MESSAGE: _("Product Type %(name)s has been created successfully."), + OrganizationLabelsKeys.ORG_CRITICAL_PRODUCT_LABEL: _("Critical Product"), + OrganizationLabelsKeys.ORG_KEY_PRODUCT_LABEL: _("Key Product"), + OrganizationLabelsKeys.ORG_FILTERS_LABEL: _("Product Type"), + OrganizationLabelsKeys.ORG_FILTERS_LABEL_HELP: _("Search for Product Type names that are an exact match"), + OrganizationLabelsKeys.ORG_FILTERS_NAME_LABEL: _("Product Type Name"), + OrganizationLabelsKeys.ORG_FILTERS_NAME_HELP: _("Search for Product Type names that are an exact match"), + OrganizationLabelsKeys.ORG_FILTERS_NAME_EXACT_LABEL: _("Exact Product Type Name"), + OrganizationLabelsKeys.ORG_FILTERS_NAME_CONTAINS_LABEL: _("Product Type Name Contains"), + OrganizationLabelsKeys.ORG_FILTERS_NAME_CONTAINS_HELP: _("Search for Product Type names that contain a given pattern"), + OrganizationLabelsKeys.ORG_FILTERS_TAGS_LABEL: _("Tags (Product Type)"), + OrganizationLabelsKeys.ORG_USERS_LABEL: _("Product Types this User can access"), + OrganizationLabelsKeys.ORG_USERS_NO_ACCESS_MESSAGE: _("This User is not assigned to any Product Types."), + OrganizationLabelsKeys.ORG_USERS_ADD_ORGANIZATIONS_LABEL: _("Add Product Types"), + OrganizationLabelsKeys.ORG_USERS_DELETE_LABEL: _("Delete Product Type Member"), + OrganizationLabelsKeys.ORG_USERS_DELETE_SUCCESS_MESSAGE: _("Product Type member deleted successfully."), + OrganizationLabelsKeys.ORG_USERS_ADD_LABEL: _("Add Product Type Member"), + OrganizationLabelsKeys.ORG_USERS_ADD_SUCCESS_MESSAGE: _("Product Type members added successfully."), + OrganizationLabelsKeys.ORG_USERS_UPDATE_LABEL: _("Edit Product Type Member"), + OrganizationLabelsKeys.ORG_USERS_UPDATE_SUCCESS_MESSAGE: _("Product Type member updated successfully."), + OrganizationLabelsKeys.ORG_USERS_MINIMUM_NUMBER_WITH_NAME_MESSAGE: _("There must be at least one owner for Product Type %(name)s."), + OrganizationLabelsKeys.ORG_GROUPS_LABEL: _("Product Types this Group can access"), + OrganizationLabelsKeys.ORG_GROUPS_NO_ACCESS_MESSAGE: _("This Group cannot access any Product Types."), + OrganizationLabelsKeys.ORG_GROUPS_ADD_ORGANIZATIONS_LABEL: _("Add Product Types"), + OrganizationLabelsKeys.ORG_GROUPS_NUM_ORGANIZATIONS_LABEL: _("Number of Product Types"), + OrganizationLabelsKeys.ORG_GROUPS_ADD_LABEL: _("Add Product Type Group"), + OrganizationLabelsKeys.ORG_GROUPS_ADD_SUCCESS_MESSAGE: _("Product Type groups added successfully."), + OrganizationLabelsKeys.ORG_GROUPS_UPDATE_LABEL: _("Edit Product Type Group"), + OrganizationLabelsKeys.ORG_GROUPS_UPDATE_SUCCESS_MESSAGE: _("Product Type group updated successfully."), + OrganizationLabelsKeys.ORG_GROUPS_DELETE_LABEL: _("Delete Product Type Group"), + OrganizationLabelsKeys.ORG_GROUPS_DELETE_SUCCESS_MESSAGE: _("Product Type group deleted successfully."), + OrganizationLabelsKeys.ORG_CREATE_LABEL: _("Add Product Type"), + OrganizationLabelsKeys.ORG_CREATE_SUCCESS_MESSAGE: _("Product Type added successfully."), + OrganizationLabelsKeys.ORG_READ_LABEL: _("View Product Type"), + OrganizationLabelsKeys.ORG_READ_LIST_LABEL: _("List Product Types"), + OrganizationLabelsKeys.ORG_UPDATE_LABEL: _("Edit Product Type"), + OrganizationLabelsKeys.ORG_UPDATE_WITH_NAME_LABEL: _("Edit Product Type %(name)s"), + OrganizationLabelsKeys.ORG_UPDATE_SUCCESS_MESSAGE: _("Product Type updated successfully."), + OrganizationLabelsKeys.ORG_DELETE_LABEL: _("Delete Product Type"), + OrganizationLabelsKeys.ORG_DELETE_WITH_NAME_LABEL: _("Delete Product Type %(name)s"), + OrganizationLabelsKeys.ORG_DELETE_CONFIRM_MESSAGE: _( + "Deleting this Product Type will remove any related objects associated with it. These relationships are listed below:"), + OrganizationLabelsKeys.ORG_DELETE_SUCCESS_MESSAGE: _("Product Type and relationships removed."), + OrganizationLabelsKeys.ORG_DELETE_SUCCESS_ASYNC_MESSAGE: _("Product Type and relationships will be removed in the background."), + OrganizationLabelsKeys.ORG_DELETE_WITH_NAME_SUCCESS_MESSAGE: _('The product type "%(name)s" was deleted'), + OrganizationLabelsKeys.ORG_DELETE_WITH_NAME_WITH_USER_SUCCESS_MESSAGE: _('The product type "%(name)s" was deleted by %(user)s'), + } diff --git a/dojo/organization/urls.py b/dojo/organization/urls.py new file mode 100644 index 00000000000..ceba6767d96 --- /dev/null +++ b/dojo/organization/urls.py @@ -0,0 +1,125 @@ +from django.conf import settings +from django.urls import re_path + +from dojo.product import views as product_views +from dojo.product_type import views +from dojo.utils import redirect_view + +# TODO: remove the else: branch once v3 migration is complete +if settings.ENABLE_V3_ORGANIZATION_ASSET_RELABEL: + urlpatterns = [ + re_path( + r"^organization$", + views.product_type, + name="product_type", + ), + re_path( + r"^organization/(?P\d+)$", + views.view_product_type, + name="view_product_type", + ), + re_path( + r"^organization/(?P\d+)/edit$", + views.edit_product_type, + name="edit_product_type", + ), + re_path( + r"^organization/(?P\d+)/delete$", + views.delete_product_type, + name="delete_product_type", + ), + re_path( + r"^organization/add$", + views.add_product_type, + name="add_product_type", + ), + re_path( + r"^organization/(?P\d+)/add_asset", + product_views.new_product, + name="add_product_to_product_type", + ), + re_path( + r"^organization/(?P\d+)/add_member$", + views.add_product_type_member, + name="add_product_type_member", + ), + re_path( + r"^organization/member/(?P\d+)/edit$", + views.edit_product_type_member, + name="edit_product_type_member", + ), + re_path( + r"^organization/member/(?P\d+)/delete$", + views.delete_product_type_member, + name="delete_product_type_member", + ), + re_path( + r"^organization/(?P\d+)/add_group$", + views.add_product_type_group, + name="add_product_type_group", + ), + re_path( + r"^organization/group/(?P\d+)/edit$", + views.edit_product_type_group, + name="edit_product_type_group", + ), + re_path( + r"^organization/group/(?P\d+)/delete$", + views.delete_product_type_group, + name="delete_product_type_group", + ), + # TODO: Backwards compatibility; remove after v3 migration is complete + re_path(r"^product/type$", redirect_view("product_type")), + re_path(r"^product/type/(?P\d+)$", redirect_view("view_product_type")), + re_path(r"^product/type/(?P\d+)/edit$", redirect_view("edit_product_type")), + re_path(r"^product/type/(?P\d+)/delete$", redirect_view("delete_product_type")), + re_path(r"^product/type/add$", redirect_view("add_product_type")), + re_path(r"^product/type/(?P\d+)/add_product", redirect_view("add_product_to_product_type")), + re_path(r"^product/type/(?P\d+)/add_member$", redirect_view("add_product_type_member")), + re_path(r"^product/type/member/(?P\d+)/edit$", redirect_view("edit_product_type_member")), + re_path(r"^product/type/member/(?P\d+)/delete$", redirect_view("delete_product_type_member")), + re_path(r"^product/type/(?P\d+)/add_group$", redirect_view("add_product_type_group")), + re_path(r"^product/type/group/(?P\d+)/edit$", redirect_view("edit_product_type_group")), + re_path(r"^product/type/group/(?P\d+)/delete$", redirect_view("delete_product_type_group")), + ] +else: + urlpatterns = [ + # product type + re_path(r"^product/type$", views.product_type, name="product_type"), + re_path(r"^product/type/(?P\d+)$", + views.view_product_type, name="view_product_type"), + re_path(r"^product/type/(?P\d+)/edit$", + views.edit_product_type, name="edit_product_type"), + re_path(r"^product/type/(?P\d+)/delete$", + views.delete_product_type, name="delete_product_type"), + re_path(r"^product/type/add$", views.add_product_type, + name="add_product_type"), + re_path(r"^product/type/(?P\d+)/add_product", + product_views.new_product, + name="add_product_to_product_type"), + re_path(r"^product/type/(?P\d+)/add_member$", views.add_product_type_member, + name="add_product_type_member"), + re_path(r"^product/type/member/(?P\d+)/edit$", views.edit_product_type_member, + name="edit_product_type_member"), + re_path(r"^product/type/member/(?P\d+)/delete$", views.delete_product_type_member, + name="delete_product_type_member"), + re_path(r"^product/type/(?P\d+)/add_group$", views.add_product_type_group, + name="add_product_type_group"), + re_path(r"^product/type/group/(?P\d+)/edit$", views.edit_product_type_group, + name="edit_product_type_group"), + re_path(r"^product/type/group/(?P\d+)/delete$", views.delete_product_type_group, + name="delete_product_type_group"), + # Forward compatibility + re_path(r"^organization$", redirect_view("product_type")), + re_path(r"^organization/(?P\d+)$", redirect_view("view_product_type")), + re_path(r"^organization/(?P\d+)/edit$", redirect_view("edit_product_type")), + re_path(r"^organization/(?P\d+)/delete$", redirect_view("delete_product_type")), + re_path(r"^organization/add$", redirect_view("add_product_type")), + re_path(r"^organization/(?P\d+)/add_product", redirect_view("add_product_to_product_type")), + re_path(r"^organization/(?P\d+)/add_member$", redirect_view("add_product_type_member")), + re_path(r"^organization/member/(?P\d+)/edit$", redirect_view("edit_product_type_member")), + re_path(r"^organization/member/(?P\d+)/delete$", redirect_view("delete_product_type_member")), + re_path(r"^organization/(?P\d+)/add_group$", redirect_view("add_product_type_group")), + re_path(r"^organization/group/(?P\d+)/edit$", redirect_view("edit_product_type_group")), + re_path(r"^organization/group/(?P\d+)/delete$", redirect_view("delete_product_type_group")), + ] diff --git a/dojo/product/signals.py b/dojo/product/signals.py index 61678e26a28..b0e9b999cc5 100644 --- a/dojo/product/signals.py +++ b/dojo/product/signals.py @@ -8,9 +8,12 @@ from django.urls import reverse from django.utils.translation import gettext as _ +from dojo.labels import get_labels from dojo.models import Product from dojo.notifications.helper import create_notification +labels = get_labels() + @receiver(post_save, sender=Product) def product_post_save(sender, instance, created, **kwargs): @@ -27,15 +30,14 @@ def product_post_save(sender, instance, created, **kwargs): def product_post_delete(sender, instance, **kwargs): # Catch instances in async delete where a single object is deleted more than once with contextlib.suppress(sender.DoesNotExist): - description = _('The product "%(name)s" was deleted') % {"name": instance.name} + description = labels.ASSET_DELETE_WITH_NAME_SUCCESS_MESSAGE % {"name": instance.name} if settings.ENABLE_AUDITLOG: if le := LogEntry.objects.filter( action=LogEntry.Action.DELETE, content_type=ContentType.objects.get(app_label="dojo", model="product"), object_id=instance.id, ).order_by("-id").first(): - description = _('The product "%(name)s" was deleted by %(user)s') % { - "name": instance.name, "user": le.actor} + description = labels.ASSET_DELETE_WITH_NAME_WITH_USER_SUCCESS_MESSAGE % {"name": instance.name, "user": le.actor} create_notification(event="product_deleted", # template does not exists, it will default to "other" but this event name needs to stay because of unit testing title=_("Deletion of %(name)s") % {"name": instance.name}, description=description, diff --git a/dojo/product/urls.py b/dojo/product/urls.py deleted file mode 100644 index 8e3568e5905..00000000000 --- a/dojo/product/urls.py +++ /dev/null @@ -1,76 +0,0 @@ -from django.urls import re_path - -from dojo.engagement import views as dojo_engagement_views -from dojo.product import views - -urlpatterns = [ - # product - re_path(r"^product$", views.product, name="product"), - re_path(r"^product/(?P\d+)$", views.view_product, - name="view_product"), - re_path(r"^product/(?P\d+)/components$", views.view_product_components, - name="view_product_components"), - re_path(r"^product/(?P\d+)/engagements$", views.view_engagements, - name="view_engagements"), - re_path( - r"^product/(?P\d+)/import_scan_results$", - dojo_engagement_views.ImportScanResultsView.as_view(), - name="import_scan_results_prod"), - re_path(r"^product/(?P\d+)/metrics$", views.view_product_metrics, - name="view_product_metrics"), - re_path(r"^product/(?P\d+)/async_burndown_metrics$", views.async_burndown_metrics, - name="async_burndown_metrics"), - re_path(r"^product/(?P\d+)/edit$", views.edit_product, - name="edit_product"), - re_path(r"^product/(?P\d+)/delete$", views.delete_product, - name="delete_product"), - re_path(r"^product/add", views.new_product, name="new_product"), - re_path(r"^product/(?P\d+)/new_engagement$", views.new_eng_for_app, - name="new_eng_for_prod"), - re_path(r"^product/(?P\d+)/new_technology$", views.new_tech_for_prod, - name="new_tech_for_prod"), - re_path(r"^technology/(?P\d+)/edit$", views.edit_technology, - name="edit_technology"), - re_path(r"^technology/(?P\d+)/delete$", views.delete_technology, - name="delete_technology"), - re_path(r"^product/(?P\d+)/new_engagement/cicd$", views.new_eng_for_app_cicd, - name="new_eng_for_prod_cicd"), - re_path(r"^product/(?P\d+)/add_meta_data$", views.add_meta_data, - name="add_meta_data"), - re_path(r"^product/(?P\d+)/edit_notifications$", views.edit_notifications, - name="edit_notifications"), - re_path(r"^product/(?P\d+)/edit_meta_data$", views.edit_meta_data, - name="edit_meta_data"), - re_path( - r"^product/(?P\d+)/ad_hoc_finding$", - views.AdHocFindingView.as_view(), - name="ad_hoc_finding"), - re_path(r"^product/(?P\d+)/engagement_presets$", views.engagement_presets, - name="engagement_presets"), - re_path(r"^product/(?P\d+)/engagement_presets/(?P\d+)/edit$", views.edit_engagement_presets, - name="edit_engagement_presets"), - re_path(r"^product/(?P\d+)/engagement_presets/add$", views.add_engagement_presets, - name="add_engagement_presets"), - re_path(r"^product/(?P\d+)/engagement_presets/(?P\d+)/delete$", views.delete_engagement_presets, - name="delete_engagement_presets"), - re_path(r"^product/(?P\d+)/add_member$", views.add_product_member, - name="add_product_member"), - re_path(r"^product/member/(?P\d+)/edit$", views.edit_product_member, - name="edit_product_member"), - re_path(r"^product/member/(?P\d+)/delete$", views.delete_product_member, - name="delete_product_member"), - re_path(r"^product/(?P\d+)/add_api_scan_configuration$", views.add_api_scan_configuration, - name="add_api_scan_configuration"), - re_path(r"^product/(?P\d+)/view_api_scan_configurations$", views.view_api_scan_configurations, - name="view_api_scan_configurations"), - re_path(r"^product/(?P\d+)/edit_api_scan_configuration/(?P\d+)$", views.edit_api_scan_configuration, - name="edit_api_scan_configuration"), - re_path(r"^product/(?P\d+)/delete_api_scan_configuration/(?P\d+)$", views.delete_api_scan_configuration, - name="delete_api_scan_configuration"), - re_path(r"^product/(?P\d+)/add_group$", views.add_product_group, - name="add_product_group"), - re_path(r"^product/group/(?P\d+)/edit$", views.edit_product_group, - name="edit_product_group"), - re_path(r"^product/group/(?P\d+)/delete$", views.delete_product_group, - name="delete_product_group"), -] diff --git a/dojo/product/views.py b/dojo/product/views.py index 66d09475ef5..6884877398a 100644 --- a/dojo/product/views.py +++ b/dojo/product/views.py @@ -70,6 +70,7 @@ ProductNotificationsForm, SLA_Configuration, ) +from dojo.labels import get_labels from dojo.models import ( App_Analysis, Benchmark_Product_Summary, @@ -133,6 +134,8 @@ logger = logging.getLogger(__name__) +labels = get_labels() + def product(request): prods = get_authorized_products(Permissions.Product_View) @@ -159,7 +162,7 @@ def product(request): # Get benchmark types for the template benchmark_types = Benchmark_Type.objects.filter(enabled=True).order_by("name") - add_breadcrumb(title=_("Product List"), top_level=not len(request.GET), request=request) + add_breadcrumb(title=str(labels.ASSET_READ_LIST_LABEL), top_level=not len(request.GET), request=request) return render(request, "dojo/product.html", { "prod_list": prod_list, @@ -305,7 +308,7 @@ def view_product(request, pid): total = critical + high + medium + low + info - product_tab = Product_Tab(prod, title=_("Product"), tab="overview") + product_tab = Product_Tab(prod, title=str(labels.ASSET_LABEL), tab="overview") return render(request, "dojo/view_product_details.html", { "prod": prod, "product_tab": product_tab, @@ -338,7 +341,7 @@ def view_product(request, pid): @user_is_authorized(Product, Permissions.Component_View, "pid") def view_product_components(request, pid): prod = get_object_or_404(Product, id=pid) - product_tab = Product_Tab(prod, title=_("Product"), tab="components") + product_tab = Product_Tab(prod, title=str(labels.ASSET_LABEL), tab="components") separator = ", " # Get components ordered by component_name and concat component versions to the same row @@ -718,7 +721,7 @@ def view_product_metrics(request, pid): open_vulnerabilities = [["CWE-" + str(f.get("cwe")), f.get("count")] for f in open_vulnerabilities] all_vulnerabilities = [["CWE-" + str(f.get("cwe")), f.get("count")] for f in all_vulnerabilities] - product_tab = Product_Tab(prod, title=_("Product"), tab="metrics") + product_tab = Product_Tab(prod, title=str(labels.ASSET_LABEL), tab="metrics") return render(request, "dojo/product_metrics.html", { "prod": prod, @@ -920,7 +923,7 @@ def new_product(request, ptid=None): product = form.save() messages.add_message(request, messages.SUCCESS, - _("Product added successfully."), + labels.ASSET_CREATE_SUCCESS_MESSAGE, extra_tags="alert-success") success, jira_project_form = jira_helper.process_jira_project_form(request, product=product) error = not success @@ -967,7 +970,7 @@ def new_product(request, ptid=None): gform = GITHUB_Product_Form() if get_system_setting("enable_github") else None - add_breadcrumb(title=_("New Product"), top_level=False, request=request) + add_breadcrumb(title=str(labels.ASSET_CREATE_LABEL), top_level=False, request=request) return render(request, "dojo/new_product.html", {"form": form, "jform": jira_project_form, @@ -997,13 +1000,13 @@ def edit_product(request, pid): if form.is_valid(): initial_sla_config = Product.objects.get(pk=form.instance.id).sla_configuration form.save() - msg = "Product updated successfully." + msg = labels.ASSET_UPDATE_SUCCESS_MESSAGE # check if the SLA config was changed, append additional context to message if initial_sla_config != form.instance.sla_configuration: - msg += " All SLA expiration dates for findings within this product will be recalculated asynchronously for the newly assigned SLA configuration." + msg += " " + labels.ASSET_UPDATE_SLA_CHANGED_MESSAGE messages.add_message(request, messages.SUCCESS, - _(msg), + msg, extra_tags="alert-success") success, jform = jira_helper.process_jira_project_form(request, instance=jira_project, product=product) @@ -1040,7 +1043,7 @@ def edit_product(request, pid): else: gform = None - product_tab = Product_Tab(product, title=_("Edit Product"), tab="settings") + product_tab = Product_Tab(product, title=str(labels.ASSET_UPDATE_LABEL), tab="settings") return render(request, "dojo/edit_product.html", {"form": form, @@ -1064,9 +1067,9 @@ def delete_product(request, pid): if get_setting("ASYNC_OBJECT_DELETE"): async_del = async_delete() async_del.delete(product) - message = _("Product and relationships will be removed in the background.") + message = labels.ASSET_DELETE_SUCCESS_ASYNC_MESSAGE else: - message = _("Product and relationships removed.") + message = labels.ASSET_DELETE_SUCCESS_MESSAGE product.delete() messages.add_message(request, messages.SUCCESS, @@ -1086,11 +1089,12 @@ def delete_product(request, pid): collector.collect([product]) rels = collector.nested() - product_tab = Product_Tab(product, title=_("Product"), tab="settings") + product_tab = Product_Tab(product, title=str(labels.ASSET_LABEL), tab="settings") logger.debug("delete_product: GET RENDER") return render(request, "dojo/delete_product.html", { + "label_delete_with_name": labels.ASSET_DELETE_WITH_NAME_LABEL % {"name": product}, "product": product, "form": form, "product_tab": product_tab, @@ -1699,6 +1703,7 @@ def edit_notifications(request, pid): def add_product_member(request, pid): product = get_object_or_404(Product, pk=pid) memberform = Add_Product_MemberForm(initial={"product": product.id}) + page_name = str(labels.ASSET_USERS_MEMBER_ADD_LABEL) if request.method == "POST": memberform = Add_Product_MemberForm(request.POST, initial={"product": product.id}) if memberform.is_valid(): @@ -1720,11 +1725,12 @@ def add_product_member(request, pid): product_member.save() messages.add_message(request, messages.SUCCESS, - _("Product members added successfully."), + labels.ASSET_USERS_MEMBER_ADD_SUCCESS_MESSAGE, extra_tags="alert-success") return HttpResponseRedirect(reverse("view_product", args=(pid,))) - product_tab = Product_Tab(product, title=_("Add Product Member"), tab="settings") + product_tab = Product_Tab(product, title=page_name, tab="settings") return render(request, "dojo/new_product_member.html", { + "name": page_name, "product": product, "form": memberform, "product_tab": product_tab, @@ -1735,6 +1741,7 @@ def add_product_member(request, pid): def edit_product_member(request, memberid): member = get_object_or_404(Product_Member, pk=memberid) memberform = Edit_Product_MemberForm(instance=member) + page_name = str(labels.ASSET_USERS_MEMBER_UPDATE_LABEL) if request.method == "POST": memberform = Edit_Product_MemberForm(request.POST, instance=member) if memberform.is_valid(): @@ -1748,13 +1755,14 @@ def edit_product_member(request, memberid): memberform.save() messages.add_message(request, messages.SUCCESS, - _("Product member updated successfully."), + labels.ASSET_USERS_MEMBER_UPDATE_SUCCESS_MESSAGE, extra_tags="alert-success") if is_title_in_breadcrumbs("View User"): return HttpResponseRedirect(reverse("view_user", args=(member.user.id,))) return HttpResponseRedirect(reverse("view_product", args=(member.product.id,))) - product_tab = Product_Tab(member.product, title=_("Edit Product Member"), tab="settings") + product_tab = Product_Tab(member.product, title=page_name, tab="settings") return render(request, "dojo/edit_product_member.html", { + "name": page_name, "memberid": memberid, "form": memberform, "product_tab": product_tab, @@ -1765,6 +1773,7 @@ def edit_product_member(request, memberid): def delete_product_member(request, memberid): member = get_object_or_404(Product_Member, pk=memberid) memberform = Delete_Product_MemberForm(instance=member) + page_name = str(labels.ASSET_USERS_MEMBER_DELETE_LABEL) if request.method == "POST": memberform = Delete_Product_MemberForm(request.POST, instance=member) member = memberform.instance @@ -1772,15 +1781,16 @@ def delete_product_member(request, memberid): member.delete() messages.add_message(request, messages.SUCCESS, - _("Product member deleted successfully."), + labels.ASSET_USERS_MEMBER_DELETE_SUCCESS_MESSAGE, extra_tags="alert-success") if is_title_in_breadcrumbs("View User"): return HttpResponseRedirect(reverse("view_user", args=(member.user.id,))) if user == request.user: return HttpResponseRedirect(reverse("product")) return HttpResponseRedirect(reverse("view_product", args=(member.product.id,))) - product_tab = Product_Tab(member.product, title=_("Delete Product Member"), tab="settings") + product_tab = Product_Tab(member.product, title=page_name, tab="settings") return render(request, "dojo/delete_product_member.html", { + "name": page_name, "memberid": memberid, "form": memberform, "product_tab": product_tab, @@ -1923,6 +1933,7 @@ def edit_product_group(request, groupid): logger.error(groupid) group = get_object_or_404(Product_Group, pk=groupid) groupform = Edit_Product_Group_Form(instance=group) + page_name = str(labels.ASSET_GROUPS_UPDATE_LABEL) if request.method == "POST": groupform = Edit_Product_Group_Form(request.POST, instance=group) @@ -1937,14 +1948,15 @@ def edit_product_group(request, groupid): groupform.save() messages.add_message(request, messages.SUCCESS, - _("Product group updated successfully."), + labels.ASSET_GROUPS_UPDATE_SUCCESS_MESSAGE, extra_tags="alert-success") if is_title_in_breadcrumbs("View Group"): return HttpResponseRedirect(reverse("view_group", args=(group.group.id,))) return HttpResponseRedirect(reverse("view_product", args=(group.product.id,))) - product_tab = Product_Tab(group.product, title=_("Edit Product Group"), tab="settings") + product_tab = Product_Tab(group.product, title=page_name, tab="settings") return render(request, "dojo/edit_product_group.html", { + "name": page_name, "groupid": groupid, "form": groupform, "product_tab": product_tab, @@ -1955,6 +1967,7 @@ def edit_product_group(request, groupid): def delete_product_group(request, groupid): group = get_object_or_404(Product_Group, pk=groupid) groupform = Delete_Product_GroupForm(instance=group) + page_name = str(labels.ASSET_GROUPS_DELETE_LABEL) if request.method == "POST": groupform = Delete_Product_GroupForm(request.POST, instance=group) @@ -1962,7 +1975,7 @@ def delete_product_group(request, groupid): group.delete() messages.add_message(request, messages.SUCCESS, - _("Product group deleted successfully."), + labels.ASSET_GROUPS_DELETE_SUCCESS_MESSAGE, extra_tags="alert-success") if is_title_in_breadcrumbs("View Group"): return HttpResponseRedirect(reverse("view_group", args=(group.group.id,))) @@ -1970,8 +1983,9 @@ def delete_product_group(request, groupid): # page return HttpResponseRedirect(reverse("view_product", args=(group.product.id,))) - product_tab = Product_Tab(group.product, title=_("Delete Product Group"), tab="settings") + product_tab = Product_Tab(group.product, title=page_name, tab="settings") return render(request, "dojo/delete_product_group.html", { + "name": page_name, "groupid": groupid, "form": groupform, "product_tab": product_tab, @@ -1982,6 +1996,7 @@ def delete_product_group(request, groupid): def add_product_group(request, pid): product = get_object_or_404(Product, pk=pid) group_form = Add_Product_GroupForm(initial={"product": product.id}) + page_name = str(labels.ASSET_GROUPS_ADD_LABEL) if request.method == "POST": group_form = Add_Product_GroupForm(request.POST, initial={"product": product.id}) @@ -2004,11 +2019,12 @@ def add_product_group(request, pid): product_group.save() messages.add_message(request, messages.SUCCESS, - _("Product groups added successfully."), + labels.ASSET_GROUPS_ADD_SUCCESS_MESSAGE, extra_tags="alert-success") return HttpResponseRedirect(reverse("view_product", args=(pid,))) - product_tab = Product_Tab(product, title=_("Edit Product Group"), tab="settings") + product_tab = Product_Tab(product, title=page_name, tab="settings") return render(request, "dojo/new_product_group.html", { + "name": page_name, "product": product, "form": group_form, "product_tab": product_tab, diff --git a/dojo/product_type/signals.py b/dojo/product_type/signals.py index b376de46845..3c67c24f2cd 100644 --- a/dojo/product_type/signals.py +++ b/dojo/product_type/signals.py @@ -8,9 +8,12 @@ from django.urls import reverse from django.utils.translation import gettext as _ +from dojo.labels import get_labels from dojo.models import Product_Type from dojo.notifications.helper import create_notification +labels = get_labels() + @receiver(post_save, sender=Product_Type) def product_type_post_save(sender, instance, created, **kwargs): @@ -27,15 +30,15 @@ def product_type_post_save(sender, instance, created, **kwargs): def product_type_post_delete(sender, instance, **kwargs): # Catch instances in async delete where a single object is deleted more than once with contextlib.suppress(sender.DoesNotExist): - description = _('The product type "%(name)s" was deleted') % {"name": instance.name} + + description = labels.ORG_DELETE_WITH_NAME_SUCCESS_MESSAGE % {"name": instance.name} if settings.ENABLE_AUDITLOG: if le := LogEntry.objects.filter( action=LogEntry.Action.DELETE, content_type=ContentType.objects.get(app_label="dojo", model="product_type"), object_id=instance.id, ).order_by("-id").first(): - description = _('The product type "%(name)s" was deleted by %(user)s') % { - "name": instance.name, "user": le.actor} + description = labels.ORG_DELETE_WITH_NAME_WITH_USER_SUCCESS_MESSAGE % {"name": instance.name, "user": le.actor} create_notification(event="product_type_deleted", # template does not exists, it will default to "other" but this event name needs to stay because of unit testing title=_("Deletion of %(name)s") % {"name": instance.name}, description=description, diff --git a/dojo/product_type/urls.py b/dojo/product_type/urls.py deleted file mode 100644 index dd64a5e4c06..00000000000 --- a/dojo/product_type/urls.py +++ /dev/null @@ -1,32 +0,0 @@ -from django.urls import re_path - -from dojo.product import views as product_views -from dojo.product_type import views - -urlpatterns = [ - # product type - re_path(r"^product/type$", views.product_type, name="product_type"), - re_path(r"^product/type/(?P\d+)$", - views.view_product_type, name="view_product_type"), - re_path(r"^product/type/(?P\d+)/edit$", - views.edit_product_type, name="edit_product_type"), - re_path(r"^product/type/(?P\d+)/delete$", - views.delete_product_type, name="delete_product_type"), - re_path(r"^product/type/add$", views.add_product_type, - name="add_product_type"), - re_path(r"^product/type/(?P\d+)/add_product", - product_views.new_product, - name="add_product_to_product_type"), - re_path(r"^product/type/(?P\d+)/add_member$", views.add_product_type_member, - name="add_product_type_member"), - re_path(r"^product/type/member/(?P\d+)/edit$", views.edit_product_type_member, - name="edit_product_type_member"), - re_path(r"^product/type/member/(?P\d+)/delete$", views.delete_product_type_member, - name="delete_product_type_member"), - re_path(r"^product/type/(?P\d+)/add_group$", views.add_product_type_group, - name="add_product_type_group"), - re_path(r"^product/type/group/(?P\d+)/edit$", views.edit_product_type_group, - name="edit_product_type_group"), - re_path(r"^product/type/group/(?P\d+)/delete$", views.delete_product_type_group, - name="delete_product_type_group"), -] diff --git a/dojo/product_type/views.py b/dojo/product_type/views.py index b15894f2500..28553db8cfc 100644 --- a/dojo/product_type/views.py +++ b/dojo/product_type/views.py @@ -26,6 +26,7 @@ Edit_Product_Type_MemberForm, Product_TypeForm, ) +from dojo.labels import get_labels from dojo.models import Finding, Product, Product_Type, Product_Type_Group, Product_Type_Member, Role from dojo.product.queries import get_authorized_products from dojo.product_type.queries import ( @@ -53,6 +54,8 @@ Product Type views """ +labels = get_labels() + def product_type(request): prod_types = get_authorized_product_types(Permissions.Product_Type_View) @@ -63,7 +66,7 @@ def product_type(request): pts.object_list = prefetch_for_product_type(pts.object_list) - page_name = _("Product Type List") + page_name = str(labels.ORG_READ_LIST_LABEL) add_breadcrumb(title=page_name, top_level=True, request=request) return render(request, "dojo/product_type.html", { @@ -100,7 +103,7 @@ def prefetch_for_product_type(prod_types): @user_has_global_permission(Permissions.Product_Type_Add) def add_product_type(request): - page_name = _("Add Product Type") + page_name = str(labels.ORG_CREATE_LABEL) form = Product_TypeForm() if request.method == "POST": form = Product_TypeForm(request.POST) @@ -113,7 +116,7 @@ def add_product_type(request): member.save() messages.add_message(request, messages.SUCCESS, - _("Product type added successfully."), + str(labels.ORG_CREATE_SUCCESS_MESSAGE), extra_tags="alert-success") return HttpResponseRedirect(reverse("product_type")) add_breadcrumb(title=page_name, top_level=False, request=request) @@ -126,7 +129,7 @@ def add_product_type(request): @user_is_authorized(Product_Type, Permissions.Product_Type_View, "ptid") def view_product_type(request, ptid): - page_name = _("View Product Type") + page_name = str(labels.ORG_READ_LABEL) pt = get_object_or_404(Product_Type, pk=ptid) members = get_authorized_members_for_product_type(pt, Permissions.Product_Type_View) global_members = get_authorized_global_members_for_product_type(pt, Permissions.Product_Type_View) @@ -163,9 +166,9 @@ def delete_product_type(request, ptid): if get_setting("ASYNC_OBJECT_DELETE"): async_del = async_delete() async_del.delete(product_type) - message = "Product Type and relationships will be removed in the background." + message = labels.ORG_DELETE_SUCCESS_ASYNC_MESSAGE else: - message = "Product Type and relationships removed." + message = labels.ORG_DELETE_SUCCESS_MESSAGE product_type.delete() messages.add_message(request, messages.SUCCESS, @@ -180,17 +183,17 @@ def delete_product_type(request, ptid): collector.collect([product_type]) rels = collector.nested() - add_breadcrumb(title=_("Delete Product Type"), top_level=False, request=request) - return render(request, "dojo/delete_product_type.html", - {"product_type": product_type, - "form": form, - "rels": rels, - }) + add_breadcrumb(title=str(labels.ORG_DELETE_LABEL), top_level=False, request=request) + return render(request, "dojo/delete_product_type.html", { + "label_delete_with_name": labels.ORG_DELETE_WITH_NAME_LABEL % {"name": product_type}, + "form": form, + "rels": rels, + }) @user_is_authorized(Product_Type, Permissions.Product_Type_Edit, "ptid") def edit_product_type(request, ptid): - page_name = "Edit Product Type" + page_name = str(labels.ORG_UPDATE_LABEL) pt = get_object_or_404(Product_Type, pk=ptid) members = get_authorized_members_for_product_type(pt, Permissions.Product_Type_Manage_Members) pt_form = Product_TypeForm(instance=pt) @@ -201,7 +204,7 @@ def edit_product_type(request, ptid): messages.add_message( request, messages.SUCCESS, - _("Product type updated successfully."), + labels.ORG_UPDATE_SUCCESS_MESSAGE, extra_tags="alert-success", ) return HttpResponseRedirect(reverse("product_type")) @@ -209,6 +212,7 @@ def edit_product_type(request, ptid): add_breadcrumb(title=page_name, top_level=False, request=request) return render(request, "dojo/edit_product_type.html", { "name": page_name, + "label_edit_with_name": labels.ORG_UPDATE_WITH_NAME_LABEL % {"name": pt.name}, "pt_form": pt_form, "pt": pt, "members": members}) @@ -216,6 +220,7 @@ def edit_product_type(request, ptid): @user_is_authorized(Product_Type, Permissions.Product_Type_Manage_Members, "ptid") def add_product_type_member(request, ptid): + page_name = str(labels.ORG_USERS_ADD_LABEL) pt = get_object_or_404(Product_Type, pk=ptid) memberform = Add_Product_Type_MemberForm(initial={"product_type": pt.id}) if request.method == "POST": @@ -238,11 +243,12 @@ def add_product_type_member(request, ptid): product_type_member.save() messages.add_message(request, messages.SUCCESS, - _("Product type members added successfully."), + labels.ORG_USERS_ADD_SUCCESS_MESSAGE, extra_tags="alert-success") return HttpResponseRedirect(reverse("view_product_type", args=(ptid, ))) - add_breadcrumb(title=_("Add Product Type Member"), top_level=False, request=request) + add_breadcrumb(title=page_name, top_level=False, request=request) return render(request, "dojo/new_product_type_member.html", { + "name": page_name, "pt": pt, "form": memberform, }) @@ -250,7 +256,7 @@ def add_product_type_member(request, ptid): @user_is_authorized(Product_Type_Member, Permissions.Product_Type_Manage_Members, "memberid") def edit_product_type_member(request, memberid): - page_name = _("Edit Product Type Member") + page_name = str(labels.ORG_USERS_UPDATE_LABEL) member = get_object_or_404(Product_Type_Member, pk=memberid) memberform = Edit_Product_Type_MemberForm(instance=member) if request.method == "POST": @@ -260,7 +266,8 @@ def edit_product_type_member(request, memberid): owners = Product_Type_Member.objects.filter(product_type=member.product_type, role__is_owner=True).exclude(id=member.id).count() if owners < 1: messages.add_message(request, messages.SUCCESS, - _("There must be at least one owner for Product Type %(product_type_name)s.") % {"product_type_name": member.product_type.name}, + labels.ORG_USERS_MINIMUM_NUMBER_WITH_NAME_MESSAGE + % {"name": member.product_type.name}, extra_tags="alert-warning") if is_title_in_breadcrumbs("View User"): return HttpResponseRedirect(reverse("view_user", args=(member.user.id, ))) @@ -274,7 +281,7 @@ def edit_product_type_member(request, memberid): memberform.save() messages.add_message(request, messages.SUCCESS, - _("Product type member updated successfully."), + labels.ORG_USERS_UPDATE_SUCCESS_MESSAGE, extra_tags="alert-success") if is_title_in_breadcrumbs("View User"): return HttpResponseRedirect(reverse("view_user", args=(member.user.id, ))) @@ -289,7 +296,7 @@ def edit_product_type_member(request, memberid): @user_is_authorized(Product_Type_Member, Permissions.Product_Type_Member_Delete, "memberid") def delete_product_type_member(request, memberid): - page_name = "Delete Product Type Member" + page_name = str(labels.ORG_USERS_DELETE_LABEL) member = get_object_or_404(Product_Type_Member, pk=memberid) memberform = Delete_Product_Type_MemberForm(instance=member) if request.method == "POST": @@ -308,7 +315,7 @@ def delete_product_type_member(request, memberid): member.delete() messages.add_message(request, messages.SUCCESS, - _("Product type member deleted successfully."), + labels.ORG_USERS_DELETE_SUCCESS_MESSAGE, extra_tags="alert-success") if is_title_in_breadcrumbs("View User"): return HttpResponseRedirect(reverse("view_user", args=(member.user.id, ))) @@ -325,7 +332,7 @@ def delete_product_type_member(request, memberid): @user_is_authorized(Product_Type, Permissions.Product_Type_Group_Add, "ptid") def add_product_type_group(request, ptid): - page_name = "Add Product Type Group" + page_name = str(labels.ORG_GROUPS_ADD_LABEL) pt = get_object_or_404(Product_Type, pk=ptid) group_form = Add_Product_Type_GroupForm(initial={"product_type": pt.id}) @@ -349,7 +356,7 @@ def add_product_type_group(request, ptid): product_type_group.save() messages.add_message(request, messages.SUCCESS, - _("Product type groups added successfully."), + labels.ORG_GROUPS_ADD_SUCCESS_MESSAGE, extra_tags="alert-success") return HttpResponseRedirect(reverse("view_product_type", args=(ptid,))) @@ -363,7 +370,7 @@ def add_product_type_group(request, ptid): @user_is_authorized(Product_Type_Group, Permissions.Product_Type_Group_Edit, "groupid") def edit_product_type_group(request, groupid): - page_name = "Edit Product Type Group" + page_name = str(labels.ORG_GROUPS_UPDATE_LABEL) group = get_object_or_404(Product_Type_Group, pk=groupid) groupform = Edit_Product_Type_Group_Form(instance=group) @@ -379,7 +386,7 @@ def edit_product_type_group(request, groupid): groupform.save() messages.add_message(request, messages.SUCCESS, - _("Product type group updated successfully."), + labels.ORG_GROUPS_UPDATE_SUCCESS_MESSAGE, extra_tags="alert-success") if is_title_in_breadcrumbs("View Group"): return HttpResponseRedirect(reverse("view_group", args=(group.group.id,))) @@ -395,7 +402,7 @@ def edit_product_type_group(request, groupid): @user_is_authorized(Product_Type_Group, Permissions.Product_Type_Group_Delete, "groupid") def delete_product_type_group(request, groupid): - page_name = "Delete Product Type Group" + page_name = str(labels.ORG_GROUPS_DELETE_LABEL) group = get_object_or_404(Product_Type_Group, pk=groupid) groupform = Delete_Product_Type_GroupForm(instance=group) @@ -405,7 +412,7 @@ def delete_product_type_group(request, groupid): group.delete() messages.add_message(request, messages.SUCCESS, - _("Product type group deleted successfully."), + labels.ORG_GROUPS_DELETE_SUCCESS_MESSAGE, extra_tags="alert-success") if is_title_in_breadcrumbs("View Group"): return HttpResponseRedirect(reverse("view_group", args=(group.group.id, ))) diff --git a/dojo/reports/urls.py b/dojo/reports/urls.py index e6f8cd166cc..a12858c840d 100644 --- a/dojo/reports/urls.py +++ b/dojo/reports/urls.py @@ -1,39 +1,136 @@ +from django.conf import settings from django.urls import re_path from dojo.reports import views +from dojo.utils import redirect_view -urlpatterns = [ - # reports - re_path(r"^product/type/(?P\d+)/report$", - views.product_type_report, name="product_type_report"), - re_path(r"^product/(?P\d+)/report$", - views.product_report, name="product_report"), - re_path(r"^product/(?P\d+)/endpoint/report$", - views.product_endpoint_report, name="product_endpoint_report"), - re_path(r"^engagement/(?P\d+)/report$", views.engagement_report, - name="engagement_report"), - re_path(r"^test/(?P\d+)/report$", views.test_report, - name="test_report"), - re_path(r"^endpoint/(?P\d+)/report$", views.endpoint_report, - name="endpoint_report"), - re_path(r"^endpoint/host/(?P\d+)/report$", views.endpoint_host_report, - name="endpoint_host_report"), - re_path(r"^product/report$", - views.product_findings_report, name="product_findings_report"), - re_path(r"^reports/cover$", - views.report_cover_page, name="report_cover_page"), - re_path(r"^reports/builder$", - views.ReportBuilder.as_view(), name="report_builder"), - re_path(r"^reports/findings$", - views.report_findings, name="report_findings"), - re_path(r"^reports/endpoints$", - views.report_endpoints, name="report_endpoints"), - re_path(r"^reports/custom$", - views.CustomReport.as_view(), name="custom_report"), - re_path(r"^reports/quick$", - views.QuickReportView.as_view(), name="quick_report"), - re_path(r"^reports/csv_export$", - views.CSVExportView.as_view(), name="csv_export"), - re_path(r"^reports/excel_export$", - views.ExcelExportView.as_view(), name="excel_export"), -] +# TODO: remove the else: branch once v3 migration is complete +if settings.ENABLE_V3_ORGANIZATION_ASSET_RELABEL: + urlpatterns = [ + re_path( + r"^organization/(?P\d+)/report$", + views.product_type_report, + name="product_type_report", + ), + re_path( + r"^asset/(?P\d+)/report$", + views.product_report, + name="product_report", + ), + re_path( + r"^asset/(?P\d+)/endpoint/report$", + views.product_endpoint_report, + name="product_endpoint_report", + ), + re_path( + r"^engagement/(?P\d+)/report$", + views.engagement_report, + name="engagement_report", + ), + re_path( + r"^test/(?P\d+)/report$", + views.test_report, + name="test_report", + ), + re_path( + r"^endpoint/(?P\d+)/report$", + views.endpoint_report, + name="endpoint_report", + ), + re_path( + r"^endpoint/host/(?P\d+)/report$", + views.endpoint_host_report, + name="endpoint_host_report", + ), + re_path( + r"^asset/report$", + views.product_findings_report, + name="product_findings_report", + ), + re_path( + r"^reports/cover$", + views.report_cover_page, + name="report_cover_page", + ), + re_path( + r"^reports/builder$", + views.ReportBuilder.as_view(), + name="report_builder", + ), + re_path( + r"^reports/findings$", + views.report_findings, + name="report_findings", + ), + re_path( + r"^reports/endpoints$", + views.report_endpoints, + name="report_endpoints", + ), + re_path( + r"^reports/custom$", + views.CustomReport.as_view(), + name="custom_report", + ), + re_path( + r"^reports/quick$", + views.QuickReportView.as_view(), + name="quick_report", + ), + re_path( + r"^reports/csv_export$", + views.CSVExportView.as_view(), + name="csv_export", + ), + re_path( + r"^reports/excel_export$", + views.ExcelExportView.as_view(), + name="excel_export", + ), + # TODO: Backwards compatibility; remove after v3 migration is complete + re_path(r"^product/type/(?P\d+)/report$", redirect_view("product_type_report")), + re_path(r"^product/(?P\d+)/report$", redirect_view("product_report")), + re_path(r"^product/(?P\d+)/endpoint/report$", redirect_view("product_endpoint_report")), + re_path(r"^product/report$", redirect_view("product_findings_report")), + ] +else: + urlpatterns = [ + # reports + re_path(r"^product/type/(?P\d+)/report$", + views.product_type_report, name="product_type_report"), + re_path(r"^product/(?P\d+)/report$", + views.product_report, name="product_report"), + re_path(r"^product/(?P\d+)/endpoint/report$", + views.product_endpoint_report, name="product_endpoint_report"), + re_path(r"^engagement/(?P\d+)/report$", views.engagement_report, + name="engagement_report"), + re_path(r"^test/(?P\d+)/report$", views.test_report, + name="test_report"), + re_path(r"^endpoint/(?P\d+)/report$", views.endpoint_report, + name="endpoint_report"), + re_path(r"^endpoint/host/(?P\d+)/report$", views.endpoint_host_report, + name="endpoint_host_report"), + re_path(r"^product/report$", + views.product_findings_report, name="product_findings_report"), + re_path(r"^reports/cover$", + views.report_cover_page, name="report_cover_page"), + re_path(r"^reports/builder$", + views.ReportBuilder.as_view(), name="report_builder"), + re_path(r"^reports/findings$", + views.report_findings, name="report_findings"), + re_path(r"^reports/endpoints$", + views.report_endpoints, name="report_endpoints"), + re_path(r"^reports/custom$", + views.CustomReport.as_view(), name="custom_report"), + re_path(r"^reports/quick$", + views.QuickReportView.as_view(), name="quick_report"), + re_path(r"^reports/csv_export$", + views.CSVExportView.as_view(), name="csv_export"), + re_path(r"^reports/excel_export$", + views.ExcelExportView.as_view(), name="excel_export"), + # Forward compatibility + re_path(r"^organization/(?P\d+)/report$", redirect_view("product_type_report")), + re_path(r"^asset/(?P\d+)/report$", redirect_view("product_report")), + re_path(r"^asset/(?P\d+)/endpoint/report$", redirect_view("product_endpoint_report")), + re_path(r"^asset/report$", redirect_view("product_findings_report")), + ] diff --git a/dojo/reports/views.py b/dojo/reports/views.py index d5853c375b2..ae6a99804eb 100644 --- a/dojo/reports/views.py +++ b/dojo/reports/views.py @@ -27,6 +27,7 @@ from dojo.finding.queries import get_authorized_findings from dojo.finding.views import BaseListFindings from dojo.forms import ReportOptionsForm +from dojo.labels import get_labels from dojo.models import Dojo_User, Endpoint, Engagement, Finding, Product, Product_Type, Test from dojo.reports.widgets import ( CoverPage, @@ -51,6 +52,10 @@ logger = logging.getLogger(__name__) + +labels = get_labels() + + EXCEL_CHAR_LIMIT = 32767 @@ -192,6 +197,7 @@ def report_findings(request): "title_words": title_words, "component_words": component_words, "title": "finding-list", + "asset_label": labels.ASSET_LABEL, }) @@ -383,8 +389,8 @@ def generate_report(request, obj, *, host_view=False): if type(obj).__name__ == "Product_Type": product_type = obj template = "dojo/product_type_pdf_report.html" - report_name = "Product Type Report: " + str(product_type) - report_title = "Product Type Report" + report_name = labels.ORG_REPORT_WITH_NAME_TITLE % {"name": str(product_type)} + report_title = labels.ORG_REPORT_LABEL findings = report_finding_filter_class(request.GET, prod_type=product_type, queryset=prefetch_related_findings_for_report(Finding.objects.filter( test__engagement__product__prod_type=product_type))) products = Product.objects.filter(prod_type=product_type, @@ -433,8 +439,8 @@ def generate_report(request, obj, *, host_view=False): elif type(obj).__name__ == "Product": product = obj template = "dojo/product_pdf_report.html" - report_name = "Product Report: " + str(product) - report_title = "Product Report" + report_name = labels.ASSET_REPORT_WITH_NAME_TITLE % {"name": str(product)} + report_title = labels.ASSET_REPORT_LABEL findings = report_finding_filter_class(request.GET, product=product, queryset=prefetch_related_findings_for_report(Finding.objects.filter( test__engagement__product=product))) ids = set(finding.id for finding in findings.qs) # noqa: C401 @@ -605,7 +611,7 @@ def generate_report(request, obj, *, host_view=False): product_tab = Product_Tab(test.engagement.product, title="Test Report", tab="engagements") product_tab.setEngagement(test.engagement) elif product: - product_tab = Product_Tab(product, title="Product Report", tab="findings") + product_tab = Product_Tab(product, title=str(labels.ASSET_REPORT_LABEL), tab="findings") elif endpoints: if host_view: product_tab = Product_Tab(endpoint.product, title="Endpoint Host Report", tab="endpoints") diff --git a/dojo/reports/widgets.py b/dojo/reports/widgets.py index 0dc5df4e1bd..47e0c6afe19 100644 --- a/dojo/reports/widgets.py +++ b/dojo/reports/widgets.py @@ -18,6 +18,7 @@ ReportFindingFilterWithoutObjectLookups, ) from dojo.forms import CustomReportOptionsForm +from dojo.labels import get_labels from dojo.models import Endpoint, Finding from dojo.utils import get_page_items, get_system_setting, get_words_for_field @@ -26,6 +27,8 @@ to be included. Each widget will provide a set of options, reporesented by form elements, to be included. """ +labels = get_labels() + class CustomReportJsonForm(forms.Form): json = forms.CharField() @@ -299,6 +302,7 @@ def get_option_form(self): "request": self.request, "title": self.title, "extra_help": self.extra_help, + "asset_label": labels.ASSET_LABEL, }) return mark_safe(html) diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index d20465e6301..5a0b050a484 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -328,6 +328,8 @@ # For HTTP requests, how long connection is open before timeout # This settings apply only on requests performed by "requests" lib used in Dojo code (if some included lib is using "requests" as well, this does not apply there) DD_REQUESTS_TIMEOUT=(int, 30), + # Dictates if v3 org/asset relabeling (+url routing) will be enabled + DD_ENABLE_V3_ORGANIZATION_ASSET_RELABEL=(bool, False), ) @@ -774,6 +776,8 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param # DEFECTDOJO SPECIFIC # ------------------------------------------------------------------------------ +ENABLE_V3_ORGANIZATION_ASSET_RELABEL = env("DD_ENABLE_V3_ORGANIZATION_ASSET_RELABEL") + # Credential Key CREDENTIAL_AES_256_KEY = env("DD_CREDENTIAL_AES_256_KEY") DB_KEY = env("DD_CREDENTIAL_AES_256_KEY") @@ -866,6 +870,7 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param "dojo.context_processors.bind_alert_count", "dojo.context_processors.bind_announcement", "dojo.context_processors.session_expiry_notification", + "dojo.context_processors.labels", ], }, }, diff --git a/dojo/system_settings/labels.py b/dojo/system_settings/labels.py new file mode 100644 index 00000000000..bf23a667102 --- /dev/null +++ b/dojo/system_settings/labels.py @@ -0,0 +1,41 @@ +from django.conf import settings +from django.utils.translation import gettext_lazy as _ + + +class SystemSettingsLabelsKeys: + + """Directory of text copy used by the System_Settings model.""" + + SETTINGS_TRACKED_FILES_ENABLE_LABEL = "settings.tracked_files.enable_label" + SETTINGS_TRACKED_FILES_ENABLE_HELP = "settings.tracked_files.enable_help" + SETTINGS_ASSET_GRADING_ENFORCE_VERIFIED_LABEL = "settings.asset_grading.enforce_verified_label" + SETTINGS_ASSET_GRADING_ENFORCE_VERIFIED_HELP = "settings.asset_grading.enforce_verified_help" + SETTINGS_ASSET_GRADING_ENABLE_LABEL = "settings.asset_grading.enable_label" + SETTINGS_ASSET_GRADING_ENABLE_HELP = "settings.asset_grading.enable_help" + SETTINGS_ASSET_TAG_INHERITANCE_ENABLE_LABEL = "settings.asset_tag_inheritance.enable_label" + SETTINGS_ASSET_TAG_INHERITANCE_ENABLE_HELP = "settings.asset_tag_inheritance.enable_help" + + +# TODO: remove the else: branch once v3 migration is complete +if settings.ENABLE_V3_ORGANIZATION_ASSET_RELABEL: + labels = { + SystemSettingsLabelsKeys.SETTINGS_TRACKED_FILES_ENABLE_LABEL: _("Enable Tracked Asset Files"), + SystemSettingsLabelsKeys.SETTINGS_TRACKED_FILES_ENABLE_HELP: _("With this setting turned off, tracked Asset files will be disabled in the user interface."), + SystemSettingsLabelsKeys.SETTINGS_ASSET_GRADING_ENFORCE_VERIFIED_LABEL: _("Enforce Verified Status - Asset Grading"), + SystemSettingsLabelsKeys.SETTINGS_ASSET_GRADING_ENFORCE_VERIFIED_HELP: _("When enabled, findings must have a verified status to be considered as part of an Asset's grading."), + SystemSettingsLabelsKeys.SETTINGS_ASSET_GRADING_ENABLE_LABEL: _("Enable Asset Grading"), + SystemSettingsLabelsKeys.SETTINGS_ASSET_GRADING_ENABLE_HELP: _("Displays a grade letter next to an Asset to show the overall health."), + SystemSettingsLabelsKeys.SETTINGS_ASSET_TAG_INHERITANCE_ENABLE_LABEL: _("Enable Asset Tag Inheritance"), + SystemSettingsLabelsKeys.SETTINGS_ASSET_TAG_INHERITANCE_ENABLE_HELP: _("Enables Asset tag inheritance globally for all Assets. Any tags added on an Asset will automatically be added to all Engagements, Tests, and Findings."), + } +else: + labels = { + SystemSettingsLabelsKeys.SETTINGS_TRACKED_FILES_ENABLE_LABEL: _("Enable Product Tracking Files"), + SystemSettingsLabelsKeys.SETTINGS_TRACKED_FILES_ENABLE_HELP: _("With this setting turned off, the product tracking files will be disabled in the user interface."), + SystemSettingsLabelsKeys.SETTINGS_ASSET_GRADING_ENFORCE_VERIFIED_LABEL: _("Enforce Verified Status - Product Grading"), + SystemSettingsLabelsKeys.SETTINGS_ASSET_GRADING_ENFORCE_VERIFIED_HELP: _("When enabled, findings must have a verified status to be considered as part of a product's grading."), + SystemSettingsLabelsKeys.SETTINGS_ASSET_GRADING_ENABLE_LABEL: _("Enable Product Grading"), + SystemSettingsLabelsKeys.SETTINGS_ASSET_GRADING_ENABLE_HELP: _("Displays a grade letter next to a product to show the overall health."), + SystemSettingsLabelsKeys.SETTINGS_ASSET_TAG_INHERITANCE_ENABLE_LABEL: _("Enable Product Tag Inheritance"), + SystemSettingsLabelsKeys.SETTINGS_ASSET_TAG_INHERITANCE_ENABLE_HELP: _("Enables product tag inheritance globally for all products. Any tags added on a product will automatically be added to all Engagements, Tests, and Findings"), + } diff --git a/dojo/templates/base.html b/dojo/templates/base.html index b0a2b914ec4..c562b598cd9 100644 --- a/dojo/templates/base.html +++ b/dojo/templates/base.html @@ -233,33 +233,33 @@
  • -