diff --git a/docs/faq.rst b/docs/faq.rst index 7450989b..50d10508 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -15,3 +15,19 @@ How can I download SBOM for my products? the product details view in the web UI or from dedicated endpoint URLs of the REST API. Refer to :ref:`how_to_3` for more details. + +How can I integrate DejaCode with my tools or applications? +----------------------------------------------------------- + +DejaCode supports three main integration approaches: + +- **Platform-specific integrations** for GitHub, GitLab, Jira, SourceHut, and Forgejo + (:ref:`platform_specific_integrations`) +- The **REST API** for programmatic access to data and operations + (:ref:`rest_api_integration`) +- **Webhooks** for receiving real-time event notifications + (:ref:`webhook_integration`) + +The last two options — REST API and webhooks — enable **generic integration** with +virtually any application or service, giving you full flexibility to connect +DejaCode to your existing tools and workflows. diff --git a/docs/index.rst b/docs/index.rst index bddd04b5..15b41b86 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -48,11 +48,14 @@ Welcome to the very start of your DejaCode journey! :maxdepth: 1 :caption: Integrations + integrations-introduction integrations-forgejo integrations-github integrations-gitlab integrations-jira integrations-sourcehut + integrations-rest-api + integrations-webhook .. toctree:: :maxdepth: 1 diff --git a/docs/integrations-introduction.rst b/docs/integrations-introduction.rst new file mode 100644 index 00000000..a4964257 --- /dev/null +++ b/docs/integrations-introduction.rst @@ -0,0 +1,100 @@ +.. _integrations_introduction: + +Integrations overview +===================== + +DejaCode offers several ways to connect with other tools and services, enabling +**automation**, **synchronization**, and **streamlined workflows**. Depending on your +needs, you can choose from :ref:`platform_specific_integrations`, the +:ref:`rest_api_integration`, or the :ref:`webhook_integration`. + +.. _platform_specific_integrations: + +Platform-specific integrations +------------------------------ + +DejaCode provides built-in support for the following platforms: + +- :ref:`integrations_github` +- :ref:`integrations_gitlab` +- :ref:`integrations_jira` +- :ref:`integrations_sourcehut` +- :ref:`integrations_forgejo` + +These integrations are designed to work **seamlessly** with each platform's features. +They typically allow **requests**, **comments**, and **status changes** in DejaCode to +be linked or synchronized with corresponding items in the external platform, such as +**issues** or **tickets**. + +Platform-specific integrations are the best choice when: + +- Your team already uses **one of the supported platforms** +- You want **minimal setup**, with features mapped directly between systems +- You prefer a **native, optimized experience** rather than building custom logic + +.. _rest_api_integration: + +REST API +-------- + +The :ref:`integrations_rest_api` provides **full programmatic access** to most features +of the platform. This makes it possible to integrate DejaCode with **any script, +application, or automation system**, regardless of the programming language or +framework. + +With the REST API, you can: + +- **Create, update, and retrieve** requests and related objects +- **Automate** administrative tasks +- Pull data into **reporting** or **analytics tools** +- Build **custom user interfaces** on top of DejaCode data + +This approach offers **maximum flexibility**, but requires you to write the logic for +**handling events**, **processing data**, and **authenticating** with the API. + +.. _webhook_integration: + +Webhook integration +------------------- + +:ref:`integrations_webhook` allow DejaCode to **push** information to an **external +system** the moment specific events occur, instead of requiring you to **poll** the +API. + +When a configured event happens (such as a **request** being created or updated), +DejaCode sends an HTTP ``POST`` request with a **JSON payload** to your **target URL**. +You can then process this payload to **trigger automation**, **update another system**, +or **log the change**. + +Webhooks can be configured for a **variety of events**, and the payload can be +extended with **custom fields** and **headers**. They are especially powerful when +combined with the REST API — **webhooks deliver the trigger**, and **API calls perform +follow-up actions**. + +Generic integrations +-------------------- + +While platform-specific integrations focus on **GitHub**, **GitLab**, **Jira**, +**SourceHut**, and **Forgejo**, both the :ref:`rest_api_integration` and +:ref:`webhook_integration` provide the tools to connect DejaCode to **virtually any +application or service**. + +Examples include: + +- Pushing updates to a **Slack** channel or **Microsoft Teams** +- Updating **internal dashboards** +- Triggering **security scans** or **CI/CD jobs** +- Synchronizing data with **proprietary in-house systems** + +Choosing the right approach +--------------------------- + +- Use a :ref:`platform_specific_integrations` integration if your workflow centers on + **one of the supported platforms** and you want the **easiest setup**. +- Use the :ref:`rest_api_integration` for **full control** and **flexibility** over + how DejaCode interacts with other systems. +- Use :ref:`webhook_integration` to receive **real-time notifications** and act + immediately on events. +- Combine :ref:`webhook_integration` with the :ref:`rest_api_integration` for + **event-driven automation** that can **react** and then **fetch or update** related + data as needed. diff --git a/docs/integrations-rest-api.rst b/docs/integrations-rest-api.rst new file mode 100644 index 00000000..24b29e93 --- /dev/null +++ b/docs/integrations-rest-api.rst @@ -0,0 +1,228 @@ +.. _integrations_rest_api: + +REST API Integration +==================== + +DejaCode offers a REST API to allow integration with external applications in a +generic way. You can use it to fetch, create, and update DejaCode Requests +from your own scripts or applications. + +The full REST API documentation is also available in the DejaCode web UI under +**Tools > API Documentation**. + +This guide focuses specifically on interacting with **DejaCode Requests**. + +.. note:: + + Example HTTP requests assume the DejaCode URL is ``https://localhost``. + Replace with your actual instance URL. + +Prerequisites +------------- + +- A **DejaCode API Key**, available from your **Profile** settings page. + +Authentication +-------------- + +Include your **API Key** in the "Authorization" HTTP header for every request. +The key must be prefixed by the string literal ``Token`` followed by a space: + + Authorization: Token abcdef123456 + +.. warning:: + Treat your API key like a password — keep it secret and secure. + +Example using cURL:: + + curl -X GET \ + https://localhost/api/v2/requests/ \ + -H "Authorization: Token abcdef123456" + +Example using Python:: + + import requests + + api_url = "https://localhost/api/v2/requests/" + headers = {"Authorization": "Token abcdef123456"} + params = {"page": "2"} + response = requests.get(api_url, headers=headers, params=params) + print(response.json()) + +Request List +------------ + +**Endpoint:** ``GET /api/v2/requests/`` + +This endpoint lists all requests. Responses include pagination fields ``next`` +and ``previous`` to navigate through pages. + +You can sort the list using ``?ordering=FIELD``. Prefix a field with ``-`` to +reverse the sort order (descending). Available fields: + +- ``title`` +- ``request_template`` +- ``status`` +- ``priority`` +- ``assignee`` +- ``requester`` +- ``created_date`` +- ``last_modified_date`` + +Filtering is supported using ``FIELD=VALUE`` syntax. Available filters include: + +- ``request_template`` +- ``status`` +- ``requester`` +- ``assignee`` +- ``priority`` +- ``content_type`` +- ``last_modified_date`` + +Example: Get closed requests sorted by last modification date:: + + api_url = "https://localhost/api/v2/requests/" + headers = {"Authorization": "Token abcdef123456"} + params = {"status": "closed", "ordering": "last_modified_date"} + response = requests.get(api_url, headers=headers, params=params) + print(response.json()) + +Request Details +--------------- + +**Endpoint:** ``GET /api/v2/requests/{uuid}/`` + +Returns all available information for a specific request. Replace ``{uuid}`` +with the UUID of the request you want to retrieve. + +Example JSON response snippet:: + + { + "uuid": "adf1835e-4b58-42d0-b1f4-c57791167d19", + "title": "Issue title", + "request_template": "https://localhost/api/v2/request_templates/5b106292-d8b6-459c-abda-e6a87527a0db/", + "status": "open", + "assignee": "username", + "notes": "", + "serialized_data": {"Notes": "This version has a known vulnerability."}, + "created_date": "2025-08-12T17:41:47.424373+04:00", + "last_modified_date": "2025-08-12T17:42:29.031833+04:00", + "comments": [ + { + "uuid": "8ee73eb2-353a-4e84-8536-fe4e25a1abf6", + "username": "username", + "text": "Comment content.", + "created_date": "2025-08-14T09:17:55.397285+04:00" + } + ] + } + +Create a Request +---------------- + +**Endpoint:** ``POST /api/v2/requests/`` + +Required fields: + +- **title** (string): A short, descriptive title of the request. +- **request_template** (string): URI of the template to use. + +Optional fields: + +- **status** (string): ``open``, ``closed``, or ``draft``. Default is ``open``. +- **assignee** (string): Username of the person assigned. +- **priority** (string|null): Priority level. +- **product_context** (string|null): URI of a product context. +- **notes** (string): Notes related to the request. +- **serialized_data** (string): Additional structured data. +- **is_private** (boolean): True if only visible to requester/reviewers. +- **content_object** (string|null): URI of associated content object. +- **cc_emails** (array of strings): List of emails to notify. + +.. note:: + + The structure of **serialized_data** depends on the "Request Template" used + for the request. To help construct valid **serialized_data**, consult the + ``form_data_layout`` field available in the Request Template list at + ``https://localhost/api/v2/request_templates/``. + +Example of minimal JSON payload:: + + { + "title": "New vulnerability found", + "request_template": "Address Vulnerabilities in Product Packages" + } + +Example using cURL:: + + api_url="https://localhost/api/v2/requests/" + headers="Authorization: Token abcdef123456" + data='{ + "title": "New vulnerability found", + "request_template": "Address Vulnerabilities in Product Packages" + }' + + curl -X POST "$api_url" -H "$headers" -d "$data" + +Example using Python:: + + import requests + api_url = "https://localhost/api/v2/requests/" + headers = { + "Authorization": "Token abcdef123456", + "Content-Type": "application/json" + } + data = { + "title": "New vulnerability found", + "request_template": "Address Vulnerabilities in Product Packages", + "assignee": "username" + } + response = requests.post(api_url, headers=headers, json=data) + print(response.json()) + +Update a Request +---------------- + +**Endpoint:** ``PUT /api/v2/requests/{uuid}/`` + +Performs a full update. All fields of the request must be provided. + +Partial Update +-------------- + +**Endpoint:** ``PATCH /api/v2/requests/{uuid}/`` + +Allows updating only specific fields. For example, to close a request:: + + import requests + api_url = "https://localhost/api/v2/requests/{uuid}/" + headers = { + "Authorization": "Token abcdef123456", + "Content-Type": "application/json" + } + data = {"status": "closed"} + response = requests.patch(api_url, headers=headers, json=data) + print(response.json()) + +Add comment +----------- + +``POST /api/v2/requests/{uuid}/add_comment/`` + +This endpoint allows you to attach a new comment to an existing request. +A successful call will store the comment and return a confirmation message. + +**Payload example**: + +.. code-block:: json + + { + "text": "Comment content" + } + +**Notes**: + - The ``uuid`` in the URL must correspond to the target request. + - The ``text`` field is required and should contain the full comment content. + - Comments are attributed to the authenticated user making the request. + - A successful request returns HTTP 201 with a status message. + - Invalid or missing fields will return HTTP 400 along with error details. diff --git a/docs/integrations-webhook.rst b/docs/integrations-webhook.rst new file mode 100644 index 00000000..aa90da11 --- /dev/null +++ b/docs/integrations-webhook.rst @@ -0,0 +1,115 @@ +.. _integrations_webhook: + +Webhook integration +=================== + +Webhooks provide a way for DejaCode to automatically send data to external systems +when certain events occur. This allows you to trigger workflows, update other tools, +or synchronize data in real time, without the need for polling the API. + +When an event is fired in DejaCode, the associated webhook sends an HTTP ``POST`` +request to the configured target URL. The request contains a JSON payload describing +the event and relevant data. + +Use cases +--------- + +Webhooks can be used to: + +- Notify a project management tool when a request is created or updated +- Push updates to a monitoring or reporting dashboard +- Synchronize status changes with an external ticketing system +- Trigger automation in CI/CD pipelines + +Available events +---------------- + +The following events can be configured as webhook triggers: + +- ``request.added`` — A new request is created +- ``request.updated`` — An existing request is modified +- ``request_comment.added`` — A comment is added to a request +- ``vulnerability.data_update`` — Vulnerability data is updated + +.. note:: + + The list of available events may vary based on your DejaCode configuration. + Check the Admin UI for the current list. + +Webhook configuration +--------------------- + +Webhooks are managed from the **Admin UI**. + +1. Go to the **Administration dashboard**. +2. Navigate to **Webhooks**. +3. Click **Add webhook** to create a new one. +4. Fill in the following fields: + + - **Target URL** — The endpoint that will receive the POST requests. + - **Event** — The event name that will trigger the webhook. + - **Is active** — Enable or disable the webhook. + - **Extra payload** — Additional JSON data to include in the request body. + - **Extra headers** — Additional HTTP headers to include in the request. + +5. Save the webhook. + +When the selected event occurs, DejaCode will send a POST request to the target URL +with the event payload. + +Payload structure +----------------- + +The default webhook payload is JSON-formatted and contains at least: + +- ``hook`` — The data related to the webhook, like event name, e.g. ``request.created`` +- ``data`` — Object containing event-specific data + +If **extra payload** is defined, it is merged into the JSON body. +If **extra headers** are defined, they are added to the HTTP request. + +Example payload:: + + { + "hook": { + "uuid": "22c9203f-e90b-4135-a142-583ef4f41e72", + "event": "request.added", + "target": "https://target.com/path/" + }, + "data": { + "api_url": "/api/v2/requests/fbc77986-06ff-4dbb-81c3-95cd36dbed66/", + "absolute_url": "/requests/fbc77986-06ff-4dbb-81c3-95cd36dbed66/", + "uuid": "fbc77986-06ff-4dbb-81c3-95cd36dbed66", + "title": "New vulnerability detected", + "request_template": "/api/v2/request_templates/f28a034f-d6df-4fa7-9283-a93730858616/", + "request_template_name": "Address Vulnerabilities in Product Packages", + "status": "open", + "priority": "Urgent", + "assignee": "username", + "product_context": null, + "notes": "", + "serialized_data": { + "Product Team Contact": "contact email", + "Need By Date": "", + "Notes": "" + }, + "is_private": false, + "requester": "username", + "content_type": "product", + "content_object": null, + "content_object_display_name": null, + "cc_emails": [], + "last_modified_by": null, + "created_date": "2025-08-14T13:48:26.909014+04:00", + "last_modified_date": "2025-08-14T13:48:26.909035+04:00", + "comments": [], + "dataspace": "Dataspace" + } + } + +Security considerations +----------------------- + +- Always validate incoming webhook requests on your server. +- If possible, restrict the target URL to accept requests only from trusted IP ranges. +- Consider adding a signature header in **extra headers** to verify authenticity. diff --git a/workflow/api.py b/workflow/api.py index e9640b1f..030fc1b4 100644 --- a/workflow/api.py +++ b/workflow/api.py @@ -14,6 +14,9 @@ import django_filters from rest_framework import serializers +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.response import Response from rest_framework.viewsets import ReadOnlyModelViewSet from dje.api import CreateRetrieveUpdateListViewSet @@ -165,7 +168,12 @@ class RequestSerializer(DataspacedSerializer): ) request_template_name = serializers.StringRelatedField(source="request_template.name") requester = serializers.StringRelatedField() - assignee = DataspacedSlugRelatedField(slug_field="username") + assignee = DataspacedSlugRelatedField( + slug_field="username", + # Not required in the REST API context to simplify external integrations. + allow_null=True, + required=False, + ) priority = DataspacedSlugRelatedField( slug_field="label", allow_null=True, @@ -402,3 +410,19 @@ def perform_update(self, serializer): event_type=RequestEvent.EDIT, dataspace=self.request.user.dataspace, ) + + @action( + detail=True, + methods=["post"], + serializer_class=RequestCommentSerializer, + ) + def add_comment(self, request, *args, **kwargs): + """Add a comment to this request.""" + request_instance = self.get_object() + + serializer = RequestCommentSerializer(data=request.data) + if serializer.is_valid(): + request_instance.add_comment(self.request.user, **serializer.validated_data) + return Response({"status": "Comment added."}, status=status.HTTP_201_CREATED) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) diff --git a/workflow/integrations/base.py b/workflow/integrations/base.py index 3f6aab1c..b41adb9e 100644 --- a/workflow/integrations/base.py +++ b/workflow/integrations/base.py @@ -94,7 +94,12 @@ def patch(self, url, json=None): """Send a PATCH request.""" return self.request("PATCH", url, json=json) + def sync(self, request): + """Sync the given request by creating or updating an external issue.""" + raise NotImplementedError + def post_comment(self, repo_id, issue_id, comment_body, base_url=None): + """Post a comment on an external issue.""" raise NotImplementedError def get_status(self, request): diff --git a/workflow/models.py b/workflow/models.py index fafd5f0e..07c020f3 100644 --- a/workflow/models.py +++ b/workflow/models.py @@ -602,6 +602,14 @@ def serialize_hook(self, hook): "data": serializer.data, } + def add_comment(self, user, text): + """Create and return a RequestComment for this Request.""" + return self.comments.create( + user=user, + text=text, + dataspace=self.dataspace, + ) + def close(self, user, reason): """ Set the Request status to CLOSED. diff --git a/workflow/tests/test_api.py b/workflow/tests/test_api.py index b9da8cbe..35549732 100644 --- a/workflow/tests/test_api.py +++ b/workflow/tests/test_api.py @@ -361,10 +361,16 @@ def test_api_request_endpoint_create(self): expected = { "title": ["This field is required."], "request_template": ["This field may not be null."], - "assignee": ["This field may not be null."], } self.assertEqual(expected, response.json()) + data = { + "title": "Title", + "request_template": self.request_template1_detail_url, + } + response = self.client.post(self.request_list_url, data) + self.assertEqual(status.HTTP_201_CREATED, response.status_code) + data = { "title": "Title", "request_template": self.request_template1_detail_url, @@ -780,6 +786,24 @@ def test_api_request_endpoint_edit_serialized_data(self): expected = {"serialized_data": ['"Organization" is required.']} self.assertEqual(expected, response.data) + def test_api_request_endpoint_add_comment_action(self): + self.client.login(username="super_user", password="secret") + add_comment_url = reverse("api_v2:request-add-comment", args=[self.request1.uuid]) + + data = {} + response = self.client.post(add_comment_url, data=data) + self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code) + expected = { + "text": ["This field is required."], + } + self.assertEqual(expected, response.json()) + + data = {"text": "Comment content."} + response = self.client.post(add_comment_url, data=data) + self.assertEqual(status.HTTP_201_CREATED, response.status_code) + expected = {"status": "Comment added."} + self.assertEqual(expected, response.data) + def test_api_request_and_request_template_endpoints_tab_permission(self): self.assertEqual((TabPermission,), RequestViewSet.extra_permissions) self.assertEqual((TabPermission,), RequestTemplateViewSet.extra_permissions)