From 2aa9bf735b9aa81c4838933d6a5d4f888cc49942 Mon Sep 17 00:00:00 2001 From: Brandon Sterne Date: Tue, 10 Dec 2024 12:29:05 -0800 Subject: [PATCH] snapshot of BT linear integration --- dojo/db_migrations/0219_linear_integration.py | 36 ++++ ...0_alter_linear_instance_addl_json_input.py | 18 ++ dojo/finding/urls.py | 2 + dojo/finding/views.py | 179 +++++++++++++++++ dojo/forms.py | 56 ++++++ dojo/linear/__init__.py | 0 dojo/linear/urls.py | 10 + dojo/linear/views.py | 184 ++++++++++++++++++ dojo/models.py | 36 ++++ dojo/templates/base.html | 7 + dojo/templates/dojo/create_linear_issue.html | 35 ++++ dojo/templates/dojo/delete_linear.html | 29 +++ dojo/templates/dojo/edit_linear.html | 18 ++ dojo/templates/dojo/linear.html | 95 +++++++++ dojo/templates/dojo/new_linear.html | 13 ++ dojo/templates/dojo/view_finding.html | 5 + dojo/templatetags/display_tags.py | 5 + dojo/urls.py | 2 + 18 files changed, 730 insertions(+) create mode 100644 dojo/db_migrations/0219_linear_integration.py create mode 100644 dojo/db_migrations/0220_alter_linear_instance_addl_json_input.py create mode 100644 dojo/linear/__init__.py create mode 100644 dojo/linear/urls.py create mode 100644 dojo/linear/views.py create mode 100644 dojo/templates/dojo/create_linear_issue.html create mode 100644 dojo/templates/dojo/delete_linear.html create mode 100644 dojo/templates/dojo/edit_linear.html create mode 100644 dojo/templates/dojo/linear.html create mode 100644 dojo/templates/dojo/new_linear.html diff --git a/dojo/db_migrations/0219_linear_integration.py b/dojo/db_migrations/0219_linear_integration.py new file mode 100644 index 00000000000..abd1394da1d --- /dev/null +++ b/dojo/db_migrations/0219_linear_integration.py @@ -0,0 +1,36 @@ +# Generated by Django 5.1.2 on 2024-12-05 00:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dojo', '0218_system_settings_enforce_verified_status_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='system_settings', + name='enable_linear', + field=models.BooleanField(default=False, verbose_name='Enable Linear integration'), + ), + migrations.CreateModel( + name='Linear_Issue', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('url', models.URLField(max_length=2000, unique=True)), + ('findings', models.ManyToManyField(related_name='linear_issues', to='dojo.finding')), + ], + ), + migrations.CreateModel( + name='Linear_Instance', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('instance_name', models.CharField(default='', help_text='Enter a name to give to this configuration', max_length=2000, unique=True)), + ('api_key', models.CharField(max_length=2000)), + ('team_id', models.CharField(default='', help_text='Enter the UUID of the Team where you want to file Issues', max_length=64)), + ('addl_json_input', models.JSONField(blank=True, default=dict, help_text='Enter any additional JSON input you want to use when creating new Issues', max_length=2000, null=True)), + ], + ), + ] diff --git a/dojo/db_migrations/0220_alter_linear_instance_addl_json_input.py b/dojo/db_migrations/0220_alter_linear_instance_addl_json_input.py new file mode 100644 index 00000000000..2817fd91e08 --- /dev/null +++ b/dojo/db_migrations/0220_alter_linear_instance_addl_json_input.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2024-12-05 20:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dojo', '0219_linear_integration'), + ] + + operations = [ + migrations.AlterField( + model_name='linear_instance', + name='addl_json_input', + field=models.JSONField(blank=True, default=dict, help_text='Enter any additional JSON input you want to use when creating new Issues, e.g. { "labelIds": ["your-label-uuid"] }', max_length=2000, null=True), + ), + ] diff --git a/dojo/finding/urls.py b/dojo/finding/urls.py index 3b59624029a..1d567888c6f 100644 --- a/dojo/finding/urls.py +++ b/dojo/finding/urls.py @@ -144,6 +144,8 @@ name="choose_finding_template_options"), re_path(r"^finding/(?P\d+)/(?P\d+)/apply_template_to_finding$", views.apply_template_to_finding, name="apply_template_to_finding"), + re_path(r"^finding/(?P\d+)/linear$", views.create_linear_issue, + name="create_linear_issue"), re_path(r"^finding/(?P\d+)/close$", views.close_finding, name="close_finding"), re_path(r"^finding/(?P\d+)/defect_review$", diff --git a/dojo/finding/views.py b/dojo/finding/views.py index ec25352a903..7eb0f63791f 100644 --- a/dojo/finding/views.py +++ b/dojo/finding/views.py @@ -18,6 +18,7 @@ from django.db.models.query import Prefetch from django.http import Http404, HttpRequest, HttpResponse, HttpResponseRedirect, JsonResponse, StreamingHttpResponse from django.shortcuts import get_object_or_404, render +from django.template import Context, Template from django.template.defaultfilters import pluralize from django.urls import reverse from django.utils import formats, timezone @@ -65,6 +66,7 @@ FindingTemplateForm, GITHUBFindingForm, JIRAFindingForm, + LinearFindingForm, MergeFindings, NoteForm, PromoteFindingForm, @@ -86,6 +88,8 @@ Finding_Template, GITHUB_Issue, GITHUB_PKey, + Linear_Instance, + Linear_Issue, Note_Type, NoteHistory, Notes, @@ -1212,6 +1216,181 @@ def post(self, request: HttpRequest, finding_id): return redirect_to_return_url_or_else(request, reverse("view_test", args=(finding.test.id,))) raise PermissionDenied +def linear_api_create_issue(post_body): + import requests + lid = int(post_body.get("linear")) + linear_instance = get_object_or_404(Linear_Instance, pk=lid) + + url = linear_instance.LINEAR_API_URL + + headers = { + "Authorization": linear_instance.api_key, + "Content-Type": "application/json" + } + + query = """ + mutation CreateIssue($input: IssueCreateInput!) { + issueCreate(input: $input) { + success + issue { + id + identifier + title + url + } + } + } + """ + + variables = { + "input": { + "teamId": linear_instance.team_id, + "title": post_body.get("title"), + "description": post_body.get("description") + } + } + # add any additional configuration that was specified (any falsey configs, e.g. + # [], {}, null, etc. won't be applied even though they can be stored as valid JSON + if linear_instance.addl_json_input: + for key, val in linear_instance.addl_json_input.items(): + variables["input"][key] = val + + payload = { + "query": query, + "variables": variables + } + + return requests.post(url, json=payload, headers=headers) + + +def linear_create_formatted_description(finding): + tmpl_desc = Template(""" +## Description + +{{ description }} + +## Affected Systems + +{{ repo }} +- {{ path }} + +{{ mitigation }} + """.strip()) + + repo = "[%s](%s)" % ( + finding.test.engagement.product.name, + finding.test.engagement.source_code_management_uri + ) + path = "" + if finding.file_path: + path = "[%s%s](%s)" % ( + finding.file_path, + ":"+str(finding.line) if finding.line else "", + finding.get_file_path_with_raw_link() + ) + + mitigation = "" + if finding.mitigation: + mitigation = (""" +## Mitigation +``` +%s +``` +""".strip() % finding.mitigation + ) + + return tmpl_desc.render( + Context({ + "description": finding.description, + "repo": repo, + "path": path, + "mitigation": mitigation + }) + ) + +# display a form to push a finding to Linear and create a new issue +@user_is_authorized(Finding, Permissions.Finding_Edit, "fid") +def create_linear_issue(request, fid): + finding = get_object_or_404(Finding, id=fid) + if request.method == "POST": + form = LinearFindingForm(request.POST) + if form.is_valid(): + response = linear_api_create_issue(request.POST) + # success creating issue + if response.status_code == 200: + resp_obj = response.json() + # 200 responses can represent an error in GraphQL, so we need to look + # in the response body + if "errors" in resp_obj: + error_msgs = [ + e.get("extensions", {}).get("userPresentableMessage", "Unknown Error") + for e in resp_obj.get("errors", []) + ] + messages.add_message( + request, + messages.ERROR, + "Error creating Linear issue: {}".format(", ".join(error_msgs)), + extra_tags="alert-danger", + ) + # successfully created the issue --> update the UI + else: + issue_url = resp_obj.get("data", {}).get("issueCreate", {}).get( + "issue", {}).get("url") + # keep track of linear issues + issue, cr = Linear_Issue.objects.get_or_create(url=issue_url) + # associate new linear issue with finding + issue.findings.add(finding) + # update UI + messages.add_message( + request, + messages.SUCCESS, + mark_safe( + "Linear issue created: {}".format( + issue_url, + resp_obj.get("data", {}).get("issueCreate", {}).get( + "issue", {}).get("identifier", "") + ) + ), + extra_tags="alert-success", + ) + # failure to create issue + else: + messages.add_message( + request, + messages.ERROR, + "Error creating Linear issue: {}".format(response.text), + extra_tags="alert-danger", + ) + return HttpResponseRedirect( + reverse("create_linear_issue", args=(finding.id,)), + ) + + # non-POST + else: + # build initial form with values, user can edit them if needed + form = LinearFindingForm( + initial = { + "title": finding.title, + "description": linear_create_formatted_description(finding) + } + ) + + product_tab = Product_Tab( + finding.test.engagement.product, title="Create Linear Issue", tab="findings", + ) + + return render( + request, + "dojo/create_linear_issue.html", + { + "finding": finding, + "product_tab": product_tab, + "active_tab": "findings", + "user": request.user, + "form": form, + "linear_issues": finding.linear_issues.all() + }, + ) @user_is_authorized(Finding, Permissions.Finding_Edit, "fid") def close_finding(request, fid): diff --git a/dojo/forms.py b/dojo/forms.py index 334a958e93f..740e1996181 100644 --- a/dojo/forms.py +++ b/dojo/forms.py @@ -71,6 +71,7 @@ JIRA_Instance, JIRA_Issue, JIRA_Project, + Linear_Instance, Note_Type, Notes, Notification_Webhooks, @@ -1306,6 +1307,30 @@ class Meta: exclude = ("reporter", "url", "numerical_severity", "active", "false_p", "verified", "endpoint_status", "cve", "inherited_tags", "duplicate", "out_of_scope", "under_review", "reviewers", "review_requested_by", "is_mitigated", "jira_creation", "jira_change", "planned_remediation_date", "planned_remediation_version", "effort_for_fixing") +class LinearFindingForm(forms.ModelForm): + title = forms.CharField(max_length=1000) + description = forms.CharField(widget=forms.Textarea) + linear = forms.ModelChoiceField( + required=True, + queryset = Linear_Instance.objects.all().order_by("instance_name") + ) + + class Meta: + fields = ["title", "description", "linear"] + model = Finding + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + queryset = Linear_Instance.objects.all().order_by("instance_name") + self.fields["linear"].queryset = queryset + + # if we only have one Linear instance, prefill and make the field read-only + if queryset.count() == 1: + single_instance = queryset.first() + self.fields["linear"].initial = single_instance + self.fields["linear"].widget.attrs['readonly'] = True + class FindingForm(forms.ModelForm): title = forms.CharField(max_length=1000) @@ -2438,6 +2463,28 @@ def clean(self): self.test_jira_connection() return self.cleaned_data +class LinearForm(forms.ModelForm): + class Meta: + model = Linear_Instance + fields = "__all__" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["team_id"].widget = forms.TextInput( + attrs={ + "placeholder": "https://developers.linear.app/docs/graphql/working-with-the-graphql-api" + } + ) + self.fields["addl_json_input"].label = "Additional JSON Input" + self.fields["addl_json_input"].widget = forms.Textarea(attrs={'rows': 3}) + self.fields["addl_json_input"].validators = [self.validate_addl_json] + self.fields["api_key"].widget = forms.PasswordInput(render_value=True) + if self.instance and self.instance.pk: + self.fields['api_key'].initial = self.instance.api_key + + def validate_addl_json(self, value): + if not isinstance(value, dict): + raise ValidationError("Value must be an object with key-value pairs.") class AdvancedJIRAForm(BaseJiraForm): issue_template_dir = forms.ChoiceField(required=False, @@ -2520,6 +2567,15 @@ class Meta: fields = ["id"] +class DeleteLinearInstanceForm(forms.ModelForm): + id = forms.IntegerField(required=True, + widget=forms.widgets.HiddenInput()) + + class Meta: + model = Linear_Instance + fields = ["id"] + + class DeleteJIRAInstanceForm(forms.ModelForm): id = forms.IntegerField(required=True, widget=forms.widgets.HiddenInput()) diff --git a/dojo/linear/__init__.py b/dojo/linear/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/dojo/linear/urls.py b/dojo/linear/urls.py new file mode 100644 index 00000000000..8f1bc9af3d3 --- /dev/null +++ b/dojo/linear/urls.py @@ -0,0 +1,10 @@ +from django.urls import re_path + +from . import views + +urlpatterns = [ + re_path(r"^linear/add", views.NewLinearView.as_view(), name="add_linear"), + re_path(r"^linear/(?P\d+)/edit$", views.EditLinearView.as_view(), name="edit_linear"), + re_path(r"^linear/(?P\d+)/delete$", views.DeleteLinearView.as_view(), name="delete_linear"), + re_path(r"^linear$", views.ListLinearView.as_view(), name="linear"), +] diff --git a/dojo/linear/views.py b/dojo/linear/views.py new file mode 100644 index 00000000000..f2b69c1cb1b --- /dev/null +++ b/dojo/linear/views.py @@ -0,0 +1,184 @@ +# Standard library imports +import datetime +import json +import logging + +# Third party imports +from django.contrib import messages +from django.contrib.admin.utils import NestedObjects +from django.core.exceptions import PermissionDenied +from django.db import DEFAULT_DB_ALIAS +from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.shortcuts import get_object_or_404, render +from django.urls import reverse +from django.views import View + +from dojo.authorization.authorization import user_has_configuration_permission + +# Local application/library imports +from dojo.forms import LinearForm, DeleteLinearInstanceForm +from dojo.models import Linear_Instance +from dojo.notifications.helper import create_notification +from dojo.utils import add_breadcrumb, add_error_message_to_response, get_setting + +logger = logging.getLogger(__name__) + +class ListLinearView(View): + def get_template(self): + return "dojo/linear.html" + + def get(self, request): + if not user_has_configuration_permission(request.user, "dojo.view_linear_instance"): + raise PermissionDenied + linear_instances = Linear_Instance.objects.all() + context = {"linear_instances": linear_instances} + add_breadcrumb(title="Linear List", top_level=not len(request.GET), request=request) + return render(request, self.get_template(), context) + + + +class NewLinearView(View): + def get_template(self): + return "dojo/new_linear.html" + + def get_form_class(self): + return LinearForm + + def get(self, request): + if not user_has_configuration_permission(request.user, "dojo.add_linear_instance"): + raise PermissionDenied + form = self.get_form_class()() + add_breadcrumb(title="New Linear Configuration", top_level=False, request=request) + return render(request, self.get_template(), {"form": form}) + + def post(self, request): + if not user_has_configuration_permission(request.user, "dojo.add_linear_instance"): + raise PermissionDenied + form = self.get_form_class()(request.POST, instance=Linear_Instance()) + if form.is_valid(): + # create the object + linear_instance = Linear_Instance( + instance_name=form.cleaned_data.get("instance_name").strip(), + team_id=form.cleaned_data.get("team_id").strip(), + api_key=form.cleaned_data.get("api_key").strip() + ) + linear_instance.save() + # update the UI + messages.add_message( + request, + messages.SUCCESS, + "Linear Configuration Successfully Created.", + extra_tags="alert-success" + ) + create_notification( + event="linear_config_added", + title=f"New Linear instance was added by {request.user}" + ) + return HttpResponseRedirect(reverse("linear")) + return render(request, self.get_template(), {"form": form}) + + + +class EditLinearView(View): + def get_template(self): + return "dojo/edit_linear.html" + + def get_form_class(self): + return LinearForm + + def get(self, request, lid=None): + if not user_has_configuration_permission(request.user, "dojo.change_linear_instance"): + raise PermissionDenied + linear = Linear_Instance.objects.get(pk=lid) + form = self.get_form_class()(instance=linear) + add_breadcrumb(title="Edit Linear Configuration", top_level=False, request=request) + return render(request, self.get_template(), {"form": form}) + + def post(self, request, lid=None): + if not user_has_configuration_permission(request.user, "dojo.change_linear_instance"): + raise PermissionDenied + linear = Linear_Instance.objects.get(pk=lid) + form = self.get_form_class()(request.POST, instance=linear) + if form.is_valid(): + linear.instance_name = form.cleaned_data.get("instance_name").strip() + linear.team_id = form.cleaned_data.get("team_id").strip() + linear.api_key = form.cleaned_data.get("api_key").strip() + linear.save() + messages.add_message( + request, + messages.SUCCESS, + "Linear Configuration Successfully Saved.", + extra_tags="alert-success" + ) + create_notification( + event="linear_config_edited", + title=f"Linear instance edited by {request.user}" + ) + return HttpResponseRedirect(reverse("linear")) + return render(request, self.get_template(), {"form": form}) + + + +class DeleteLinearView(View): + def get_template(self): + return "dojo/delete_linear.html" + + def get_form_class(self): + return DeleteLinearInstanceForm + + def get(self, request, lid=None): + if not user_has_configuration_permission(request.user, "dojo.delete_linear_instance"): + raise PermissionDenied + linear_instance = get_object_or_404(Linear_Instance, pk=lid) + form = self.get_form_class()(instance=linear_instance) + rels = ["Previewing the relationships has been disabled.", ""] + display_preview = get_setting("DELETE_PREVIEW") + if display_preview: + collector = NestedObjects(using=DEFAULT_DB_ALIAS) + collector.collect([linear_instance]) + rels = collector.nested() + + add_breadcrumb(title="Delete", top_level=False, request=request) + return render(request, self.get_template(), { + "inst": linear_instance, + "form": form, + "rels": rels, + "deletable_objects": rels, + }) + + def post(self, request, lid=None): + if not user_has_configuration_permission(request.user, "dojo.delete_linear_instance"): + raise PermissionDenied + linear_instance = get_object_or_404(Linear_Instance, pk=lid) + if "id" in request.POST and str(linear_instance.id) == request.POST["id"]: + form = self.get_form_class()(request.POST, instance=linear_instance) + if form.is_valid(): + try: + linear_instance.delete() + messages.add_message( + request, + messages.SUCCESS, + "Linear Instance and relationships removed.", + extra_tags="alert-success") + create_notification( + event="linear_config_deleted", + title=f"Linear instance deleted by {request.user}" + ) + return HttpResponseRedirect(reverse("linear")) + except Exception as e: + add_error_message_to_response(f"Unable to delete Linear Instance: {str(e)}") + + rels = ["Previewing the relationships has been disabled.", ""] + display_preview = get_setting("DELETE_PREVIEW") + if display_preview: + collector = NestedObjects(using=DEFAULT_DB_ALIAS) + collector.collect([linear_instance]) + rels = collector.nested() + + add_breadcrumb(title="Delete", top_level=False, request=request) + return render(request, self.get_template(), { + # "inst": linear_instance, + "form": form, + "rels": rels, + "deletable_objects": rels, + }) diff --git a/dojo/models.py b/dojo/models.py index 99074a9cf3b..d37649445ad 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -327,6 +327,10 @@ class System_Settings(models.Model): verbose_name=_("Add vulnerability Id as a JIRA label"), blank=False) + enable_linear = models.BooleanField(default=False, + verbose_name=_("Enable Linear integration"), + blank=False) + enable_github = models.BooleanField(default=False, verbose_name=_("Enable GITHUB integration"), blank=False) @@ -4017,6 +4021,36 @@ def set_obj(self, obj): raise TypeError(msg) +class Linear_Instance(models.Model): + LINEAR_API_URL = "https://api.linear.app/graphql" + + instance_name = models.CharField( + max_length=2000, + help_text=_("Enter a name to give to this configuration"), + default="", + unique=True + ) + team_id = models.CharField( + max_length=64, + help_text=_("Enter the UUID of the Team where you want to file Issues"), + default="" + ) + addl_json_input = models.JSONField( + max_length=2000, + blank=True, + null=True, + default=dict, + help_text=_('Enter any additional JSON input you want to use when creating new Issues, e.g. { "labelIds": ["your-label-uuid"] }') + ) + api_key = models.CharField(max_length=2000) + + def __str__(self): + return self.instance_name + +class Linear_Issue(models.Model): + url = models.URLField(max_length=2000, unique=True) + findings = models.ManyToManyField(Finding, related_name="linear_issues") + NOTIFICATION_CHOICE_SLACK = ("slack", "slack") NOTIFICATION_CHOICE_MSTEAMS = ("msteams", "msteams") NOTIFICATION_CHOICE_MAIL = ("mail", "mail") @@ -4666,6 +4700,8 @@ def __str__(self): admin.site.register(JIRA_Issue) admin.site.register(JIRA_Instance, JIRA_Instance_Admin) admin.site.register(JIRA_Project) +admin.site.register(Linear_Instance) +admin.site.register(Linear_Issue) admin.site.register(GITHUB_Conf) admin.site.register(GITHUB_Issue) admin.site.register(GITHUB_Clone) diff --git a/dojo/templates/base.html b/dojo/templates/base.html index 5470baf13bd..2de262c9a18 100644 --- a/dojo/templates/base.html +++ b/dojo/templates/base.html @@ -524,6 +524,13 @@ {% endif %} + {% if system_settings.enable_linear and "dojo.view_linear_instance"|has_configuration_permission:request %} +
  • + + {% trans "Linear" %} + +
  • + {% endif %} {% if "dojo.change_bannerconf"|has_configuration_permission:request %}
  • diff --git a/dojo/templates/dojo/create_linear_issue.html b/dojo/templates/dojo/create_linear_issue.html new file mode 100644 index 00000000000..210ac894880 --- /dev/null +++ b/dojo/templates/dojo/create_linear_issue.html @@ -0,0 +1,35 @@ +{% extends "base.html" %} +{% load event_tags %} +{% block content %} + {{ block.super }} +

    Create Linear Issue

    +

    {{ finding.title }}

    + +
    + {% csrf_token %} + {% include "dojo/form_fields.html" with form=form %} + + {% if linear_issues %} + + {% endif %} + +
    +
    + +
    +
    + +
    +{% endblock %} diff --git a/dojo/templates/dojo/delete_linear.html b/dojo/templates/dojo/delete_linear.html new file mode 100644 index 00000000000..553d95bf676 --- /dev/null +++ b/dojo/templates/dojo/delete_linear.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} +{% block content %} + {{ block.super }} +

    Delete Linear Instance: {{ inst.instance_name }}

    +

    + Deleting this configuration will remove any Linear product configurations, settings, and other relationships associated + with this configuration. These relationships are listed below: +

    +
    +
    +

    Danger Zone

    +
    + {% if rels|length > 1 %} +
      {{ rels|unordered_list }}
    + {% else %} +

    No relationships found.

    + {% endif %} +
    + {% csrf_token %} + {{ form }} + +
    + +
    +
    +
    +
    +
    +{% endblock %} diff --git a/dojo/templates/dojo/edit_linear.html b/dojo/templates/dojo/edit_linear.html new file mode 100644 index 00000000000..feeb991a7e9 --- /dev/null +++ b/dojo/templates/dojo/edit_linear.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% load static %} + +{% block content %} + {{ block.super }} +

    Edit Linear Configuration

    +
    {% csrf_token %} + {% include "dojo/form_fields.html" with form=form %} +
    +
    + +
    +
    +
    +{% endblock %} +{% block postscript %} + {{ block.super }} +{% endblock %} diff --git a/dojo/templates/dojo/linear.html b/dojo/templates/dojo/linear.html new file mode 100644 index 00000000000..9ca5d40629d --- /dev/null +++ b/dojo/templates/dojo/linear.html @@ -0,0 +1,95 @@ +{% extends "base.html" %} +{% load navigation_tags %} +{% load authorization_tags %} +{% load display_tags %} +{% block content %} + {{ block.super }} +
    +
    +
    +
    +

    + Linear Instances + +

    +
    + +
    + {% if linear_instances %} + +
    + {% include "dojo/paging_snippet.html" with page=linear_instances page_size=True %} +
    +
    + + + + + + + + + + + {% for linear_instance in linear_instances %} + + + + + + + {% endfor %} + +
    NameTeam IDAPI KeyAction
    + {% if "dojo.change_linear_instance"|has_configuration_permission:request %} + {{ linear_instance.instance_name }} + {% else %} + {{ linear_instance.instance_name }} + {% endif %} + {{ linear_instance.team_id }} + {{ linear_instance.api_key|mask_secret }} + + {% if "dojo.change_linear_instance"|has_configuration_permission:request %} + + + Edit + + {% endif %} + {% if "dojo.delete_linear_instance"|has_configuration_permission:request %} + + + Delete + + {% endif %} +
    +
    +
    + {% include "dojo/paging_snippet.html" with page=linear_instances page_size=True %} +
    + {% else %} +

    No Linear instances found.

    + {% endif %} +
    +
    +{% endblock %} +{% block postscript %} + {{ block.super }} + {% include "dojo/filter_js_snippet.html" %} +{% endblock %} diff --git a/dojo/templates/dojo/new_linear.html b/dojo/templates/dojo/new_linear.html new file mode 100644 index 00000000000..a1d89292d80 --- /dev/null +++ b/dojo/templates/dojo/new_linear.html @@ -0,0 +1,13 @@ +{% extends "base.html"%} +{% block content %} + {{ block.super }} +

    Add a Linear Configuration

    +
    {% csrf_token %} + {% include "dojo/form_fields.html" with form=form %} +
    +
    + +
    +
    +
    +{% endblock %} diff --git a/dojo/templates/dojo/view_finding.html b/dojo/templates/dojo/view_finding.html index 03842eff781..01def191279 100755 --- a/dojo/templates/dojo/view_finding.html +++ b/dojo/templates/dojo/view_finding.html @@ -151,6 +151,11 @@

    Close Finding

  • +
  • + + Create Linear Issue + +
  • {% endif %} {% endif %} {% if finding|has_object_permission:"Risk_Acceptance" %} diff --git a/dojo/templatetags/display_tags.py b/dojo/templatetags/display_tags.py index edcc109ffbf..af39dd69dc5 100644 --- a/dojo/templatetags/display_tags.py +++ b/dojo/templatetags/display_tags.py @@ -1033,3 +1033,8 @@ def import_history(finding, autoescape=True): list_of_status_changes += "" + status_change.created.strftime("%b %d, %Y, %H:%M:%S") + ": " + status_change.get_action_display() + "
    " return mark_safe(html % (list_of_status_changes)) + + +@register.filter +def mask_secret(secret, display_chars=6): + return "%s%s" % (secret[:display_chars], "*" * (len(secret) - display_chars)) diff --git a/dojo/urls.py b/dojo/urls.py index 1e36b67d4f8..49230cd047d 100644 --- a/dojo/urls.py +++ b/dojo/urls.py @@ -86,6 +86,7 @@ from dojo.group.urls import urlpatterns as group_urls from dojo.home.urls import urlpatterns as home_urls from dojo.jira_link.urls import urlpatterns as jira_urls +from dojo.linear.urls import urlpatterns as linear_urls from dojo.metrics.urls import urlpatterns as metrics_urls from dojo.note_type.urls import urlpatterns as note_type_urls from dojo.notes.urls import urlpatterns as notes_urls @@ -194,6 +195,7 @@ ur += user_urls ur += group_urls ur += jira_urls +ur += linear_urls ur += github_urls ur += tool_type_urls ur += tool_config_urls