diff --git a/.github/workflows/release-3-master-into-dev.yml b/.github/workflows/release-3-master-into-dev.yml index 9c242ee1c31..66cfc0ca0d7 100644 --- a/.github/workflows/release-3-master-into-dev.yml +++ b/.github/workflows/release-3-master-into-dev.yml @@ -70,8 +70,8 @@ jobs: description: No special instructions. --- There are no special instructions for upgrading to $minorv.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/$patchv) for the contents of the release. - " > docs/content/en/open_source/upgrading/$minorv.md - git add docs/content/en/open_source/upgrading/$minorv.md + " > docs/content/releases/os_upgrading/$minorv.md + git add docs/content/releases/os_upgrading/$minorv.md if: endsWith(inputs.release_number_new, '.0') && endsWith(inputs.release_number_dev, '.0-dev') - name: Update values in HELM chart diff --git a/Dockerfile.django-alpine b/Dockerfile.django-alpine index c16c09d7a17..e17e82ad557 100644 --- a/Dockerfile.django-alpine +++ b/Dockerfile.django-alpine @@ -5,7 +5,7 @@ # Dockerfile.nginx to use the caching mechanism of Docker. # Ref: https://devguide.python.org/#branchstatus -FROM python:3.14.5-alpine3.22@sha256:6b91e66ab2a880ce9ca5a1b91c70f45963ff71ff68268df056336e1a657d5efd AS base +FROM python:3.14.6-alpine3.23@sha256:b165067c5afc37fa5608a3c05609cc3d51aafd808a30fbfd822ee594fef55ad4 AS base FROM base AS build WORKDIR /app RUN \ @@ -15,7 +15,7 @@ RUN \ gcc \ build-base \ bind-tools \ - postgresql16-client \ + postgresql18-client \ xmlsec \ git \ util-linux \ @@ -49,7 +49,7 @@ RUN \ xmlsec \ git \ util-linux \ - postgresql16-client \ + postgresql18-client \ curl-dev \ openssl \ # needed for integration-tests diff --git a/Dockerfile.django-debian b/Dockerfile.django-debian index a62cf79ed94..da86fc51cc1 100644 --- a/Dockerfile.django-debian +++ b/Dockerfile.django-debian @@ -43,6 +43,15 @@ RUN \ apt-get -y upgrade && \ # ugly fix to install postgresql-client without errors mkdir -p /usr/share/man/man1 /usr/share/man/man7 && \ + # ca-certificates + curl are needed to fetch the PostgreSQL APT (PGDG) signing key + apt-get -y install --no-install-recommends ca-certificates curl && \ + # Debian trixie only ships postgresql-client-17, but the bundled PostgreSQL + # server is 18. pg_dump refuses to dump a server newer than itself, which + # breaks `manage.py dbbackup`, so pull the matching v18 client from PGDG. + install -d /usr/share/postgresql-common/pgdg && \ + curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc && \ + echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt trixie-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \ + apt-get -y update && \ apt-get -y install --no-install-recommends \ # libopenjp2-7 libjpeg62 are required by the pillow package libopenjp2-7 \ @@ -52,8 +61,9 @@ RUN \ git \ uuid-runtime \ libpq-dev \ - # only required for the dbshell (used by the initializer job) - postgresql-client \ + # only required for the dbshell (used by the initializer job) and dbbackup; + # must match the server major version (18) for pg_dump to work + postgresql-client-18 \ # libcurl4-openssl-dev is required for installing pycurl python package libcurl4-openssl-dev \ && \ diff --git a/Dockerfile.nginx-alpine b/Dockerfile.nginx-alpine index a5cbf14724c..c6920922cac 100644 --- a/Dockerfile.nginx-alpine +++ b/Dockerfile.nginx-alpine @@ -5,7 +5,7 @@ # Dockerfile.django-alpine to use the caching mechanism of Docker. # Ref: https://devguide.python.org/#branchstatus -FROM python:3.14.5-alpine3.22@sha256:6b91e66ab2a880ce9ca5a1b91c70f45963ff71ff68268df056336e1a657d5efd AS base +FROM python:3.14.6-alpine3.23@sha256:b165067c5afc37fa5608a3c05609cc3d51aafd808a30fbfd822ee594fef55ad4 AS base FROM base AS build WORKDIR /app RUN \ @@ -15,7 +15,7 @@ RUN \ gcc \ build-base \ bind-tools \ - postgresql16-client \ + postgresql18-client \ xmlsec \ git \ util-linux \ diff --git a/components/package.json b/components/package.json index 569c97c0e6a..fd8fb6a0558 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "3.1.200", + "version": "3.1.300", "license": "BSD-3-Clause", "private": true, "dependencies": { diff --git a/docs/assets/images/pro_role_permission_grid.png b/docs/assets/images/pro_role_permission_grid.png new file mode 100644 index 00000000000..84484617733 Binary files /dev/null and b/docs/assets/images/pro_role_permission_grid.png differ diff --git a/docs/assets/images/pro_role_permissions_modal.png b/docs/assets/images/pro_role_permissions_modal.png new file mode 100644 index 00000000000..33854a4ea71 Binary files /dev/null and b/docs/assets/images/pro_role_permissions_modal.png differ diff --git a/docs/assets/images/pro_roles_list.png b/docs/assets/images/pro_roles_list.png new file mode 100644 index 00000000000..5c203d6b539 Binary files /dev/null and b/docs/assets/images/pro_roles_list.png differ diff --git a/docs/assets/images/profindings_ss1.png b/docs/assets/images/profindings_ss1.png new file mode 100644 index 00000000000..4c1ca3ffa27 Binary files /dev/null and b/docs/assets/images/profindings_ss1.png differ diff --git a/docs/assets/images/profindings_ss2.png b/docs/assets/images/profindings_ss2.png new file mode 100644 index 00000000000..13545a73ab3 Binary files /dev/null and b/docs/assets/images/profindings_ss2.png differ diff --git a/docs/assets/images/profindings_ss3.png b/docs/assets/images/profindings_ss3.png new file mode 100644 index 00000000000..9c49a56af5f Binary files /dev/null and b/docs/assets/images/profindings_ss3.png differ diff --git a/docs/assets/images/profindings_ss4.png b/docs/assets/images/profindings_ss4.png new file mode 100644 index 00000000000..9136e2c59ef Binary files /dev/null and b/docs/assets/images/profindings_ss4.png differ diff --git a/docs/assets/js/custom.js b/docs/assets/js/custom.js index a9d57852a8c..da6f8df4a00 100644 --- a/docs/assets/js/custom.js +++ b/docs/assets/js/custom.js @@ -22,24 +22,34 @@ window.ddOffcanvas = Offcanvas; (() => { "use strict"; - // Asset-modelling landing pages per edition. The top nav is otherwise - // static, so a single URL can't be correct for both editions: we keep the - // "Model Your Assets" nav link in sync with the selected version, and — - // when the user is already viewing one of these pages — navigate to the - // matching edition's page so the main content follows the toggle. - const assetNavUrls = { - opensource: "/asset_modelling/engagements_tests/os__assets/", - pro: "/asset_modelling/pro_hierarchy/assets_organizations/", + // Edition-specific landing pages per top-nav tab. The top nav is otherwise + // static, so a single URL can't be correct for both editions: we keep these + // nav links in sync with the selected version, and — when the user is + // already viewing one of these pages — navigate to the matching edition's + // page so the main content follows the toggle. Keyed by nav-link label. + const editionNavUrls = { + "Model Your Assets": { + opensource: "/asset_modelling/engagements_tests/os__assets/", + pro: "/asset_modelling/pro_hierarchy/asset_hierarchy/", + }, + // Sensei is Pro-only: Open Source shows a short "Pro feature" page, + // Pro shows the full guide. + "Sensei": { + opensource: "/sensei/os__sensei/", + pro: "/sensei/about_sensei/", + }, }; - const switchAssetPageForVersion = (version) => { - const target = assetNavUrls[version]; - const other = assetNavUrls[version === "pro" ? "opensource" : "pro"]; - // Only redirect when currently on the *other* edition's asset page, - // so this never fires on unrelated pages or loops on the target page. - if (target && location.pathname === other) { - location.assign(target); - } + const switchPageForVersion = (version) => { + Object.values(editionNavUrls).forEach((urls) => { + const target = urls[version]; + const other = urls[version === "pro" ? "opensource" : "pro"]; + // Only redirect when currently on the *other* edition's page, so + // this never fires on unrelated pages or loops on the target page. + if (target && location.pathname === other) { + location.assign(target); + } + }); }; const setVersion = (version) => { @@ -58,11 +68,12 @@ window.ddOffcanvas = Offcanvas; btn.setAttribute("aria-checked", btn.dataset.versionValue === version ? "true" : "false"); }); - // Edition-aware top nav: route "Model Your Assets" to the page that - // matches the selected version (see assetNavUrls above). + // Edition-aware top nav: route these tabs to the page that matches the + // selected version (see editionNavUrls above). document.querySelectorAll("a.nav-link").forEach(link => { - if (link.textContent.trim() === "Model Your Assets") { - link.setAttribute("href", assetNavUrls[version] || assetNavUrls.opensource); + const urls = editionNavUrls[link.textContent.trim()]; + if (urls) { + link.setAttribute("href", urls[version] || urls.opensource); } }); }; @@ -80,8 +91,8 @@ window.ddOffcanvas = Offcanvas; if (btn) { setVersion(btn.dataset.versionValue); // Only on an explicit user toggle (not on load) follow the page to - // the matching edition when viewing an asset-modelling page. - switchAssetPageForVersion(btn.dataset.versionValue); + // the matching edition when viewing one of the edition-specific pages. + switchPageForVersion(btn.dataset.versionValue); } }); diff --git a/docs/config/_default/menus/menus.en.toml b/docs/config/_default/menus/menus.en.toml index 67ad0c35f91..61b8d26daf1 100644 --- a/docs/config/_default/menus/menus.en.toml +++ b/docs/config/_default/menus/menus.en.toml @@ -23,6 +23,42 @@ url = "/metrics_reports/dashboards/introduction_dashboard/" weight = 14 +[[main]] + name = "Sensei" + # Sensei is Pro-only. Default to the Open Source "Pro feature" page; custom.js + # swaps this tab to the full guide (/sensei/about_sensei/) when Pro is selected. + url = "/sensei/os__sensei/" + weight = 14 + +# Left-hand section sidebar for the Sensei chapter (the chapter is flat, so the +# Doks subsection-based sidebar has nothing to render without this menu). +# Entries are audience-filtered by the version toggle: the opensource page shows +# in the OS view, the pro pages show in the Pro view. +[[sidebar_sensei]] + name = "Sensei" + pageRef = "/sensei/OS__sensei" + weight = 0 + +[[sidebar_sensei]] + name = "About Sensei" + pageRef = "/sensei/about_sensei" + weight = 1 + +[[sidebar_sensei]] + name = "Set Up Sensei" + pageRef = "/sensei/setup_sensei" + weight = 2 + +[[sidebar_sensei]] + name = "Fixing Findings with Sensei" + pageRef = "/sensei/fixing_findings" + weight = 3 + +[[sidebar_sensei]] + name = "Sensei Reference" + pageRef = "/sensei/sensei_reference" + weight = 4 + [[main]] name = "Admin" url = "/admin/admin_intro/intro/" diff --git a/docs/config/_default/params.toml b/docs/config/_default/params.toml index ecf7514c818..875d324cc79 100644 --- a/docs/config/_default/params.toml +++ b/docs/config/_default/params.toml @@ -56,7 +56,8 @@ mainSections = ["docs"] "admin", "automation", "asset_modelling", - "issue_tracking"] # ["docs"] (default) or list of sections (e.g. ["docs", "guides"]) + "issue_tracking", + "sensei"] # ["docs"] (default) or list of sections (e.g. ["docs", "guides"]) toTopButton = true # scroll-to-top button breadcrumbTrail = true # breadcrumb navigation on doc pages diff --git a/docs/content/admin/feature_flags/PRO__feature_flags.md b/docs/content/admin/feature_flags/PRO__feature_flags.md index 17c8f70af3f..958b427c4cc 100644 --- a/docs/content/admin/feature_flags/PRO__feature_flags.md +++ b/docs/content/admin/feature_flags/PRO__feature_flags.md @@ -26,11 +26,22 @@ Use the search box to filter the list by feature name or description. 1. Find the feature in the list. 2. Click its toggle. -3. The change takes effect immediately. You do not need to restart anything, and other users pick the change up on their next page load. +3. The change takes effect immediately. Other users pick the change up on their next page load. Some features show a confirmation dialog before the change is applied. This happens when enabling a feature that carries a warning (for example one that requires a restart or may affect existing data), or one that cannot be turned back off. -Turning a feature off is normally just the reverse of turning it on. The exceptions are called out in the next section. +Turning a feature off is normally just the reverse of turning it on. The exceptions are called out in [When a toggle is locked](#when-a-toggle-is-locked). + +### Organization / Asset Relabeling + +**Organization / Asset Relabeling** renames "Product Type" to "Organization" and "Product" to "Asset". It is on by default and toggles from this page like any other feature, but it is worth knowing which parts of DefectDojo it governs: + +* The **Pro UI** follows this toggle. The new labels appear on your next page load. +* The **Classic UI** pages, their URLs, and generated reports take their naming from the `DD_ENABLE_V3_ORGANIZATION_ASSET_RELABEL` deployment setting (also on by default), which is read when DefectDojo starts. This toggle does not change them, and restarting does not make it change them. + +The stored toggle was seeded from that deployment setting, so the two agree until you change one of them. If you turn relabeling off here and you also use the Classic UI, set `DD_ENABLE_V3_ORGANIZATION_ASSET_RELABEL=False` on your deployment and restart so both surfaces match. On [DefectDojo Pro (Cloud)](/get_started/pro/cloud/), contact [DefectDojo Support](mailto:support@defectdojo.com) to have the deployment setting changed. + +The feature carries a **Restart Recommended** tag on the Feature Flags page for this reason: the naming used outside the Pro UI is fixed when the process starts. Relabeling is cosmetic either way. Database models, field names, and API endpoints are unchanged, so existing automation keeps working. See [Asset Hierarchy](/asset_modelling/pro_hierarchy/asset_hierarchy/). ## When a toggle is locked @@ -58,7 +69,7 @@ Cloud instances also have access to features that are not offered on-premise. Se On [DefectDojo Pro (On-Premise)](/get_started/pro/onprem/), most features work exactly as they do on Cloud: open **Settings > Feature Flags** and toggle them. -A small number of features are read from your deployment configuration instead — they change how the application starts, so they cannot be flipped at runtime. These appear on the page as read-only, labeled **Managed by deployment**, and name the environment variable that controls them, for example `DD_ENABLE_V3_ORGANIZATION_ASSET_RELABEL`. +A small number of features are read from your deployment configuration instead. They change how the application starts, so they cannot be flipped at runtime. These appear on the page as read-only, labeled **Managed by deployment**, and name the environment variable that controls them, for example `DD_V3_FEATURE_LOCATIONS` for [Locations](/asset_modelling/locations/pro__locations_overview/). Because these features require a restart, and some of them cannot be reversed once enabled, check the feature's own documentation before changing one. Several are best enabled with help from [DefectDojo Support](mailto:support@defectdojo.com). @@ -81,7 +92,6 @@ Most features are available on both installation types. The exceptions are: | Downstream Connections | [DefectDojo Pro (Cloud)](/get_started/pro/cloud/) only | Feature Flags page. Shown as **Unavailable on This Deployment** on-premise, which does not run the required infrastructure. See [Pro Integrations](/issue_tracking/pro_integration/integrations/). | | Request a New Connector | [DefectDojo Pro (Cloud)](/get_started/pro/cloud/) only | Feature Flags page. Shown as **Unavailable on This Deployment** on-premise. | | Locations | Both | Deployment configuration. Locations is in Beta and cannot be turned back off once enabled, so contact [DefectDojo Support](mailto:support@defectdojo.com) to have it enabled. See [Locations Overview](/asset_modelling/locations/pro__locations_overview/). | -| Organization / Asset Relabeling | Both | Deployment configuration: `DD_ENABLE_V3_ORGANIZATION_ASSET_RELABEL`. | Every other optional feature is toggled directly on the Feature Flags page on both Cloud and On-Premise instances. diff --git a/docs/content/admin/user_management/PRO__custom_rbac_roles.md b/docs/content/admin/user_management/PRO__custom_rbac_roles.md new file mode 100644 index 00000000000..b68b0022cad --- /dev/null +++ b/docs/content/admin/user_management/PRO__custom_rbac_roles.md @@ -0,0 +1,211 @@ +--- +title: "Custom RBAC Roles" +description: "Build your own roles by choosing individual permissions, using the five built-in roles as clonable starting points" +weight: 5 +audience: pro +--- + +> **DefectDojo Pro feature.** The Members / Groups / Global Roles RBAC system described on this page is part of DefectDojo Pro. Open-source DefectDojo uses the [Authorized Users](../os__authorized_users/) model. See that page for open-source access control, and the [3.0 upgrade notes](/releases/os_upgrading/3.0/#authorized-users-panel-replaces-membersgroups-under-legacy-authorization) if you're moving between editions. + +DefectDojo Pro ships five roles: **Reader**, **Writer**, **Maintainer**, **Owner**, and **API Importer**. If none of them is the right fit, you can now build your own role by choosing exactly which permissions it grants. + +A custom role works anywhere a built-in role works: as a Global Role, as a Group's role, as the default group role, and as a member role on an individual Organization or Asset. + +The five built-in roles become **locked, clonable presets**. Their permissions are unchanged (see the [Action permission charts](../user_permission_chart/) for what each one grants), they cannot be edited or deleted, and cloning one is the recommended way to start a new role. + +## Before you start + +Custom role management is off by default. A **superuser** turns it on from **Settings > Feature Flags**, by enabling **Custom Roles**. See [Feature Flags](/admin/feature_flags/pro__feature_flags/) for how that page works. + +While the feature is off, the Roles page is still readable: you can view the built-in roles and their permissions, but you cannot create, edit, clone, or delete anything. + +Managing roles requires **superuser** status or the built-in **Owner** Global Role. This is deliberate and cannot be delegated to a custom role: see [What a custom Global Role unlocks](#what-a-custom-global-role-unlocks). + +## Opening the Roles page + +Go to **👤 Users > Roles** in the left sidebar. The menu entry is visible to superusers and holders of the built-in Owner Global Role. + +![The Roles page listing built-in and custom roles](images/pro_roles_list.png) + +The table lists every role in your instance: + +| Column | What it shows | +| --- | --- | +| **ID** | The role's numeric id. Useful when filtering the Users table or calling the API. | +| **Name** | The role name. | +| **Description** | Your own note about what the role is for. Optional, and empty unless someone fills it in. The built-in roles ship without one. | +| **Permissions** | A count of granted permissions. Click it to open a read-only view of the full grid. | +| **Users** | How many users hold this role as their Global Role. Click through to see them in the Users table. | +| **Type** | **Built-in** for the five presets, **Custom** for roles you created. | + +Every column is sortable and filterable, and the keyword search matches on name and description. + +## Creating a role + +### Clone a built-in role (recommended) + +Cloning starts you from a known-good permission set instead of an empty grid, which makes it much harder to accidentally leave out a permission a role needs. + +1. Find the role closest to what you want. +2. Open its **⋮** menu and choose **Clone Role**. +3. A copy is created immediately, named ` (copy)`, with the same permissions and description as the role it came from. +4. Open the copy's **⋮** menu, choose **Edit Role**, then rename it and adjust its permissions. + +Built-in roles can be cloned even though they cannot be edited. The clone records which role it came from. + +### Start from scratch + +1. Click **New Role**. +2. Give it a **Name** (required) and optionally a **Description**. +3. Choose its permissions in the grid below (see the next section). +4. Click **Save Role**. + +Role names must be unique, and the check ignores case: if `Triage Lead` exists, `triage lead` is rejected. + +## Choosing permissions + +![The permission grid in the role form](images/pro_role_permission_grid.png) + +Permissions are grouped into three tables plus a checklist. + +**Object Permissions** apply to the Organizations and Assets the role is assigned to, and to everything nested under them. + +| Row | View | Add | Edit | Delete | +| --- | --- | --- | --- | --- | +| Organization | ☑️ | ☑️ | ☑️ | ☑️ | +| Asset | ☑️ | ☑️ ¹ | ☑️ | ☑️ | +| Engagement | ☑️ | ☑️ | ☑️ | ☑️ | +| Test | ☑️ | ☑️ | ☑️ | ☑️ | +| Finding | ☑️ | ☑️ | ☑️ | ☑️ | +| Finding Group | ☑️ | ☑️ | ☑️ | ☑️ | +| Risk Acceptance | ☑️ | ☑️ | ☑️ | ☑️ | +| Location | ☑️ | ☑️ | ☑️ | ☑️ | +| Component | ☑️ | | | | +| Note | ² | ☑️ | ☑️ | ☑️ | +| Benchmark | ² | | ☑️ | ☑️ | +| Language | ☑️ | ☑️ | ☑️ | ☑️ | +| Technology | ☑️ | ☑️ | ☑️ | ☑️ | +| Asset API Scan Configuration | ☑️ | ☑️ | ☑️ | ☑️ | +| Asset Tracking Files | ☑️ | ☑️ | ☑️ | ☑️ | +| Group | ☑️ | | ☑️ | ☑️ | + +1. **Asset > Add** means creating a new Asset inside an Organization the role is assigned to. +2. View for Notes and Benchmarks is inherited: a role that can view the parent Engagement, Test, Finding, or Asset can view its Notes and Benchmarks. These cells show a **?** icon instead of a checkbox. + +**Group & Member Permissions** control who can manage membership. The columns here are View, Manage, Add, Add Owner, Edit, and Delete. + +| Row | Available actions | +| --- | --- | +| Organization Group, Asset Group | View, Add, Add Owner, Edit, Delete | +| Organization Member, Asset Member, Group Member | Manage, Add Owner, Delete | + +**Global Feature Permissions** gate instance-wide Pro features rather than individual Organizations or Assets, so **they only take effect when the role is held as a Global Role**. Granting them on a role that is only used as an Asset membership has no effect. + +| Row | Available actions | +| --- | --- | +| Report Template | View, Add, Edit, Delete | +| Generated Report | View, Add, Delete | +| Connector, Sensei, Asset Hierarchy, Version Manager, Tuner, Universal Parser, Rule, Integration | View, Edit | +| Mitigation Policy | Edit | +| Audit Log, Metering | View | + +**Additional Permissions** is a checklist of capabilities that don't fit a View/Add/Edit/Delete shape: + +* **Configure Asset Notifications**: choose which notifications a single Asset sends, and where. +* **Import Scan Result**: import and re-import scan results, creating and updating findings. +* **Share Dashboard Layout**: publish a dashboard layout to other users. Global Role only. +* **Share Table Preference**: publish a saved table view (columns, filters, sort order). Global Role only. +* **View Note History**: see who changed a note and when. + +### How to read the grid + +![The read-only view of a role's permissions](images/pro_role_permissions_modal.png) + +| What you see | What it means | +| --- | --- | +| An empty checkbox | The permission exists and is not granted. Click to grant it. | +| A checked checkbox | Granted. | +| A shaded, empty cell | The permission does not exist for that row and action. Not selectable. | +| A **?** icon | View is inherited from a parent object, so there is nothing to grant here. | +| A green ✔ (read-only view) | Granted. | +| A red ✘ (read-only view) | Not granted. | + +In each row, the leftmost permission (**View**, or **Manage** on member rows) gates the rest of the row. You have to grant it before the other cells in that row become available, because a role cannot meaningfully edit or delete what it cannot see. Clearing the gate clears the rest of the row with it. + +## Editing, cloning, and deleting + +Each row's **⋮** menu offers **Edit Role**, **Clone Role**, **Delete Role**, and **Role History**. + +Built-in roles only offer **Clone Role**. They cannot be edited or deleted, by anyone, including superusers. This keeps a known baseline in place and keeps upgrades predictable. + +Deleting a role that is still assigned to anyone will fail. Reassign or remove those assignments first, then delete the role. Assignments that count for this purpose are Organization and Asset memberships (both user and group), Global Roles, Group memberships, and the default group role in System Settings. + +The API can do the reassignment for you in a single call. See [Managing roles through the API](#managing-roles-through-the-api). + +## Assigning a custom role + +Custom roles appear in every role dropdown, alongside the built-ins: + +| Where | How | +| --- | --- | +| **Global Role on a user** | The **Global Role** field on the user's form. Superusers only. See [Set a User's permissions](../set_user_permissions/). | +| **Global Role on a group** | The **Global Role** field on the group's form. See [Share permissions: User Groups](../create_user_group/). | +| **Organization or Asset membership** | The Permissions dialog on the Organization or Asset, for both users and groups. See [Set Permissions in Pro](../pro_permissions_overhaul/). | +| **Default group role** | **Default group role** in System Settings, applied to newly created users. See [Manage default permissions](../about_perms_and_roles/#manage-default-permissions). | +| **Role within a group** | The role dropdown on a group's member list. This dropdown only offers roles that grant at least one Group permission, so a role with no Group permissions will not appear there. | + +Two constraints are worth knowing: + +* **Owner-tier is reserved.** A custom role can never be an owner-tier role. Only the built-in Owner is, so only it carries the implicit power to manage other Owners. +* **Granting the Owner role to someone else still requires the matching Add Owner permission**, whether you do it on an Organization, an Asset, or a Group. + +## What a custom Global Role unlocks + +Parts of the UI are gated on a minimum Global Role rather than on an individual permission. To make custom roles work with those gates, DefectDojo ranks a custom Global Role against the built-in tiers: a custom role earns the highest tier whose permissions it **completely** covers. + +* A custom role that covers everything Maintainer grants is treated as Maintainer for those gates. +* Cover everything Writer grants, and it is treated as Writer. Same for Reader. +* Cover none of them completely, and it earns no tier. Its individual permissions still work exactly as granted; only the tier-based UI gates stay closed. +* **Owner can never be earned this way.** Role management, and everything else gated on the Owner Global Role, stays with superusers and the built-in Owner. + +Coverage has to be complete, which occasionally surprises people. A role cloned from Maintainer earns the Maintainer tier. Rebuild Maintainer's permissions by hand, miss one, and the role lands on the Writer tier instead. If a custom Global Role is missing UI you expected, compare it against the built-in tier in the [Action permission charts](../user_permission_chart/). + +## Role history + +Custom roles keep an audit trail. Open **Role History** from a role's **⋮** menu to see which permissions were granted or revoked, by whom, and when, alongside changes to who holds the role. + +Two things this history does not show: changes to a role's own name and description, and the permissions of built-in roles (those are seeded, never edited, and so never generate history). + +Role history is a read, so it is available whether or not the Custom Roles feature is on. + +## Managing roles through the API + +Roles are available at `/api/v2/roles/`. Reads are open to any authenticated user, because clients need the role list to populate dropdowns. Writes require superuser status or the built-in Owner Global Role, plus the Custom Roles feature flag. + +| Operation | Request | +| --- | --- | +| List roles | `GET /api/v2/roles/` | +| Retrieve one role | `GET /api/v2/roles/{id}/` | +| List every grantable permission | `GET /api/v2/roles/permissions_catalog/` | +| Create a role | `POST /api/v2/roles/` with `name`, optional `description`, and a `permissions` list | +| Replace a role's permissions | `PATCH /api/v2/roles/{id}/` with a `permissions` list | +| Clone a role | `POST /api/v2/roles/{id}/clone/` with an optional `name` and `description` | +| Delete a role | `DELETE /api/v2/roles/{id}/` | +| Delete a role and move its assignments | `DELETE /api/v2/roles/{id}/?reassign_to={other_role_id}` | +| Read a role's history | `GET /api/v2/roles/{id}/history/` | + +Notes: + +* `permissions` **replaces** the role's grant list rather than adding to it. Send the full set you want the role to end up with. +* `?reassign_to=` moves every assignment of the deleted role to the role you name, in one transaction. This is the only way to reassign in bulk: the UI does not offer it. +* Attempting to edit or delete a built-in role returns `403`. Editing an unknown permission value, reusing an existing role name, or deleting an in-use role without `reassign_to` returns `400` with an explanation. +* `is_owner` cannot be set through the API. Posting it is accepted and ignored. + +## Things to know + +* **Multiple roles on the same object grant the union of their permissions.** If a user holds a role directly on an Asset and inherits another through a group, they get everything either role grants. Roles only ever add permissions, never remove them. +* **Permission changes are picked up on the next page load**, not instantly in the current view. Background jobs may take up to 30 seconds, and cached permission data up to 5 minutes, to reflect an edit. +* **Role dropdowns list up to 250 roles.** Beyond that, some roles will not appear in dropdowns, though they continue to work. +* **Maintainer and Owner can add Organizations, but the grid does not show it.** For those two roles that grant is stored as a global-scope grant, and the grid reads only object-scope grants, so their **Organization > Add** cell reads as not granted. Cloning either role preserves the grant. +* **Terminology follows your instance.** These docs use Organization and Asset, the default labels. If your instance has turned Organization / Asset relabeling off, the same rows read Product Type and Product instead. +* **The Roles page is read-only for everyone else.** A user who reaches `/settings/roles` directly can see the roles and their permissions but cannot change anything. Permission data is not sensitive, and the server enforces the real boundary on every write. diff --git a/docs/content/admin/user_management/_index.md b/docs/content/admin/user_management/_index.md index ae5322fdda9..00987f712d7 100644 --- a/docs/content/admin/user_management/_index.md +++ b/docs/content/admin/user_management/_index.md @@ -33,7 +33,8 @@ DefectDojo Pro uses a role-based system with Members, Groups, and Global Roles. * [Set a User's Permissions](./set_user_permissions/) — assigning Roles, Global Roles, and Configuration Permissions * [Share permissions: User Groups](./create_user_group/) — assigning permissions to many users at once * [Set Permissions in Pro](./pro_permissions_overhaul/) — Pro-specific UI for managing Members and Permissions -* [Action permission charts](./user_permission_chart/) — full reference of every permission for every Role +* [Action permission charts](./user_permission_chart/) — full reference of every permission for every built-in Role +* [Custom RBAC Roles](./pro__custom_rbac_roles/) — build your own roles by choosing individual permissions * [Single Sign-On](/admin/sso/) — SAML and OAuth setup for Pro ## Migrating between editions diff --git a/docs/content/admin/user_management/about_perms_and_roles.md b/docs/content/admin/user_management/about_perms_and_roles.md index 3ca3e2d21e5..63eaf31a4d7 100644 --- a/docs/content/admin/user_management/about_perms_and_roles.md +++ b/docs/content/admin/user_management/about_perms_and_roles.md @@ -33,7 +33,9 @@ When users are assigned as members to a Product or Product Type, they also recei ### Role Summaries -Users can be assigned a role of Reader, Writer, Maintainer, Owner or API Importer, either globally or within a Product / Product Type. +DefectDojo Pro ships five **built-in roles**: Reader, Writer, Maintainer, Owner and API Importer. Any of them can be assigned either globally or within a Product / Product Type. + +The built-in roles are locked presets. They cannot be edited or deleted, and their permissions are the same on every DefectDojo Pro instance. If none of them fits how your team works, you can build a role that does, by choosing individual permissions or by cloning a built-in role and adjusting it. See [Custom RBAC Roles](../pro__custom_rbac_roles/). ‘Underlying data’ refers to all Products, Engagements, Tests, Findings or Endpoints nested under a Product, or Product Type. @@ -47,7 +49,7 @@ Users can be assigned a role of Reader, Writer, Maintainer, Owner or API Importe ​ * **API Importer** **Users** have limited abilities. This Role allows limited API access without exposing the majority of the API endpoints, so is useful for automation or users who are meant to be ‘external’ to DefectDojo. They can view underlying data, Add / Edit Engagements, and Import Scan Data. -For detailed information on Roles, please see our **[Role Permission Chart](../user_permission_chart/)**. +For detailed information on the built-in Roles, please see our **[Role Permission Chart](../user_permission_chart/)**. For the full list of permissions a role can be given, and how to build your own, see **[Custom RBAC Roles](../pro__custom_rbac_roles/)**. ### Global Roles @@ -61,6 +63,8 @@ User Groups can be added as Members of a Product or Product Type. Users who are * If a User is assigned as a member of a Product, they are not granted any associated Product Type permissions by default. +* If a User ends up with more than one role on the same Product or Product Type (for example one assigned directly and another inherited from a Group), they receive the **combined** permissions of every role they hold there. + * A User's Product Role always supersedes their 'default' Product Type Role. ​ * A User's Product / Product Type Role always supersedes their Global Role within the underlying Product or Product Type. For example, if a User has a Product Type Role of Reader, but is also assigned as an Owner on a Product nested under that Product Type, they will have additional Owner permissions added for that Product only. diff --git a/docs/content/admin/user_management/user_permission_chart.md b/docs/content/admin/user_management/user_permission_chart.md index 4556d189dd8..3a52ead0d83 100644 --- a/docs/content/admin/user_management/user_permission_chart.md +++ b/docs/content/admin/user_management/user_permission_chart.md @@ -13,6 +13,8 @@ aliases: This chart is intended to list all permissions related to a Product or Product Type, as well as which permissions are available to each role. +The five roles below are DefectDojo Pro's **built-in roles**. They are locked presets: their permissions are the same on every instance and cannot be changed. If you have built your own roles, this chart describes the built-ins they were cloned from rather than the roles themselves. For the full catalog of permissions a role can be given, see [Custom RBAC Roles](../pro__custom_rbac_roles/#choosing-permissions). + | **Section** | **Permission** | Reader | Writer | Maintainer | Owner | API Importer | | --- | --- | --- | --- | --- | --- | --- | | **Product / Product Type Access** | View assigned Product or Product Type ¹ | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | diff --git a/docs/content/asset_modelling/PRO_hierarchy/assets_organizations.md b/docs/content/asset_modelling/PRO_hierarchy/asset_hierarchy.md similarity index 87% rename from docs/content/asset_modelling/PRO_hierarchy/assets_organizations.md rename to docs/content/asset_modelling/PRO_hierarchy/asset_hierarchy.md index 2b4252539d0..2f43d376233 100644 --- a/docs/content/asset_modelling/PRO_hierarchy/assets_organizations.md +++ b/docs/content/asset_modelling/PRO_hierarchy/asset_hierarchy.md @@ -1,10 +1,11 @@ --- -title: "Assets and Organization structure" +title: "Asset Hierarchy" description: "DefectDojo Pro - Product Hierarchy Overhaul" audience: pro weight: 1 aliases: - /en/working_with_findings/organizing_engagements_tests/pro_assets_organizations + - /asset_modelling/pro_hierarchy/assets_organizations --- DefectDojo Pro is extending the Product/Product Type object classes to provide greater flexibility with the data model. @@ -26,16 +27,18 @@ A superuser can turn it on from **Settings > Feature Flags**, on both Cloud and **Label Changes** renames "Product Type" to "Organization" and "Product" to "Asset" throughout the UI. This is a separate step from enabling the hierarchy and can be done at the same time or later. -Label changes are read from your deployment configuration when DefectDojo starts, so they are not toggled from the Feature Flags page: +Label changes are on by default as of 3.0. There are two controls, covering different parts of the application: -* **[DefectDojo Pro (Cloud)](/get_started/pro/cloud/)** — email [support@defectdojo.com](mailto:support@defectdojo.com) with your instance URL. -* **[DefectDojo Pro (On-Premise)](/get_started/pro/onprem/)** — set `DD_ENABLE_V3_ORGANIZATION_ASSET_RELABEL` and restart DefectDojo. Contact Support if you would like guidance before making the change. +* **Pro UI** (the default UI): a superuser toggles "Organization / Asset Relabeling" at **Settings > Feature Flags**, on both Cloud and On-Premise instances. The new labels appear on the next page load. See [Feature Flags](/admin/feature_flags/pro__feature_flags/). +* **Classic UI pages and generated reports**: their labels and URLs come from the `DD_ENABLE_V3_ORGANIZATION_ASSET_RELABEL` deployment setting, which is read when DefectDojo starts. On-premise, set it and restart DefectDojo. On [DefectDojo Pro (Cloud)](/get_started/pro/cloud/), email [support@defectdojo.com](mailto:support@defectdojo.com) with your instance URL. + +Both default to on, and the Feature Flags value was seeded from the deployment setting, so the two agree unless you change one of them. Keep them in sync if you use the Classic UI as well as the Pro UI. Note that label changes are cosmetic only: API endpoints and field names remain unchanged, so existing automation will continue to work. ## Significant Changes -* **Product Types** have been renamed to "Organizations", and **Products** have been renamed to "Assets". Currently, this name change is opt-in for existing DefectDojo Pro subscriptions. +* **Product Types** have been renamed to "Organizations", and **Products** have been renamed to "Assets". As of 3.0 this name change is on by default. See [Label Changes](#label-changes-optional) for the controls that turn it off. * **Assets** can now have parent/child relationships with one another to further sub-categorize Organizational components. ### Organizations diff --git a/docs/content/asset_modelling/engagements_tests/OS__findings.md b/docs/content/asset_modelling/engagements_tests/OS__findings.md index bad4e73365e..72ea7e1df7f 100644 --- a/docs/content/asset_modelling/engagements_tests/OS__findings.md +++ b/docs/content/asset_modelling/engagements_tests/OS__findings.md @@ -43,7 +43,7 @@ Finding views contain a variety of tables to help interpret a Finding’s status - **Severity**: The severity rating of that Finding, which is applied automatically. - As mentioned above, DefectDojo automatically translates a security tool’s severity metric into a Severity score for each Finding, which confers an SLA onto the Finding according to your Asset’s SLA configuration. - **SLA**: The intended due date by which the Finding is intended to be resolved. - - **Status**: The status of the Finding (e.g., Active, Verified, False Positive, Duplicate, Out of SCope, and Under Defect Review). + - **Status**: The status of the Finding (e.g., Active, Verified, False Positive, Duplicate, Out of Scope, and Under Defect Review). - **Finding type**: Whether the Finding is Static (SAST) or Dynamic (DAST). - **Date discovered**: The date on which the Finding was discovered. - **CWE**: The CWE classification of the Finding. @@ -160,7 +160,7 @@ The ⋮ kebab menu next to Findings contains the following functions: - **Delete**: Deletes the selected Finding. #### Attaching Files to Findings -You can attach image files to any Finding to provide visual context — for example, a screenshot of a vulnerability in action or a proof-of-concept image. +You can attach files to any Finding to provide visual context — for example, a screenshot of a vulnerability in action or a proof-of-concept image. Supported file types include: @@ -233,7 +233,7 @@ To manually perform Finding Group actions: 1. Navigate to a list of Findings within a Test. 2. Select the Finding(s) you wish to add to a Finding Group by clicking the corresponding checkbox. 3. Click the **Group** checkbox. -4. Click the corresponding action you wish to complete +4. Click the corresponding action you wish to complete. - **Create**: Creates a Finding Group that includes the selected Findings. - **Add to**: Adds the selected Findings to a pre-existing Finding Group. - **Remove from any group**: Removes the selected Findings from any Finding Groups they were previously a part of. @@ -254,6 +254,8 @@ The adjoining **Create Finding Groups for all Findings** checkbox performs two f ![image](images/osfindings_ss5.png) +If an option is not selected from the Group By dropdown menu during import, no grouping will occur. + If the grouping criteria (e.g., component name, vulnerability ID, etc.) isn’t populated in the Finding, it will not have a group created or be added to a pre-existing Finding Group. If a scan is imported that reveals 10 Findings that are not grouped, and the same scan is reimported and the Findings are grouped, the first 10 Findings will not be added to that Finding Group (i.e., the Finding Group will only include the 10 Findings from the reimport, not the 10 Findings from the initial and subsequent import). @@ -294,6 +296,6 @@ More information about DefectDojo’s Report Builder can be found [here](/metric #### Export Findings -Pages that show a list of Findings or a list of engagements have a CSV and Excel export option in the top-right dropdown menu. +Pages that show a list of Findings or a list of Engagements have a CSV and Excel export option in the top-right dropdown menu. -From any Findings list page, open the dropdown menu in the top-right corner to export the visible Findings as a CSV or Excel file. The list of engagements can also be exported as CSV or Excel using the same dropdown menu on the engagements list page. \ No newline at end of file +From any Findings list page, open the dropdown menu in the top-right corner to export the visible Findings as a CSV or Excel file. The list of Engagements can also be exported as CSV or Excel using the same dropdown menu on the Engagements list page. \ No newline at end of file diff --git a/docs/content/asset_modelling/engagements_tests/PRO__assets.md b/docs/content/asset_modelling/engagements_tests/PRO__assets.md index e8d171b69f4..84c7f1244e9 100644 --- a/docs/content/asset_modelling/engagements_tests/PRO__assets.md +++ b/docs/content/asset_modelling/engagements_tests/PRO__assets.md @@ -182,4 +182,4 @@ You can visualize the structure of Assets in DefectDojo and change relationships After selecting the Assets to be visualized from the corresponding table, click **View Asset Hierarchy** to generate a flow chart of the relationship between the chosen Assets, if any. -Further information on the effect of nesting Assets on deduplication, RBAC, and other details, as well as example use cases, can be found [here](/asset_modelling/pro_hierarchy/assets_organizations/#asset-nesting-examples). +Further information on the effect of nesting Assets on deduplication, RBAC, and other details, as well as example use cases, can be found [here](/asset_modelling/pro_hierarchy/asset_hierarchy/#asset-nesting-examples). diff --git a/docs/content/asset_modelling/engagements_tests/PRO__findings.md b/docs/content/asset_modelling/engagements_tests/PRO__findings.md new file mode 100644 index 00000000000..8e428e2a556 --- /dev/null +++ b/docs/content/asset_modelling/engagements_tests/PRO__findings.md @@ -0,0 +1,271 @@ +--- +title: "Findings" +description: "Understanding Findings in DefectDojo Pro" +audience: pro +weight: 5 +--- +Organizations → Assets → Engagements → Tests → **FINDINGS** + +## Overview +**Findings** represent the lowest level of the Product Hierarchy where individual vulnerabilities are tracked and managed and serve as the main way that DefectDojo standardizes and guides the reporting and remediation process of your security tools. Regardless of whether a vulnerability was reported in SonarQube, Acunetix, or your team’s custom tool, Findings give you the ability to manage each vulnerability in the same way. + +Examples of Findings include: +- **Cookie Not Marked as HttpOnly** +- **Out-of-Date Version (PHP)** +- **Out-of-Band Code Evaluation (PHP)** +- **Out-of-Date Version (MySQL)** +- **Backup Source Code Detected** +- **Blind Cross-Site Scripting** + +In addition to storing the vulnerability data and providing a remediation framework, DefectDojo also enhances your Findings in the following ways: +- Automatically adding related EPSS scores to a Finding to describe exploitability +- Automatically translating a security tool’s severity metric into a Severity score for each Finding, which confers an SLA onto the Finding according to your Asset’s SLA configuration. For more information on SLA configuration, click [here](/asset_modelling/pro_hierarchy/priority_sla/#working-with-slas). + +Overall, Findings are designed to work with the Product Hierarchy to standardize your efforts, and apply a consistent method to each Asset. + +## Accessing Findings +Findings are accessible via the sidebar. The submenu provides access to Active and Mitigated Findings, All Findings (regardless of Open or Closed status), Finding Groups, Finding Templates, and the New Finding workflow. Individual Findings are also accessible from within the Test that contains them. + +[Risk Accepted Findings] (/triage_findings/findings_workflows/os__risk_acceptance/) are accessible from the **Risk Acceptances** section of the sidebar. + +![image](images/profindings_ss1.png) + +### Permissions +Every Finding belongs to a Test, allowing DefectDojo to preserve which scan or assessment originally identified the vulnerability. + +As Findings belong to Tests, access to Findings is determined by a User’s access to the Asset that contains the Test. Tests do not have independent access control lists. + +## Findings View +Finding views contain a variety of tables to help interpret a Finding’s status at a glance. + +### Finding Overview +- **Description**: The description of the Finding (added automatically depending on the type of Finding, or created manually). +- **Mitigation**: Suggested steps to mitigate. +- **General Mitigation Policy**: The standardized mitigation policy for the selected Finding. +Mitigation policies can be found and edited in the sidebar under **Configuration** → **Mitigation Policies**. +- **Impact**: Potential impact of leaving the Finding unresolved. +- **References**: URL to cross-reference the third-party scan tool’s specific description of the Finding. For example, References could be links to a relevant entry in a Finding catalog, or a single advisory URL. +- **Files**: Any files that have been added to contextualize the Finding. +- **Notes**: Notes left by Users related to the Finding. Marking a note as Private will mean it will not be included in any generated reports that include the selected Finding. + +### Metadata +- **ID**: DefectDojo’s unique Finding ID. +- **Organization, Asset, Engagement, and Test**: The parent objects of the selected Finding. +- **Status**: The status of the Finding (e.g., Active, Verified, False Positive, Duplicate, Out of Scope, and Under Defect Review). +- **Severity**: The severity rating of that Finding, which is applied automatically. + - As mentioned above, DefectDojo automatically translates a security tool’s severity metric into a Severity score for each Finding, which confers an SLA onto the Finding according to your Asset’s SLA configuration. +- **Risk**: A 4-level ranking system that factors in a Finding’s exploitability and is applied automatically. + - Details about how priority, risk, and SLAs are calculated can be found [here](/asset_modelling/pro_hierarchy/priority_sla/#main-content). Further details about Finding status and risk level definitions can be found [here](/triage_findings/findings_workflows/finding_status_definitions/). +- **Priority**: A calculated numerical rank applied to all Findings that allows you to quickly understand vulnerabilities in context. +- **Age**: How old the selected Finding is. +- **SLA**: The due date by which the Finding is intended to be resolved. +- **Type**: Whether the Finding has been detected from a static or dynamic application security tool (Static, Dynamic or Static/Dynamic). +- **Location and Line**: The file and line number in which the selected Finding was found. +- **Component Name and Version**: The name and version of the component in which the selected Finding was found. +- **Date Discovered**: The date on which the Finding was discovered. +- **Planned Remediation Date and Version**: The date on which the Finding is planned to be remediated, and the version of the affected component in which the fix will be implemented. +- **Service**: Connected Services (self-contained pieces of functionality within an Asset) that are affected by the selected Finding. When populated, this field is included in deduplication matching (i.e., Findings with identical Service fields will deduplicate). +- **Reporter**: The User who revealed the Finding. +- **CWE**: The CWE classification of the Finding. +- **Vulnerability IDs**: Publicly recognized vulnerability identifiers associated with the Finding, such as CVE, GHSA, or other standardized advisory references. In DefectDojo Pro, they are also used to perform EPSS and KEV lookups. +- **Unique ID From Tool**: A stable identifier assigned by the source tool to a specific Finding instance. Unique IDs are intended to remain consistent across repeated scans, allowing the tool to recognize the same Finding over time. + - Unlike Vulnerability IDs, this value is proprietary to the reporting tool and is not a public vulnerability reference. + - Example: `finding-12345` +- **Vulnerability ID From Tool**: A proprietary vulnerability or rule identifier assigned by the source tool to describe the type of vulnerability that was detected. + - Unlike the Unique ID From Tool, this identifier is not unique to an individual Finding and may appear on many Findings that match the same detection rule. + - Unlike Vulnerability IDs, these identifiers are specific to the reporting tool and are not publicly standardized. + - Example: `semgrep.rule.lang.security.sql-injection` +- **EPSS Score / Percentile**: ESS score and percentile for the CVE. +- **Known Exploited**: Whether there is confirmation that the vulnerability has been exploited. +- **Ransomware Used**: Whether ransomware was involved in the exploitation of the vulnerability. +- **KEV Date**: The date the Finding was added to the KEV catalog. +- **Found By**: The type of tool that identified the vulnerability. +- **CVSSv3 and CVSSv4 Vector and Score**: The CVSS3 and CVSS4 vector and score of the selected Finding. +- **Integrator Tickets**: Third-party issue tracker ticket numbers associated with the Finding. + +### Vulnerable Endpoints +This section includes a table of the Endpoints that the selected Finding affects, along with any relevant metadata. + +### Additional Details +- **Request/Response Pairs**: A copy of the message sent by the client and the server's reply to the request. +- **Steps to Reproduce**: Steps for reproducing the Finding. +- **Severity Justification**: Written description of why a certain Severity rating was associated with the Finding. + +## Findings Data +Findings require the following metadata: +- **Name** +- **Date** +- **Severity** +- **Description** + +In addition to metadata corresponding to the tables in a Finding’s view, optional metadata fields include: +- **Tags**: Any tags that have been added to the Finding. +- **Owners**: The group of users that will be responsible for the selected Finding. +- **Push to Jira**: Pushes the Finding to Jira for ticketing purposes. +- **Push to Integrator**: Pushes the Finding to any integrated third-party issue trackers. +- **Risk and priority settings**: Offers the option to override DefectDojo’s automatic calculation of the Finding’s risk and priority. +- **Endpoints to add**: Vulnerable endpoints that may be affected by the selected Finding that are not reflected in the preceding list of systems/endpoints. +- **Defect review requested by**: Records who requested a defect review for the flaw in question. +- **SAST source object, line number, and file path**: Source object, line number, and file path of the attack vector. +- **SAST sink object**: Sink object of the attack vector. +- **Number of occurrences**: Number of occurrences in the source tool when several vulnerabilities were found and aggregated by the scanner. +- **Publish date**: The date on which the vulnerability was published. +- **Effort estimation**: The level of effort involved in fixing the Finding (e.g., Low, Medium, or High). + +The exact metadata available will depend on the parser/scanner that revealed the Finding. Some provide only basic information such as title and severity, while others include CVSS vectors, vulnerable components, endpoints, request/response pairs, and other scanner-specific metadata. + +This metadata improves filtering, reporting, and prioritization across your security program, enabling long-term tracking and trend analysis. Additional details and metadata descriptions can be found [here](/triage_findings/findings_workflows/intro_to_findings/#a-finding-page). + +### Deduplication +DefectDojo includes deduplication capabilities that help identify and manage Findings representing the same underlying vulnerability. As scan results are imported from one or more tools, DefectDojo uses configurable matching logic to identify Findings that represent the same vulnerability. + +Deduplication prevents the same vulnerability from appearing multiple times when discovered repeatedly by the same or different scanners, allowing remediation history to remain attached to a single Finding. + +More information about deduplication can be found [here](/triage_findings/finding_deduplication/about_deduplication/). + +### Reimport +DefectDojo's Reimport function allows Findings to be updated as new scan results are imported. When a scan is reimported, DefectDojo compares the incoming results against existing Findings and updates matching records instead of creating entirely new ones. This preserves valuable context such as status changes, remediation history, comments, and ownership information, providing a continuous record of a Finding's lifecycle across multiple testing cycles. + +More information about the Reimport function can be found [here](/import_data/import_intro/reimport/). + +### Risk Acceptances +Risk Acceptances are a special status that can be applied to Findings to formally document and operationalize the decision to acknowledge them without immediately remediating them. + +More information about Risk Acceptances can be found [here](/triage_findings/findings_workflows/pro__risk_acceptance/). + +### Statuses +Each Finding created in DefectDojo has a Status that communicates relevant information and helps your team keep track of their progress in resolving issues. + +More information about Statuses can be found [here](/triage_findings/findings_workflows/finding_status_definitions/). + +## Working with Findings + +### Creating Findings +While most Findings are generated automatically through scan imports and integrations, DefectDojo also supports the manual creation of Findings. Manual Findings are useful for tracking vulnerabilities and security concerns identified through penetration testing, architecture reviews, compliance assessments, bug bounty programs, consultant engagements, or other activities that do not produce scanner output. + +Findings can be manually added by either clicking **New Finding** within the **Findings** section of the sidebar, or by selecting **Add Finding** within the gear menu of the Test you wish to add the Finding to. + +### Editing Findings +The ⋮ kebab menu next to Findings contains the following functions: +- **Edit Finding**: Edit the Finding. +- **Close Finding**: Initiates the process of closing the Finding. +- **Request Review**: Initiates the Peer Review process and changes the Finding’s status to “Under Review.” More information about Peer Reviews can be found [here](/triage_findings/findings_workflows/finding_status_definitions/#under-review). +- **Add Risk Acceptance**: Initiates the Risk Acceptance process. More information can be found [here](/triage_findings/findings_workflows/pro__risk_acceptance/). +- **Add File**: Initiates the process to add a file to the Finding (see the section below). +- **Add Note**: Initiates the process to add a note to the Finding. +- **Add Custom Field**: Initiates a pop-up that allows you to add and define a custom field to apply to the Finding. +- **Push to Jira**: Pushes the Finding to Jira for ticketing purposes. +- **Push to Integrator**: Pushes the Finding to any integrated third-party issue trackers. +- **Delete Finding**: Deletes the selected Finding. +- **Finding History**: Reveals the history of the selected Finding. + +#### Attaching Files to Findings +You can attach files to any Finding to provide additional context — for example, a screenshot of a vulnerability in action or a proof-of-concept image. + +Supported file types include: + +``` +.txt .pdf .json .xml .csv .yml .png .jpeg +.sarif .xlsx .doc .html .js .nessus .zip .fpr +``` + +To attach a file to a Finding, click **Add File** from within either the ⋮ kebab menu or the gear menu of the selected Finding. Enter a Title for the file, choose the file from your computer, and click **Submit**. + +The file will then appear in the Files section of the **Test Overview** table within the Finding’s view. + +#### Bulk Edit Findings +Findings can be edited in bulk from a Finding List, such as the table of All Findings accessible from the sidebar, or from the table of Findings within a specific Test. + +More information about how to bulk edit Findings can be found [here](/triage_findings/findings_workflows/editing_findings/#bulk-edit-findings). + +### Closing Findings +Once the work on a Finding is complete, you can manually close it by clicking **Close Finding** within the Finding’s ⋮ kebab menu or gear menu. Alternatively, if a scan is re-imported into DefectDojo which does not contain a previously-recorded Finding, the previously-recorded Finding will automatically close. + +If you don’t want any Findings to be closed, you can disable this behavior on the Reimport Scan form: + +- Uncheck the Close Old Findings checkbox if using the UI +- Set close_old_findings to False if using the API ​ + +### Deleting Findings +Deleting a Finding can be done from the Finding’s ⋮ kebab menu or gear menu. This action can’t be undone. + +For auditing purposes, it is recommended to close any remediated Findings, rather than deleting them. + +## Finding Groups +**Finding Groups** allow you to treat multiple related Findings as a single logical unit for triage, reporting, and remediation coordination. + +For example, a scan might produce 10 SQL injection Findings across different endpoints. Instead of managing each one independently, you can group them into a single Finding Group representing the broader SQL injection issue. + +A Finding Group does not replace the individual Findings. Each Finding still exists with its own severity, status, metadata, comments, and remediation history. A Finding Group simply provides an additional organizational layer above the Findings it contains. + +### Accessing Finding Groups +Finding Groups can be accessed via the sidebar. The submenu provides access to Open and Closed Finding Groups as well as All Finding Groups (regardless of Open status). + +![image](images/profindings_ss1.png) + +### Creating Finding Groups +Finding Groups can be created either manually or automatically. + +Notably, Finding Groups can only be created from the Findings contained within a single Test. Findings from different Tests, Engagements, or Products cannot be added to the same Finding Group. + +#### Manual Finding Groups +To manually perform Finding Group actions: +1. Navigate to a list of Findings within a Test. +2. Select the Finding(s) you wish to add to a Finding Group by clicking the Finding’s corresponding checkbox. +3. Click the **Finding Group** button that appears at the top of the Finding list. +4. Click the corresponding action you wish to complete. + - **Add to New Finding Group**: Creates a new Finding Group that includes the selected Findings. + - **Add to Existing Finding Group**: Adds the selected Findings to a preexisting Finding Group. + - **Remove from Finding Group**: Removes the selected Findings from any Finding Groups they were previously a part of. +5. Click **Submit**. + +Note that grouping will be disabled unless every selected finding is editable, ungrouped, and in the same Test. + +Further, note that the only possible action when selecting Findings from the All Findings list is to remove the selected Findings from any Finding Group. This is because, as mentioned, Finding Groups can only be created from the Findings contained within a single Test. + +#### Automatic Finding Groups +When importing a scan, the **Group By** feature within the collapsible **Optional Fields** menu can automatically create Finding Groups based on a chosen grouping method. This is useful when a scanner produces many related Findings that should be managed together. + +The adjoining **Create Finding Groups for all Findings** checkbox performs two functions: +- **Checked**: Creates a Finding Group for every imported Finding, even if that Finding is the only member of the group. +- **Unchecked**: Creates Finding Groups only when there are actually multiple Findings to group together. + +![image](images/profindings_ss2.png) + +If an option is not selected from the Group By dropdown menu during import (e.g., **Finding Title** in the screenshot above, etc.), no grouping will occur. + +If the grouping criteria (e.g., component name, vulnerability ID, Finding title, etc.) isn’t populated in the Finding, it will not have a group created or be added to a preexisting Finding Group. + +If a scan is imported that reveals 10 Findings that are not grouped and the same scan is reimported and the Findings are grouped, the first 10 Findings will not be added to that Finding Group (i.e., the Finding Group will only include the 10 Findings from the reimport, not the 10 Findings from the initial import). + +## Finding Templates +**Finding Templates** allow Users to create reusable templates for commonly reported vulnerabilities and security issues. A template can include standardized information such as a title, description, impact, steps to reproduce, mitigation, references, and other Finding metadata. + +Finding Templates are most useful in situations where Users need to create manual Findings repeatedly and want to avoid re-entering the same supporting information each time. + +### Accessing Finding Templates +Finding Templates are found within the Findings submenu in the sidebar. + +![image](images/profindings_ss1.png) + +### Creating Finding Templates +Finding Templates can be created by clicking the **New Finding Template** button at the top left of the Finding Templates view. + +The ensuing page provides an overview of the metadata that will be applied to a Finding when a Finding Template is used. + +### Applying Finding Templates +Finding Templates differ between OS DefectDojo and DefectDojo Pro. In Pro, Finding Templates can’t be applied to preexisting Findings, and they can’t be created based on preexisting Findings. + +However, you can manually add a Finding to a Test based on a Finding Template using either the ⋮ kebab menu next to the Test in the parent Engagement’s view, or using the gear menu in the Test’s view. + +![image](images/profindings_ss3.png) + +![image](images/profindings_ss4.png) + +## Reporting +DefectDojo’s report builder lets you assemble a custom report from a set of content widgets, run it, and export the result (for example, by printing it to PDF). Custom reports can summarize the Findings or Endpoints you want to share with an external audience, and can include branding and boilerplate text. + +More information about DefectDojo’s Report Builder can be found [here](/metrics_reports/reports/report-builder/). + +### Export Findings +Pages that show a list of Findings or a list of Engagements have a CSV and Excel export option at the top left. For Findings, there is also the option to perform a Quick Export, which will open a new tab with tables of metadata pertaining to each Finding. diff --git a/docs/content/en/open_source/upgrading/2.57.md b/docs/content/en/open_source/upgrading/2.57.md deleted file mode 100644 index aeb39538930..00000000000 --- a/docs/content/en/open_source/upgrading/2.57.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: 'Upgrading to DefectDojo Version 2.57.x' -toc_hide: true -weight: -20260302 -description: No special instructions. ---- -There are no special instructions for upgrading to 2.57.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.57.0) for the contents of the release. diff --git a/docs/content/en/open_source/upgrading/2.58.md b/docs/content/en/open_source/upgrading/2.58.md deleted file mode 100644 index 233bdf1802f..00000000000 --- a/docs/content/en/open_source/upgrading/2.58.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: 'Upgrading to DefectDojo Version 2.58.x' -toc_hide: true -weight: -20260406 -description: No special instructions. ---- -There are no special instructions for upgrading to 2.58.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.58.0) for the contents of the release. diff --git a/docs/content/en/open_source/upgrading/2.59.md b/docs/content/en/open_source/upgrading/2.59.md deleted file mode 100644 index e51e9faa82d..00000000000 --- a/docs/content/en/open_source/upgrading/2.59.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: 'Upgrading to DefectDojo Version 2.59.x' -toc_hide: true -weight: -20260504 -description: No special instructions. ---- -There are no special instructions for upgrading to 2.59.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.59.0) for the contents of the release. diff --git a/docs/content/en/open_source/upgrading/2.60.md b/docs/content/en/open_source/upgrading/2.60.md deleted file mode 100644 index e7811aa0b99..00000000000 --- a/docs/content/en/open_source/upgrading/2.60.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: 'Upgrading to DefectDojo Version 2.60.x' -toc_hide: true -weight: -20260601 -description: No special instructions. ---- -There are no special instructions for upgrading to 2.60.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.60.0) for the contents of the release. diff --git a/docs/content/en/open_source/upgrading/3.1.md b/docs/content/en/open_source/upgrading/3.1.md deleted file mode 100644 index 0adbbef6a27..00000000000 --- a/docs/content/en/open_source/upgrading/3.1.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -title: 'Upgrading to DefectDojo Version 3.1.x' -toc_hide: true -weight: -20260615 -description: New optional setting DD_OS_MESSAGE_ENABLED to control the open-source promo banner, and a new deduplication execution mode for import/reimport. ---- -There are no breaking changes when upgrading to 3.1.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/3.1.0) for the contents of the release. - -### New setting: `DD_OS_MESSAGE_ENABLED` - -This release adds the `DD_OS_MESSAGE_ENABLED` setting (default `True`), which controls the open-source promotional ("Upgrade to Pro") banner. The default preserves the existing behavior. Set `DD_OS_MESSAGE_ENABLED=False` to hide the banner; when disabled, DefectDojo skips the outbound request that fetches the message. - -## Deduplication execution mode for import/reimport - -This release adds a new `deduplication_execution_mode` setting that controls how -import/reimport deduplication post-processing is dispatched and whether the API -response waits for it. It can be set per user (profile) and overridden per request -on the import and reimport endpoints. - -Modes: - -- `async` (default): deduplication and the rest of post-processing are dispatched - to the background and the response returns immediately. This is the historical - behavior; nothing changes for existing users. -- `async_wait`: post-processing is still dispatched to the background, but the - request waits for deduplication to finish before responding. As a result the - `scan_added` notification and the statistics in the import/reimport response - reflect the deduplicated state (findings that turned out to be duplicates are - no longer counted/listed as new). JIRA push, product grading and other - non-deduplication tasks remain asynchronous and are not awaited. -- `sync`: import deduplication runs inline in the web request. - -The wait in `async_wait` is bounded by the new `DD_DEDUPLICATION_ASYNC_WAIT_TIMEOUT` -environment variable (default `60` seconds). If no worker picks up the work within -the timeout, the request responds anyway (degrading to the `async` outcome) rather -than hanging. - -The import/reimport response now also includes a `deduplication_complete` boolean -indicating whether deduplication had finished by the time the response was produced. - -### Relationship to `block_execution` - -The existing `block_execution` profile flag is unchanged. It remains the global -switch that forces **all** of a user's asynchronous tasks (notifications, JIRA -push, product grading, deduplication, ...) to run in the foreground. -`deduplication_execution_mode` is independent and narrower — it only affects -import/reimport deduplication post-processing. A user who has `block_execution` -enabled continues to get fully synchronous imports; the upgrade migration seeds -their `deduplication_execution_mode` to `sync` so behavior is unchanged. - -No action is required to upgrade. diff --git a/docs/content/get_started/pro/pro_features.md b/docs/content/get_started/pro/pro_features.md index 513c0755832..f7c06776770 100644 --- a/docs/content/get_started/pro/pro_features.md +++ b/docs/content/get_started/pro/pro_features.md @@ -22,7 +22,7 @@ See our [Pro UI Guide](/get_started/about/ui_pro_vs_os/) for more information. ### Assets/Organizations -DefectDojo Pro allows for improved organizational visualization for large lists of repositories or other business structures. See [Assets/Organizations documentation](/asset_modelling/pro_hierarchy/assets_organizations/) for details. +DefectDojo Pro allows for improved organizational visualization for large lists of repositories or other business structures. See [Assets/Organizations documentation](/asset_modelling/pro_hierarchy/asset_hierarchy/) for details. ![image](images/asset_hierarchy_diagram.png) @@ -39,6 +39,12 @@ See our [Rules Engine Guide](/automation/rules_engine/about) for more info. ![image](images/rules_engine_4.png) +### Sensei + +DefectDojo Pro's **Sensei** (BETA) is an AI-powered scan-and-fix capability: connect a repository through a GitHub App and Sensei scans it, imports the findings, and opens pull requests that remediate them — with a preview-first workflow, so nothing runs (and no LLM cost is incurred) until you approve. + +See our [Sensei Guide](/sensei/about_sensei/) for more info. + ### Pro Dashboards and Reporting Generate [instant reports and metrics](/get_started/about/ui_pro_vs_os/#new-dashboards) to share the security posture of your apps and repos, evaluate your security tools and analyze your team's performance in addressing security issues. diff --git a/docs/content/import_data/pro/connectors/about_connectors.md b/docs/content/import_data/pro/connectors/about_connectors.md index 21219670467..ff12aa702e8 100644 --- a/docs/content/import_data/pro/connectors/about_connectors.md +++ b/docs/content/import_data/pro/connectors/about_connectors.md @@ -34,8 +34,10 @@ We currently support Connectors for the following tools, with more on the way: * **Checkmarx ONE** * **Dependency-Track** * **Group-IB ASM** +* **Intruder** (cloud vulnerability scanner: pulls targets, issues, and occurrences) * **IriusRisk** * **JFrog Xray** +* **Kubescape** (Kubernetes posture: reads the Kubescape operator's in-cluster results) * **Probely** * **Semgrep** * **SonarQube** diff --git a/docs/content/import_data/pro/connectors/connectors_tool_reference.md b/docs/content/import_data/pro/connectors/connectors_tool_reference.md index 1ba6cb02969..78ed01e472c 100644 --- a/docs/content/import_data/pro/connectors/connectors_tool_reference.md +++ b/docs/content/import_data/pro/connectors/connectors_tool_reference.md @@ -26,10 +26,29 @@ Most Connectors import **findings** from a security tool. **Asset Connectors** w * **Discover** and **Sync** both reconcile the asset list. New assets appear as `NEW` Records; once mapped (automatically, if auto-mapping is enabled), DefectDojo creates the Product and groups it under a Product Type derived from the tool — for example, the GitLab namespace or the Azure DevOps project. * If an asset is later removed upstream (for example, a repository is deleted), its mapped Record is flagged `MISSING` on the next Sync so your team can triage it. DefectDojo never silently deletes a Product. -Azure DevOps, Bitbucket, GitHub, GitLab, and Jira Service Management Assets are Asset Connectors. All other Connectors listed below import findings. +Azure DevOps, Bitbucket, GitHub, GitLab, Jira Service Management Assets, and ServiceNow CMDB are Asset Connectors. All other Connectors listed below import findings. # **Supported Connectors** +## **Acunetix 360** + +The Acunetix 360 connector imports **DAST vulnerability findings** from the Acunetix 360 cloud platform (the Invicti platform). DefectDojo discovers your account's scanned websites and creates a Record for each **website**; the findings for a website come from its latest completed scan. + +**Please note:** this connector is for **Acunetix 360** (the cloud product at `online.acunetix360.com`). It is not for the on\-premises Acunetix Standard/Premium scanner, which has a different API. + +#### Prerequisites + +An Acunetix 360 account and an **API credential**: in Acunetix 360, open your account menu \> **API Settings**, and note the **API User ID** and generate an **API Token**. The connector authenticates with these as HTTP Basic credentials, so a dedicated service account is recommended to distinguish automated activity from manual team actions. + +#### Connector Mappings + +1. Enter your Acunetix 360 URL in the **Location** field: `https://online.acunetix360.com`. +2. Enter the API User ID in the **API User ID** field. +3. Enter the API Token in the **API Token** field. +4. Optionally, set a **Minimum Severity** to limit which findings are imported. + +Each scanned website becomes a Record. Findings come from the website's latest completed scan; vulnerabilities Acunetix 360 has marked **Accepted Risk** or **False Positive** are still imported but flagged inactive (risk\-accepted or false\-positive) so the DefectDojo product reflects the vendor's triage. + ## **Akamai API Security** The Akamai API Security connector uses an API key to pull security findings from the Akamai API. DefectDojo will discover your Akamai environment and create separate Records for each **Application** and **Host** configured in your account. @@ -186,6 +205,22 @@ With **Auto\-Map** enabled, a single Discover \+ Sync builds the complete Produc **A note on the reverse direction:** displaying DefectDojo findings and grades *inside* Backstage (on entity pages) is a natural follow\-on that would be built as a Backstage frontend plugin consuming the DefectDojo REST API — it is deliberately out of scope for this connector, which only pulls catalog data into DefectDojo. +## **Black Duck** + +The Black Duck connector imports **software composition analysis (SCA)** findings from a Black Duck (Synopsys / Black Duck) Hub instance. DefectDojo discovers every project in the instance and creates a Record for each **project**; the findings for a project come from the vulnerable BOM components of its selected version. + +#### Prerequisites + +A Black Duck **API token** for a user that can see the projects you want to import. In Black Duck, open your user menu \> **My Access Tokens** \> **Create New Token**, grant it (at least) read access, and copy the token when it is shown — it is displayed only once. The connector exchanges this token for a short\-lived bearer on each sync; it is never stored in cleartext beyond the connector's secret field. + +#### Connector Mappings + +1. Enter your Black Duck hub URL in the **Location** field — for example `https://your-company.app.blackduck.com`. +2. Enter the API token in the **Secret** field. +3. Optionally, set a **Minimum Severity** to limit which findings are imported. + +Each Black Duck project becomes a Record. By default the connector imports the project's **released** version (falling back to its first version); each vulnerable BOM component of that version becomes a finding, titled `{vulnerability} in {component}:{version}`. + ## **Bitbucket** The Bitbucket connector is an **Asset Connector**: it enumerates the repositories in the Bitbucket Cloud workspaces you name and creates a DefectDojo Asset for each repository, grouped into Organizations by Bitbucket project. No findings are imported. @@ -208,6 +243,22 @@ Only Bitbucket Cloud (bitbucket.org) is supported. Bitbucket Server reached end Each repository becomes a Record named after the repository, grouped by its Bitbucket **project**. +## **Bugcrowd** + +The Bugcrowd connector uses the Bugcrowd REST API to import submissions from your bug bounty and vulnerability disclosure programs. DefectDojo discovers the programs your API token can access and creates a Record for each one, importing that program's submissions as findings. + +#### Prerequisites + +You will need a Bugcrowd **API token** with access to the programs you want to import. We recommend creating a dedicated service account for DefectDojo so automated activity is easy to distinguish from manual team actions. Generate the token in Bugcrowd under **Organization settings \> API credentials**; read access to submissions, programs, and targets is sufficient. + +#### Connector Mappings + +1. Enter `https://api.bugcrowd.com` in the **Location** field. +2. Enter your Bugcrowd API token in the **Secret** field. It is sent as an `Authorization: Token` header. +3. Optionally, set a **Minimum Severity** to limit which findings are imported. + +Each Bugcrowd **program** becomes a Record, and its submissions are imported as findings with the Bugcrowd severity preserved. Duplicate submissions are excluded, so reimport does not create repeated findings for the same issue. + ## **BurpSuite** DefectDojo’s Burp connector calls Burp’s GraphQL API to fetch data. @@ -437,6 +488,30 @@ You will need a GitGuardian API key. We recommend a **Service Account token** (r Only **open** incidents (status `TRIGGERED` or `ASSIGNED`) are imported; incidents you resolve or ignore in GitGuardian are automatically mitigated in DefectDojo on the next sync. A confirmed-live secret (validity *valid*) is imported as a verified finding. +## **GitHub** + +The GitHub connector is an **Asset Connector**: it enumerates the repositories your token can access and creates a DefectDojo Asset for each one, grouped into Organizations by GitHub owner (organization or user). No findings are imported. + +**Please note:** this connector imports your repository **inventory** only. To import GitHub security alerts — code scanning, Dependabot, and secret scanning — as findings, use the separate **GitHub Advanced Security** connector below. The two are independent and can be run together. + +#### Prerequisites + +The connector authenticates with a GitHub **personal access token** and reads only repository **metadata** (name, description, URL, and owner) — it does not access your code, issues, or security alerts. It imports every repository the token's account owns, collaborates on, or is an organization member of, so confirm the token's account can see the repositories you want to mirror. We recommend a dedicated service account. + +The token only needs read-only access to repository metadata: + +- A *fine-grained* token needs **Repository permissions → Metadata: Read-only**, granted to the repositories (or the whole organization) you want to import. +- A *classic* token needs the **`repo`** scope to include private repositories (use **`public_repo`** if you only need public ones), plus **`read:org`** so organization-owned repositories resolve. + +Only GitHub.com (including GitHub Enterprise Cloud) is supported. GitHub Enterprise **Server** is not supported by this connector at this time. + +#### Connector Mappings + +1. Enter `https://api.github.com` in the **Location** field. +2. Enter the personal access token in the **Secret** field. + +No organization or repository list needs to be entered — DefectDojo imports every repository the token can see. Each repository becomes a Record named after the repository, grouped by its GitHub **owner** (organization or user). If a repository is later deleted, or the token loses access to it, its mapped Record is flagged `MISSING` on the next Sync rather than removed — DefectDojo never silently deletes a Product. + ## **GitHub Advanced Security** The GitHub Advanced Security connector imports **code scanning**, **Dependabot**, and **secret scanning** alerts from GitHub, as three separate finding types (`GitHub:CodeScanning`, `GitHub:Dependabot`, and `GitHub:SecretScanning`). DefectDojo discovers every non\-archived repository in the configured organization and creates a Record for each one. @@ -568,6 +643,19 @@ DefectDojo creates a separate Record for each domain you have verified with HIBP See the [Have I Been Pwned API documentation](https://haveibeenpwned.com/API/v3) for more information. +## **Intruder** + +The Intruder connector uses the [Intruder REST API](https://developers.intruder.io/) to pull your whole account's posture into DefectDojo. Each Intruder **target** is discovered as a Record (Product); each **occurrence** of an issue on a target becomes a Finding. + +#### Connector Mappings + +1. Leave the **Location** field as `https://api.intruder.io/` (the default Intruder API server). +2. Enter an Intruder **API access token** in the **Secret** field. + +Generate an access token in Intruder under **My account > API Access Tokens** (you'll need your account password to create it, and the token is shown only once). See the [Intruder API documentation](https://developers.intruder.io/docs/creating-an-access-token) for details. + +Findings are derived per occurrence: severity comes from the issue severity, CVEs and CVSS from the occurrence, the location from the target/port, and a snoozed occurrence is imported as an inactive (false-positive or risk-accepted) finding. + ## **IriusRisk** The IriusRisk connector uses an API token to pull threat modeling data from your IriusRisk instance. @@ -612,7 +700,20 @@ Required token scopes for JFrog Xray: - **All Services**, as DefectDojo needs access to both access to both XRay and Artifactory services - **Manage Reports + Manage Resources** at a minimum. -DefectDojo maps each Artifactory **repository** as a separate Record. On first Sync, DefectDojo generates a full historical vulnerability report; subsequent Syncs generate incremental (delta) reports covering new findings since the last Sync. +By default, DefectDojo maps each Artifactory **repository** as a separate Record. Each Sync generates a complete vulnerability report per repository via Xray, so finding statuses in DefectDojo always reflect the current state of the repository. + +#### Artifact-Level Records + +Enabling the **Artifact-Level Records** toggle on the connection changes discovery to one level below the repository: every first-level entry under a repository root (for Docker repositories, each image; for generic repositories, each top-level file or folder) becomes its own Record. Each Sync still generates a single Xray report per repository — DefectDojo attributes each vulnerability to the artifacts it impacts, so the load on your JFrog instance does not increase. + +With Artifact-Level Records enabled: + +* Repositories remain as Records and become **parent assets**: they carry no findings themselves, but when the Asset Hierarchy feature is enabled, DefectDojo automatically relates each artifact asset to its repository asset with a `parent` relationship. Assets can then be filtered by parent/child, and findings roll up the hierarchy. +* A vulnerability that impacts several artifacts is imported into each affected artifact's asset, so every asset shows the complete set of findings that affect it. +* Hierarchy relationships created by the connector never overwrite relationships you created by hand. If an asset already has a parent you assigned, the connector leaves it alone. +* The token additionally needs read access to the Artifactory storage API (included in the scopes above). + +**Switching an existing connection to Artifact-Level Records:** the toggle can be changed at any time. On the first Sync afterward, new artifact Records appear for mapping — enable **Auto Map** on the connection when flipping the toggle so findings move without a gap. The repository-level assets stop receiving findings and their previously imported findings are closed on their next Sync (the same findings are re-imported under the new artifact assets, with fresh status); notes and history on the old repository-level findings stay on the repository asset. Switching back reverses this: repository Records resume carrying findings (previously closed findings re-open as they re-match), and artifact Records are marked MISSING — their assets and findings are kept but stop updating, so you can archive them at your convenience. See the [JFrog Xray REST API documentation](https://jfrog.com/help/r/jfrog-rest-apis/xray-rest-apis) for more information. @@ -634,6 +735,23 @@ The JSM Assets connector is an **Asset Connector**: it enumerates the objects in Each Assets object becomes a Record named after the object's label, grouped by its **object schema**. +## **Kubescape** + +The Kubescape connector reads Kubernetes posture (misconfiguration) results produced by the [Kubescape operator](https://kubescape.io/docs/install-operator/) directly from the cluster's Kubernetes API — no ARMO SaaS account is required. It reads the `WorkloadConfigurationScan` objects served by the operator's in-cluster storage aggregated API (`spdx.softwarecomposition.kubescape.io/v1beta1`). Each Kubernetes **namespace** that has posture results is mapped to a Record (Product); each failed control on a workload becomes a Finding. + +#### Prerequisites + +- The Kubescape operator must be installed in the target cluster with configuration scanning enabled (see [Installing in your cluster](https://kubescape.io/docs/install-operator/)). Confirm results exist with `kubectl get workloadconfigurationscans -A`. +- A **kubeconfig** granting read access to the `spdx.softwarecomposition.kubescape.io` API group (list/get on `workloadconfigurationscans`) for the target cluster. + +#### Connector Mappings + +1. Enter the cluster's API server URL (or a friendly cluster identifier) in the **Location** field. +2. Paste the **kubeconfig** for the target cluster in the `kubeconfig` field. Optionally set `kube_context` to select a context within it, and `cluster_name` to label the discovered Products. +3. Each namespace with posture results is discovered as a Record; map the ones you want to import to DefectDojo Products. + +Findings are derived per failed control: the control name and workload identify the Finding, severity comes from the control's score factor, the control ID becomes the vulnerability ID, and each Finding links to its control reference at `https://hub.armosec.io/docs/`. + ## **Microsoft Defender** The Microsoft Defender connector imports device vulnerability findings from **Microsoft Defender Vulnerability Management (MDVM)** — one finding per device / software version / CVE combination, including severity, CVSS score, exploitability level and recommended security updates. DefectDojo will discover your Defender **device groups** and create a Record for each one; devices that aren't assigned to any device group are collected under a synthetic **Unassigned** group. @@ -697,6 +815,25 @@ Unlike the device\-based Microsoft Defender connector, no API permissions or adm Each enabled Azure subscription becomes a Record. Findings are read through Azure Resource Graph, so they surface promptly once Defender for Cloud has scanned your resources — but the scans themselves run on Microsoft's schedule: container\-registry images are usually scanned within an hour of being pushed, while a VM's first agentless vulnerability scan can take several hours. A newly enabled subscription will legitimately Sync zero findings until its resources have been scanned. +## **NeuVector** + +The NeuVector connector uses the [NeuVector](https://github.com/neuvector/neuvector) controller REST API to import container **image vulnerability scans**. DefectDojo discovers every image NeuVector has scanned and creates a Record for each, then imports that image's scan report as findings. + +#### Prerequisites + +You will need a NeuVector **username and password** for a controller account with permission to read scan results. The connector logs in with these credentials to obtain a session token; the password and token are never logged. + +#### Connector Mappings + +1. Enter your NeuVector controller URL in the **Location** field, including the REST API port — for example `https://neuvector.example.com:10443`. +2. Enter the controller **Username** and **Password**. +3. If your controller uses a self-signed certificate, set **Skip TLS Verification** to `true`. +4. Optionally, set a **Minimum Severity** to limit which findings are imported. + +DefectDojo maps each scanned **image** to a Record and each **CVE** in its scan report to a finding. The severity comes from NeuVector's own rating, and the affected package and version, CVSSv3 score and vector, fix version (as mitigation) and reference link are carried over. Findings are de-duplicated on the image, CVE, package, version and severity. + +See the [NeuVector API documentation](https://open-docs.neuvector.com/automation/automation) for more information. + ## **Nuclei (ProjectDiscovery Cloud)** The Nuclei connector uses the ProjectDiscovery Cloud Platform (PDCP) REST API to pull [nuclei](https://github.com/projectdiscovery/nuclei) scan results from your PDCP account. DefectDojo discovers every scan in the account and creates a separate Record for each **scan**. @@ -779,6 +916,28 @@ A Qualys user account with **VMDR API access**, and your subscription's **API se Each Qualys host becomes a Record. Detections Qualys has marked **Fixed** are excluded, so reimport closes remediated findings. +## **Quay** + +The Quay connector uses the Project Quay REST API to discover container repositories and import the vulnerability reports produced by Quay's built-in **Clair** scanner. DefectDojo creates a Record for each Quay **repository** and, on each Sync, reads the Clair security report of every active tag's image manifest. + +#### Prerequisites + +Security scanning (Clair) must be enabled on your Quay instance, and you will need a Quay **OAuth 2 access token**: + +* In Quay, create (or open) an Organization, go to **Applications**, create an OAuth application, then **Generate Token** with at least the **Read repositories** scope. A dedicated application for DefectDojo is recommended. +* The token is sent as a Bearer token on every request and is never logged. + +#### Connector Mappings + +1. Enter your Quay base URL in the **Location** field, for example `https://quay.io` or your self-hosted `https://quay.example.com`. The URL must be HTTPS; do not include a trailing API path — DefectDojo constructs the API paths automatically. +2. Enter the OAuth access token in the **Secret** field. +3. Optionally, set a **Namespace** to restrict discovery to a single Quay organization or user. Leave blank to discover every repository the token can read. +4. Optionally, set a **Minimum Severity** to limit which findings are imported. + +DefectDojo maps each Quay **repository** to a Record. For each repository it lists the active tags, deduplicates them to their unique image manifests (a manifest shared by multiple tags is scanned once), and reads each manifest's Clair report. Manifests Clair has not finished scanning (for example a multi-architecture manifest list, or an image still queued) are skipped until a later Sync. Each Clair vulnerability becomes a finding — the affected package is the component, the fixed version becomes the mitigation, and Clair's **Negligible**/**Unknown** severities are recorded as **Informational**. + +See the [Project Quay API documentation](https://docs.projectquay.io/api_quay.html) and the [Clair documentation](https://quay.github.io/clair/) for more information. + ## **Rapid7 InsightAppSec** The Rapid7 InsightAppSec connector imports **DAST vulnerability findings** from the InsightAppSec cloud platform, enriched with attack\-module metadata (for example *SQL Injection*), CVSS scores, and the evidence collected by the scan. DefectDojo creates a Record for each InsightAppSec **app**. @@ -816,6 +975,25 @@ Network access from DefectDojo to your Security Console, and a console **user ac Each InsightVM site becomes a Record; the connector walks the site's assets and imports their vulnerable findings. +## **runZero** + +The runZero connector uses the runZero Export API to sync your whole organization's asset inventory into DefectDojo. It is primarily an **asset** connector: DefectDojo discovers every asset and creates a Record for each, grouped into a Product Type by its runZero **site**. It can optionally also import runZero's vulnerabilities as findings. + +#### Prerequisites + +You will need an organization **Export Token** from runZero (Account → API), which is prefixed `XT`. The token is organization-scoped (the organization is encoded in the token), read-only, and is sent as a Bearer token — it is never logged. A community/starter tier is available. + +#### Connector Mappings + +1. Enter your runZero console URL in the **Location** field, for example `https://console.runzero.com`. The URL must be HTTPS. +2. Enter the Export Token in the **Secret** field. +3. Optionally set **Import Vulnerabilities** to `true` to also import runZero vulnerabilities as findings; leave it blank to sync assets only. +4. Optionally, set a **Minimum Severity** to limit which vulnerability findings are imported (applies only when vulnerabilities are imported). + +DefectDojo maps each runZero **asset** to a Record (VEP): the display name comes from the asset's name or address, and its site, type, OS, addresses and tags are attached as attributes; the asset's **site** becomes its Product Type. Assets are synced with a full export that DefectDojo reconciles (adds/removes). When **Import Vulnerabilities** is enabled, each runZero vulnerability becomes a finding on its asset — mapping the severity, CVSS score, CVE, affected service (`protocol://address:port`) endpoint and the remediation. + +See the [runZero API documentation](https://help.runzero.com/) for more information. + ## **Semgrep** This connector uses the Semgrep REST API to fetch data. @@ -830,6 +1008,21 @@ Enter `https://semgrep.dev/api/v1/` in the **Location** field. See [Semgrep documentation](https://semgrep.dev/docs/semgrep-cloud-platform/semgrep-api/#tag__badge-list) for more info. +## **ServiceNow CMDB** + +The ServiceNow CMDB connector is an **Asset Connector**: instead of importing findings, it reads Configuration Items (CIs) from your ServiceNow Configuration Management Database and creates a DefectDojo Asset for each CI, grouped into Organizations by CI class. No findings are imported. + +#### Prerequisites + +You will need a ServiceNow instance and an account that can read the CMDB tables over the ServiceNow Table API. We recommend a dedicated, read-only service account for DefectDojo. The account needs read access to the `cmdb_ci` tables you want to import. + +#### Connector Mappings + +1. Enter your ServiceNow instance URL in the **Location** field: `https://{your-instance}.service-now.com`. +2. Select or create a ServiceNow **Tool Configuration** holding the instance credentials (the ServiceNow username and password). + +Each Configuration Item becomes a Record named after the CI, grouped by its **CI class** (for example, application, server, or business service). Discovery and Sync reconcile the CI list: new CIs appear as `NEW` Records, and a CI removed from the CMDB is flagged `MISSING` on the next Sync so your team can triage it. DefectDojo never silently deletes a Product. + ## **Shodan** The Shodan connector uses the Shodan REST API to import the vulnerabilities (CVEs) Shodan has observed on your internet-exposed hosts. You provide a Shodan search query that scopes the import to your own assets; DefectDojo creates a Record for each matching host and imports its CVEs as findings. @@ -876,6 +1069,41 @@ The Snyk connector uses the Snyk REST API to fetch data. See the [Snyk API documentation](https://docs.snyk.io/snyk-api) for more info. +## **Sonatype IQ** + +The Sonatype IQ connector uses the Sonatype IQ Server (Nexus Lifecycle) REST API to import open\-source component vulnerabilities. It enumerates every application in your IQ organization and, for each one, imports the component vulnerabilities from that application's latest report at the lifecycle stage you configure. DefectDojo creates a Record for each application automatically — there is no per\-application configuration. + +#### Prerequisites + +You will need a Sonatype IQ user account with the **View IQ Elements** permission on the applications you want to import. Sonatype recommends authenticating with a **user token** (generated under **My Profile > User Token** in IQ Server) rather than a password; the token's two parts map to the Username and User Token fields below. The connector works with both self\-hosted IQ Server and Sonatype\-hosted (SaaS) instances. + +#### Connector Mappings + +1. In the **Location** field, enter your IQ Server base URL — for a self\-hosted server, `https://iq.example.com`; for a Sonatype\-hosted instance, `https://.sonatype.app/platform`. +2. Enter the IQ user (or the user\-code part of your user token) in the **Username** field. +3. Enter the IQ user token (or password) in the **User Token** field. +4. Optionally, set a **Stage** to choose which lifecycle stage's report is imported per application (`build`, `stage-release`, `release`, and so on). Leave it blank to use `build`. +5. Optionally, set a **Minimum Severity** to limit which findings are imported. + +Each application becomes a Record, and each security issue in that application's latest report for the selected stage is imported as a finding. Severity is derived from the issue's numeric score, and CVE references, CWE, the CVSS vector, and the affected component's package URL (PURL) are included where available. +## **Sysdig Secure** + +The Sysdig Secure connector imports **container / CNAPP vulnerability findings** from Sysdig Secure's vulnerability management API. It syncs the whole account across the configured scope(s) and creates a DefectDojo product for each scanned asset grouping. + +#### Prerequisites + +A Sysdig Secure **API token**: in Sysdig Secure, go to **Settings \> Sysdig Secure API Token** and copy the token. You also need your Sysdig **region URL** (for example `https://us2.app.sysdig.com`, `https://eu1.app.sysdig.com`, or your on\-premises host). + +#### Connector Mappings + +1. Enter your Sysdig region/base URL in the **Location** field. +2. Enter the API token in the **Secret** field. +3. Optionally set **Scopes** — a comma\-separated list of `runtime`, `registry`, and/or `pipeline` (leave blank for `runtime`, the deployed\-workload scope). +4. Optionally set **Runtime Product Grouping** — how runtime results map to products: `cluster`, `namespace`, `workload`, or `image` (leave blank for `namespace`). Registry and pipeline results always group by image repository. +5. Optionally, set a **Minimum Severity** to limit which findings are imported. + +Each asset grouping becomes a Record. For each scan result the connector imports every vulnerable package as a finding. **Runtime** findings (deployed workloads) are recorded as dynamic findings and tagged with their Kubernetes cluster / namespace / workload / container context; **registry** and **pipeline** findings are recorded as static image\-scan findings. Sysdig's `NEGLIGIBLE` severity maps to Info. + ## Tenable The Tenable connector uses the **Tenable.io** REST API to fetch data. Scans are pulled from the Tenable VM `/scans` endpoint. diff --git a/docs/content/issue_tracking/pro_integration/integrations.md b/docs/content/issue_tracking/pro_integration/integrations.md index 6bc6860fad4..0d9b76bf44e 100644 --- a/docs/content/issue_tracking/pro_integration/integrations.md +++ b/docs/content/issue_tracking/pro_integration/integrations.md @@ -18,6 +18,7 @@ Supported Integrations: - GitHub - GitLab Boards - Jira +- Linear - PagerDuty - ServiceDesk Plus - ServiceNow @@ -57,6 +58,19 @@ Findings can also be pushed automatically, with the **Issue Tracker Assignment** - **Automatically Update Existing Link**: When Findings or Finding Groups are **updated** in the assigned Product or Engagement, automatically push the object to the Issue Tracker if an existing link has already been created manually. - **Automatically Link New and Update Existing Link**: When Findings or Finding Groups are created **or** updated in the assigned Product or Engagement, automatically push the object to the Issue Tracker. +#### Push Filters + +Each Issue Tracker Assignment can optionally narrow which Findings are pushed **automatically**: + +- **Minimum Severity**: only automatically create tickets for Findings at or above the selected severity. Leave it blank to include every severity. +- **Active findings only**: only automatically create tickets for active Findings, skipping ones that are already mitigated, false positive, or risk accepted when the assignment first sees them. + +These filters apply to automatic **creation** only. Updates to a Finding that already has a linked ticket are always sent, so status changes (including closures) continue to propagate. A manual **Push to Integrators** always ignores the filters. Leaving both at their defaults preserves the original behavior of pushing every Finding. + +#### Assigning multiple Products + +An Issue Tracker Assignment targets a single Product or Engagement. To cover several assets, create one Assignment per Product (or Engagement). If you also need vendor fields to differ per asset — for example a distinct ServiceNow **Assignment group** or **Assigned to**, or a different Jira project — create a separate Issue Tracker Mapping (with its own Custom Field Mappings) for each asset and point each Assignment at the matching Mapping. + ## Issue Tracker Ticket Representation Issue Tracker Tickets are represented by a series of icons under the "Integrator Tickets" column when viewing and listing @@ -83,6 +97,7 @@ For the complete list of requirements, please open the vendor specific pages bel - [GitHub](/issue_tracking/pro_integration/integrations_toolreference/#github) - [GitLab Boards](/issue_tracking/pro_integration/integrations_toolreference/#gitlab) - [Jira](/issue_tracking/pro_integration/integrations_toolreference/#jira) +- [Linear](/issue_tracking/pro_integration/integrations_toolreference/#linear) - [PagerDuty](/issue_tracking/pro_integration/integrations_toolreference/#pagerduty) - [ServiceDesk Plus](/issue_tracking/pro_integration/integrations_toolreference/#servicedesk-plus) - [ServiceNow](/issue_tracking/pro_integration/integrations_toolreference/#servicenow) diff --git a/docs/content/issue_tracking/pro_integration/integrations_toolreference.md b/docs/content/issue_tracking/pro_integration/integrations_toolreference.md index d501f6c2eca..446ef5d21a7 100644 --- a/docs/content/issue_tracking/pro_integration/integrations_toolreference.md +++ b/docs/content/issue_tracking/pro_integration/integrations_toolreference.md @@ -286,6 +286,49 @@ By default Jira issues use DefectDojo's built-in title and body. To customize th - **Token lifecycle (OAuth):** DefectDojo owns the whole flow — it performs the authorization-code exchange, stores the access and refresh tokens, and refreshes on demand before a push, persisting the new refresh token each time (Atlassian rotates it on every refresh). - **Credential storage:** all connection credentials (passwords, tokens, client secrets, OAuth tokens) are encrypted at rest and are never returned through the API — editing a connection shows a "leave blank to keep" placeholder for stored secrets. +## Linear + +The Linear integration allows you to push DefectDojo Findings as [Linear](https://linear.app/) Issues. Issues are created in a Team in your Linear workspace. + +### Instance Setup + +- **Label** should be the label that you want to use to identify this integration. +- **Location** should be set to `https://api.linear.app/graphql`. +- **API Key** should be set to a Linear personal API key. Keys can be generated in Linear under Settings, then Security & access, then [API](https://linear.app/settings/account/security). The key is sent to Linear's GraphQL API in the `Authorization` header. + +### Issue Tracker Mapping + +- **Team (Group) ID** should be set to the ID of the Linear Team that Issues will be created for. You can list your Teams and their IDs by calling the Linear GraphQL API: + +``` +curl -H "Authorization: {{API_KEY}}" -H "Content-Type: application/json" \ + -d '{"query":"{ teams { nodes { id name key } } }"}' https://api.linear.app/graphql +``` + +### Severity Mapping Details + +A Linear Issue carries a numeric **priority** rather than a severity field. Each DefectDojo severity maps to a Linear priority, where `1` is Urgent and `4` is Low: + +- **Severity Field Name**: `Priority` +- **Info Mapping**: `4` +- **Low Mapping**: `4` +- **Medium Mapping**: `3` +- **High Mapping**: `2` +- **Critical Mapping**: `1` + +### Status Mapping Details + +Each status value must be set to the ID of a Workflow State in your Linear Team. Workflow State IDs are unique to each workspace, so there are no default values. You can list the Workflow States and their IDs by calling the Linear GraphQL API: + +``` +curl -H "Authorization: {{API_KEY}}" -H "Content-Type: application/json" \ + -d '{"query":"{ workflowStates { nodes { id name type team { key } } } }"}' https://api.linear.app/graphql +``` + +- **Status Field Name**: `Workflow State ID` +- **Active Mapping**: the ID of a started or unstarted state, for example `Todo` or `In Progress`. +- **Closed Mapping**: the ID of a completed state, for example `Done`. When a Finding is deleted in DefectDojo, its Issue is moved to this state. + ## PagerDuty The PagerDuty Integration allows you to push DefectDojo Findings and Finding Groups as PagerDuty Incidents, opened on a PagerDuty Service of your choice. @@ -332,13 +375,34 @@ The ServiceNow Integration allows you to push DefectDojo Findings as ServiceNow ### Instance Setup -Your ServiceNow instance will require you to obtain a Refresh Token, associated with the User or Service account that will push Incidents to ServiceNow. +DefectDojo authenticates to ServiceNow over OAuth 2.0. How you create the OAuth credentials depends on your ServiceNow release — newer releases (Zurich and later) use a Client Credentials grant, while earlier releases use a refresh token. + +#### ServiceNow Zurich and later (client credentials) + +Recent ServiceNow releases deprecated the classic "Create an OAuth API endpoint for external clients" option in favor of the **New Inbound Integration Experience**, which issues an OAuth **Client Credentials** grant bound to a service account: + +1. In the left-hand navigation bar, search for "Application Registry" and select it. +2. Click **New**, then choose **New Inbound Integration Experience**. +3. Select **New Integration → OAuth - Client credentials grant**. +4. Set the **OAuth Application User** to the service account that will create Incidents. That account's roles determine what DefectDojo is allowed to write. +5. Save the registration. ServiceNow auto-generates the **Client ID** and **Client Secret** (leave those fields blank when creating the registration). + +Then, in DefectDojo: + +- **Instance Label** should be the label that you want to use to identify this integration. +- **Location** should be set to the URL for your ServiceNow server, for example `https://your-organization.service-now.com/`. +- **Client ID** should be the Client ID from the OAuth registration. +- **Client Secret** should be the Client Secret from the OAuth registration. + +Leave the Refresh Token, Username, and Password fields empty — DefectDojo requests a fresh client-credentials token for each sync. + +#### Earlier ServiceNow releases (refresh token) -You'll need to start by creating an OAuth registration on your ServiceNow instance for DefectDojo: +On releases that still offer the classic registration, obtain a Refresh Token associated with the User or Service account that will push Incidents to ServiceNow: -1. In the left-hand navigation bar, search for “Application Registry” and select it. -2. Click “New”. -3. Choose “Create an OAuth API endpoint for external clients”. +1. In the left-hand navigation bar, search for "Application Registry" and select it. +2. Click "New". +3. Choose "Create an OAuth API endpoint for external clients". 4. Fill in the required fields: * Name: Provide a meaningful name for your application (e.g., Vulnerability Integration Client). * (Optional) Adjust the Token Lifespan: @@ -366,7 +430,7 @@ If your ServiceNow credentials are correct, and allow for admin level-access to - **Location** should be set to the URL for your ServiceNow server, for example `https://your-organization.service-now.com/`. - **Refresh Token** is where the Refresh Token should be entered. - **Client ID** should be the Client ID set in the OAuth App Registration. -- **Client ID** should be the Client Secret set in the OAuth App Registration. +- **Client Secret** should be the Client Secret set in the OAuth App Registration. ### Severity Mapping Details @@ -385,6 +449,35 @@ This maps to the ServiceNow Impact field. - **False Positive Mapping**: `Resolved` - **Risk Accepted Mapping**: `Resolved` +Each mapping accepts a standard state label (`New`, `In Progress`, `On Hold`, `Resolved`, `Closed`, `Cancelled`) or a numeric state value. On instances with customized Incident states — or when targeting a table other than `incident` — use the numeric **state value** from your instance's choice list; a numeric value outside the standard set is sent to ServiceNow exactly as configured. The built-in Resolution-code default only accompanies the standard resolved/closed states, so pair custom state values with the close and resolution field mappings below. + +### Close and resolution fields + +Some ServiceNow instances enforce a Data Policy that makes fields such as the **Resolution code** (`close_code`) mandatory whenever an Incident moves to a resolved or closed state. If DefectDojo closes an Incident without them, ServiceNow rejects the write with an HTTP 403 *"Data Policy Exception"* and the reason is recorded in the integration's Errors view. + +Attach the required fields to the state change with **Custom Field Mappings**, setting **Apply On** to the disposition that should carry them: + +- **Transition to Closed** — sent when a Finding is mitigated / closed. +- **Transition to False Positive** — sent when a Finding is marked a false positive. +- **Transition to Risk Accepted** — sent when a Finding is risk accepted. + +For example, to satisfy a mandatory Resolution code: + +| Source | Field Name | Value | Apply On | +|---|---|---|---| +| Static | `close_code` | `Resolved by DefectDojo` | Transition to Closed | +| Static | `close_notes` | `Reviewed by the security team` | Transition to Closed | +| Static | `close_code` | `Not a defect` | Transition to False Positive | + +Notes: + +- Field Name is the ServiceNow column name — `close_code`, `close_notes`, or a custom `u_...` field. +- Transition mappings fire when the record's state actually changes: a Finding that is already closed when first pushed, an update that closes or reopens the record, and the forced close when a ticket link is deleted. They are not re-sent on routine updates of an unchanged record, so journal fields such as `work_notes` receive one entry per transition. +- Reference fields such as `assignment_group` and `assigned_to` expect a **sys_id**, not a display name. +- Values that parse as JSON are sent typed: `true`, `42`, `[...]`, `{...}` — and `null`, which clears the field. To send such text as a literal string, wrap it in double quotes (e.g. `"null"`). +- `short_description`, `description`, `state`, `impact`, `urgency`, and `priority` are owned by the description template and the severity/status mappings, so they cannot be set through a custom field mapping. +- On tables other than `incident`, state values that match the standard Incident set (`1`, `2`, `3`, `6`, `7`, `8`) are still interpreted with Incident semantics — including the automatic Resolution code default on `6`/`7`/`8`. Prefer state values outside that range on custom tables, or supply the close fields explicitly as above. + ## Shortcut The Shortcut integration allows you to push DefectDojo Findings as [Shortcut](https://www.shortcut.com/) Stories. Stories are created with the story type of Bug and assigned to a Team in your Shortcut workspace. diff --git a/docs/content/releases/os_upgrading/3.1.md b/docs/content/releases/os_upgrading/3.1.md index 2a672f0eb2d..e8b213a18c7 100644 --- a/docs/content/releases/os_upgrading/3.1.md +++ b/docs/content/releases/os_upgrading/3.1.md @@ -2,7 +2,7 @@ title: 'Upgrading to DefectDojo Version 3.1.x' toc_hide: true weight: -20260617 -description: Blank Finding components are now normalized to NULL so component-less findings group together; JIRA project configurations now support multiple comma-separated components; Tool Configuration credentials are re-encrypted to AES-256-GCM; the JFrog Xray API Summary Artifact parser now sorts impact paths so findings deduplicate consistently across re-imports. +description: Blank Finding components are now normalized to NULL so component-less findings group together; JIRA project configurations now support multiple comma-separated components; Tool Configuration credentials are re-encrypted to AES-256-GCM; the JFrog Xray API Summary Artifact parser now sorts impact paths so findings deduplicate consistently across re-imports; a new optional DD_OS_MESSAGE_ENABLED setting controls the open-source promo banner; and a new deduplication execution mode controls how import/reimport deduplication is dispatched. --- ## Blank Finding components normalized to NULL @@ -47,4 +47,46 @@ The parser now sorts the impact paths before selecting the first one, so `file_p Nothing is required for new imports. Findings imported before this release may have been created with a non-deterministic first impact path; re-importing an affected report will now deduplicate consistently, and you may want to clean up any duplicate findings that a previous import created. +## New setting: `DD_OS_MESSAGE_ENABLED` + +This release adds the `DD_OS_MESSAGE_ENABLED` setting (default `True`), which controls the open-source promotional ("Upgrade to Pro") banner. The default preserves the existing behavior. Set `DD_OS_MESSAGE_ENABLED=False` to hide the banner; when disabled, DefectDojo skips the outbound request that fetches the message. + +## Deduplication execution mode for import/reimport + +This release adds a new `deduplication_execution_mode` setting that controls how +import/reimport deduplication post-processing is dispatched and whether the API +response waits for it. It can be set per user (profile) and overridden per request +on the import and reimport endpoints. + +Modes: + +- `async` (default): deduplication and the rest of post-processing are dispatched + to the background and the response returns immediately. This is the historical + behavior; nothing changes for existing users. +- `async_wait`: post-processing is still dispatched to the background, but the + request waits for deduplication to finish before responding. As a result the + `scan_added` notification and the statistics in the import/reimport response + reflect the deduplicated state (findings that turned out to be duplicates are + no longer counted/listed as new). JIRA push, product grading and other + non-deduplication tasks remain asynchronous and are not awaited. +- `sync`: import deduplication runs inline in the web request. + +The wait in `async_wait` is bounded by the new `DD_DEDUPLICATION_ASYNC_WAIT_TIMEOUT` +environment variable (default `60` seconds). If no worker picks up the work within +the timeout, the request responds anyway (degrading to the `async` outcome) rather +than hanging. + +The import/reimport response now also includes a `deduplication_complete` boolean +indicating whether deduplication had finished by the time the response was produced. + +### Relationship to `block_execution` + +The existing `block_execution` profile flag is unchanged. It remains the global +switch that forces **all** of a user's asynchronous tasks (notifications, JIRA +push, product grading, deduplication, ...) to run in the foreground. +`deduplication_execution_mode` is independent and narrower — it only affects +import/reimport deduplication post-processing. A user who has `block_execution` +enabled continues to get fully synchronous imports; the upgrade migration seeds +their `deduplication_execution_mode` to `sync` so behavior is unchanged. + For more information, check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/3.1.0). diff --git a/docs/content/releases/pro/changelog.md b/docs/content/releases/pro/changelog.md index 7f02ab71cc3..f797f9aaf6e 100644 --- a/docs/content/releases/pro/changelog.md +++ b/docs/content/releases/pro/changelog.md @@ -12,6 +12,30 @@ For Open Source release notes, please see the [Releases page on GitHub](https:// ## July 2026: v3.1 +### July 22, 2026: v3.1.202 + +* **(Connectors)** Registered the Intigriti bug bounty connector and the runZero asset connector in the Pro UI. The Qualys connector now sizes its request timeout for large detection exports. +* **(Integrations)** Integrator assignments now support per-assignment push filters, so you can limit what gets pushed by minimum severity and active-only status. +* **(Sensei)** Added a cloud dispatch guard, retroactive re-staging of auto-fixes, and a per-row actions menu. Fixed the "Configure Product" button clipping in the findings list. +* **(Findings)** Request Review is now gated on `Finding_View` instead of `Finding_Edit`. +* **(Pro UI)** Export options now prefill from the active table preference, the AI menu was flattened into top-level Sensei, Model Settings, and MCP links, and the PSIRT menu link now opens in a new tab. +* **(Import)** Scan-import cleanup now streams files and fails loudly on error. +* **(Performance)** Dashboard count tiles no longer time out on large finding buckets. +* **(Bug Fixes)** Cleared default ordering in count subqueries + +### July 20, 2026: v3.1.200 + +* **(Connectors)** Added another large batch of Connectors. New findings connectors: Rapid7 InsightAppSec, Cobalt.io PtaaS, Sonatype IQ (Nexus Lifecycle), Acunetix 360, Mend (WhiteSource), Bugcrowd, Black Duck, Edgescan, Sysdig Secure, Coverity Connect, Harbor, OpenVAS / Greenbone, Nuclei / ProjectDiscovery Cloud, Endor Labs, Prowler, Kubescape / ARMO, Quay + Clair, Intruder.io, and YesWeHack. Added a ServiceNow CMDB asset connector. You can now request a new connector directly from the cloud UI, and CrowdStrike Spotlight now derives its severity floor from structured sync filters. +* **(Integrations)** Added a Linear integrator for pushing findings to Linear. +* **(Feature Flags)** Redesigned feature flags into a two-tier, metadata-driven system with a dedicated Feature Flags admin page. +* **(Findings)** Similar Findings now only surfaces genuinely similar findings, the CVSS and EPSS columns now expose numeric filter operators, and several broken findings-table column filters were fixed. +* **(UI)** Metric colors in the Vue UI are now configurable per instance. +* **(SSO)** Added a configurable OIDC username claim and hardened SSO user creation. +* **(Performance)** Authorized-finding queries now filter by a literal product-id list, and the paginated count-cache refill is now single-flighted to avoid redundant recounts on busy instances. +* **(Import)** The generic parser no longer produces a nested list when a finding has both a CVE and vulnerability IDs, and the Import/ReImport forms no longer touch the database at import time. +* **(Settings)** Added `DD_EDITABLE_MITIGATED_DATA` to control whether mitigation data is editable, and ignored close-finding fields are now hidden. +* **(Bug Fixes)** Connector backend config refresh now encodes datetimes correctly + ### July 15, 2026: v3.1.101 * **(Findings)** Consolidated the bulk-edit actions in the findings table into a single surface, and added bulk "replace tag" and bulk review actions. @@ -322,7 +346,7 @@ No significant UX changes. #### Dec 8, 2025: v2.53.1 -* **(Assets/Organizations)** Introduced overhaul to Products/Product Types, added the ability to create and diagram relationships between Assets. See [Assets/Organizations documentation](/asset_modelling/pro_hierarchy/assets_organizations/) for details, and information on opting in to the Beta. +* **(Assets/Organizations)** Introduced overhaul to Products/Product Types, added the ability to create and diagram relationships between Assets. See [Assets/Organizations documentation](/asset_modelling/pro_hierarchy/asset_hierarchy/) for details, and information on opting in to the Beta. * **(Findings)** Added new KEV fields for ransomware, exploits, and date handling. * **(Pro UI)** Added Table Preferences menu, allowing you to store preset lists of columns for each table. diff --git a/docs/content/sensei/OS__sensei.md b/docs/content/sensei/OS__sensei.md new file mode 100644 index 00000000000..f0960382290 --- /dev/null +++ b/docs/content/sensei/OS__sensei.md @@ -0,0 +1,27 @@ +--- +title: "Sensei" +description: "Sensei is a DefectDojo Pro feature" +draft: false +audience: opensource +weight: 1 +--- +Note: Sensei is a DefectDojo Pro-only feature. + +**Sensei**, DefectDojo's AI-powered **scan-and-fix** for source code repositories, is available only in **DefectDojo Pro**. It is not part of the open-source edition. + +With Sensei, DefectDojo Pro can: + +- Scan your GitHub repositories and import the results as findings. +- Open pull requests that **remediate** those findings with a large language model, using a preview-first approval workflow (nothing runs until you approve). +- Start a fix from the findings table, from staged auto-fix candidates, or with a `/fix` comment on a pull request. + +## See the full Sensei documentation + +The complete Sensei guide is part of the **Pro** documentation. Switch the documentation version toggle (top of the left navigation) to **Pro** to read it: + +- **About Sensei:** what it is and how hosted scan-and-fix works +- **Set Up Sensei:** connect a GitHub App and onboard a repository +- **Fixing Findings with Sensei:** scan, triage candidates, and open fix PRs +- **Sensei Reference:** statuses, quotas, and troubleshooting + +To learn more about DefectDojo Pro, visit [defectdojo.com](https://www.defectdojo.com/) or contact your DefectDojo account team. diff --git a/docs/content/sensei/_index.md b/docs/content/sensei/_index.md new file mode 100644 index 00000000000..585a746bb8a --- /dev/null +++ b/docs/content/sensei/_index.md @@ -0,0 +1,19 @@ +--- +title: "Sensei" +description: "AI-powered scan-and-fix for your source code repositories" +summary: "" +date: 2023-09-07T16:06:50+02:00 +lastmod: 2023-09-07T16:06:50+02:00 +draft: false +weight: 5 +chapter: true +sidebar: + collapsed: true +seo: + title: "" # custom title (optional) + description: "" # custom description (recommended) + canonical: "" # custom canonical URL (optional) + robots: "" # custom robot tags (optional) +audience: pro +exclude_search: true +--- diff --git a/docs/content/sensei/about_sensei.md b/docs/content/sensei/about_sensei.md new file mode 100644 index 00000000000..f08ca75a280 --- /dev/null +++ b/docs/content/sensei/about_sensei.md @@ -0,0 +1,58 @@ +--- +title: "About Sensei" +description: "What Sensei is and how DefectDojo-hosted scan-and-fix works" +draft: false +audience: pro +weight: 1 +--- +Note: Sensei is a DefectDojo Pro-only feature and is currently in BETA. + +**Sensei** is DefectDojo's AI-powered **scan-and-fix** capability for source code repositories. Connect a repository (through a **GitHub App**, **GitLab**, **Bitbucket**, or **Azure DevOps**) and Sensei scans it, imports the results as DefectDojo findings, and then uses a large language model to **remediate those findings by opening pull/merge requests**, all without leaving DefectDojo. + +> **🔀 Multiple providers:** Sensei supports **GitHub** (github.com and GitHub Enterprise Server), **GitLab** (gitlab.com and self-managed), **Bitbucket** (Cloud and Server/Data Center), and **Azure DevOps**, all with the same scan-and-fix flow. Where this guide says *pull request*, GitLab uses a **merge request**; the PR *status check* is posted as a GitLab/Azure **commit status** or a Bitbucket **build status**. Connection differs by provider (see [Set up Sensei](/sensei/setup_sensei/)); everything after onboarding is identical. + +- **Scan-and-fix in one place:** repositories are scanned and remediated from the Sensei page and from your findings, using the same normalized, deduplicated finding data as the rest of DefectDojo. +- **Preview-first:** Sensei stages fix *candidates* for review. Nothing is sent to an LLM and no pull request is opened until you approve, so there is no surprise cost or unexpected PR. +- **Short-lived credentials:** Sensei runs entirely through a GitHub App and uses short-lived installation tokens. There is nothing to paste and nothing to rotate. +- **Metered and license-gated:** Sensei is a Pro feature with per-instance quotas for fixes and onboarded repositories. + +> **🔎 BETA:** Sensei is under active development and is labeled **BETA** throughout the UI. Behavior and screens may change between releases. + +> **📍 Where to find it:** open **Sensei** from the left-hand navigation. + +![Sensei hub](images/hub_overview.png) + +## How DefectDojo-hosted scanning works + +DefectDojo-hosted scanning is the recommended way to run Sensei. Scans run **inside DefectDojo**, and nothing is added to your repository: + +1. **Connect a GitHub App** and install it on the organization (or account) that owns your repositories. +2. **Onboard a repository** for hosted scanning and choose how findings are reported and (optionally) auto-fixed. +3. **Sensei scans the repository** (on demand, or automatically when a pull request is opened) and imports the results into an engagement named after the branch. +4. **Sensei remediates findings** by generating a fix and opening a pull request against the repository's default branch. + +Each onboarded repository is linked to a DefectDojo **asset** (product), so its findings, engagements, and fixes live alongside the rest of your data. + +## The three ways a fix gets started + +Sensei can remediate a finding in three ways: + +- **The Fix button on a finding:** trigger a one-off fix directly from the findings table or a finding's detail page. See [Fixing findings with Sensei](/sensei/fixing_findings/). +- **Auto-fix candidates:** after each scan, Sensei stages the findings that match your criteria as candidates. You review them and approve the ones to fix (or let Sensei remediate them automatically). See [Auto-fix candidates](/sensei/fixing_findings/#auto-fix-candidate-triage). +- **A `/fix` comment on a pull request:** comment `/fix` on a pull request and Sensei pushes a remediation to that PR. + +## Requirements + +- A **DefectDojo Pro** license that includes the **Sensei** feature. +- A connected source-control provider (see [Set up Sensei](/sensei/setup_sensei/)): a **GitHub App** (github.com or Enterprise Server), a **GitLab** project/group access token (gitlab.com or self-managed), a **Bitbucket** connection (Cloud or Server/Data Center — OAuth, API token, or access token), or an **Azure DevOps** Personal Access Token. +- To **configure** Sensei (connect apps, onboard repositories): a global **Maintainer** or **Owner** role. +- To **trigger a fix** on a finding: at least **Writer** access to that finding's product. + +## Quotas + +Sensei is metered against your license. The Sensei hub shows two usage meters at the top of the page: + +- **Fixes:** the number of remediations applied against your prepaid limit. Approving a candidate or triggering a fix consumes from this quota. +- **Onboarded Repositories:** the number of repositories onboarded against your repository limit. + +When a quota is reached, Sensei blocks further fixes (or onboarding) until it is raised. See [Reference](/sensei/sensei_reference/#quotas-and-metering) for details. diff --git a/docs/content/sensei/fixing_findings.md b/docs/content/sensei/fixing_findings.md new file mode 100644 index 00000000000..cec5eb58917 --- /dev/null +++ b/docs/content/sensei/fixing_findings.md @@ -0,0 +1,70 @@ +--- +title: "Fixing Findings with Sensei" +description: "Scan, triage auto-fix candidates, and open fix pull requests" +draft: false +audience: pro +weight: 3 +--- +Note: Sensei is a DefectDojo Pro-only feature and is currently in BETA. + +Once a repository is onboarded, Sensei surfaces directly on your findings and on the Sensei hub. This page covers scanning a repository, triaging auto-fix candidates, and remediating individual findings. You need at least **Writer** access to a finding's product to trigger a fix. + +## Scan a repository + +Scans import findings into an engagement named after the branch. You can trigger a scan on demand from the Sensei hub: open a repository's row actions and choose **Scan now**. + +![Scan with Sensei dialog](images/scan_dialog.png) + +Pick the branch to scan (it defaults to the repository's default branch) and choose **Start scan**. In DefectDojo-hosted mode, scans also run automatically when a pull request is opened. + +## The Sensei column on findings + +Onboarded repositories add a **Sensei** column to the findings table. Each finding shows a **Fix** button (or its current fix status), so you can remediate without leaving your triage view. + +![Sensei column on the findings table](images/findings_sensei_column.png) + +The button has two states: + +- **Fix:** the finding's product is onboarded to Sensei. Clicking it starts a remediation. +- **Configure Product:** the finding's product is **not** onboarded yet. Clicking it takes you to Sensei to onboard a repository for that product; once onboarded, the button becomes **Fix**. + +## Fix a single finding + +Clicking **Fix** (on the findings table or in a finding's detail header) opens the **Fix with Sensei** dialog. Choose the base branch the fix pull request should target, then click **Fix**. + +![Fix with Sensei dialog](images/fix_with_sensei_dialog.png) + +Sensei generates a remediation and opens a pull request. The finding's fix status is shown as a badge that moves through *in progress* → *PR open* (or *failed*). Once the pull request is open, the badge links straight to it. + +![Finding detail with fix status badge](images/finding_detail_fix.png) + +> **💡 One fix, one PR:** each approved fix consumes one fix from your quota and opens one pull request. Review and merge the PR in GitHub as you would any other. + +## Auto-fix candidate triage + +When a repository has automated fixes enabled, each scan stages matching findings as **candidates** on the **Auto-fix Candidates** tab of the Sensei hub. This is Sensei's preview-first model: findings are staged, but **nothing runs (no LLM cost) until you approve**. Approving opens fix pull requests and consumes fixes. + +![Auto-fix candidate triage](images/auto_fix_candidates.png) + +Each candidate shows the finding, its status, severity, risk, priority, target repository, and PR branch. To remediate: + +- **Approve one:** click **Approve** on a row to open the branch picker and start that fix. +- **Approve several:** select multiple rows and use the bulk approve action. + +Approved findings stay listed as **In Progress** (or **Failed**) until their pull request is attached, so an in-flight or failed fix never disappears before it produces a PR. + +> **🔎 Hands-off remediation:** if you enabled *Automatically remediate candidates* on the repository, a background check opens fix PRs for staged candidates automatically, up to your fix quota, without manual approval. + +## Track scans and impact + +Two places on the Sensei hub help you follow what Sensei has done: + +- **Scan Activity:** a ledger of every scan and fix run, with its mode (Branch Scan, PR Scan, Fix (Finding)), trigger (Manual, Webhook, Auto Remediated), status, execution time, and links to the engagement or the pull request it produced. + + ![Scan Activity ledger](images/scan_activity.png) + +- **Fix Impact:** a summary of fixes applied, with the assets fixed most often, at the top of the hub. + + ![Fix Impact panel](images/fix_impact.png) + +Use the **Scan now**, **Scan history**, **Configure**, and **Re-stage candidates** row actions to manage each onboarded repository over time (see [Reference](/sensei/sensei_reference/#repository-row-actions)). diff --git a/docs/content/sensei/images/auto_fix_candidates.png b/docs/content/sensei/images/auto_fix_candidates.png new file mode 100644 index 00000000000..94301c12ae6 Binary files /dev/null and b/docs/content/sensei/images/auto_fix_candidates.png differ diff --git a/docs/content/sensei/images/connections.png b/docs/content/sensei/images/connections.png new file mode 100644 index 00000000000..27d64e11dd1 Binary files /dev/null and b/docs/content/sensei/images/connections.png differ diff --git a/docs/content/sensei/images/finding_detail_fix.png b/docs/content/sensei/images/finding_detail_fix.png new file mode 100644 index 00000000000..c59f0051854 Binary files /dev/null and b/docs/content/sensei/images/finding_detail_fix.png differ diff --git a/docs/content/sensei/images/findings_sensei_column.png b/docs/content/sensei/images/findings_sensei_column.png new file mode 100644 index 00000000000..f02245e524d Binary files /dev/null and b/docs/content/sensei/images/findings_sensei_column.png differ diff --git a/docs/content/sensei/images/fix_impact.png b/docs/content/sensei/images/fix_impact.png new file mode 100644 index 00000000000..017989be9e9 Binary files /dev/null and b/docs/content/sensei/images/fix_impact.png differ diff --git a/docs/content/sensei/images/fix_with_sensei_dialog.png b/docs/content/sensei/images/fix_with_sensei_dialog.png new file mode 100644 index 00000000000..85a23d59802 Binary files /dev/null and b/docs/content/sensei/images/fix_with_sensei_dialog.png differ diff --git a/docs/content/sensei/images/github_create_app.png b/docs/content/sensei/images/github_create_app.png new file mode 100644 index 00000000000..44f145b4133 Binary files /dev/null and b/docs/content/sensei/images/github_create_app.png differ diff --git a/docs/content/sensei/images/github_install_app.png b/docs/content/sensei/images/github_install_app.png new file mode 100644 index 00000000000..95ab746a351 Binary files /dev/null and b/docs/content/sensei/images/github_install_app.png differ diff --git a/docs/content/sensei/images/hub_overview.png b/docs/content/sensei/images/hub_overview.png new file mode 100644 index 00000000000..f088234c2c9 Binary files /dev/null and b/docs/content/sensei/images/hub_overview.png differ diff --git a/docs/content/sensei/images/repo_config.png b/docs/content/sensei/images/repo_config.png new file mode 100644 index 00000000000..6095c4263f8 Binary files /dev/null and b/docs/content/sensei/images/repo_config.png differ diff --git a/docs/content/sensei/images/repo_config_advanced.png b/docs/content/sensei/images/repo_config_advanced.png new file mode 100644 index 00000000000..9f829e979b3 Binary files /dev/null and b/docs/content/sensei/images/repo_config_advanced.png differ diff --git a/docs/content/sensei/images/repo_row_menu.png b/docs/content/sensei/images/repo_row_menu.png new file mode 100644 index 00000000000..42c0339cc05 Binary files /dev/null and b/docs/content/sensei/images/repo_row_menu.png differ diff --git a/docs/content/sensei/images/scan_activity.png b/docs/content/sensei/images/scan_activity.png new file mode 100644 index 00000000000..2493a865a3b Binary files /dev/null and b/docs/content/sensei/images/scan_activity.png differ diff --git a/docs/content/sensei/images/scan_dialog.png b/docs/content/sensei/images/scan_dialog.png new file mode 100644 index 00000000000..7a7ac70d9a5 Binary files /dev/null and b/docs/content/sensei/images/scan_dialog.png differ diff --git a/docs/content/sensei/images/setup_create_app.png b/docs/content/sensei/images/setup_create_app.png new file mode 100644 index 00000000000..59d32391e86 Binary files /dev/null and b/docs/content/sensei/images/setup_create_app.png differ diff --git a/docs/content/sensei/images/setup_install_app.png b/docs/content/sensei/images/setup_install_app.png new file mode 100644 index 00000000000..fe947c4a171 Binary files /dev/null and b/docs/content/sensei/images/setup_install_app.png differ diff --git a/docs/content/sensei/images/setup_providers.png b/docs/content/sensei/images/setup_providers.png new file mode 100644 index 00000000000..f028ae23466 Binary files /dev/null and b/docs/content/sensei/images/setup_providers.png differ diff --git a/docs/content/sensei/images/setup_repo_picker.png b/docs/content/sensei/images/setup_repo_picker.png new file mode 100644 index 00000000000..c6fd4dad7cb Binary files /dev/null and b/docs/content/sensei/images/setup_repo_picker.png differ diff --git a/docs/content/sensei/sensei_reference.md b/docs/content/sensei/sensei_reference.md new file mode 100644 index 00000000000..55af7a1df64 --- /dev/null +++ b/docs/content/sensei/sensei_reference.md @@ -0,0 +1,103 @@ +--- +title: "Sensei Reference" +description: "Statuses, row actions, quotas, and troubleshooting" +draft: false +audience: pro +weight: 4 +--- +Note: Sensei is a DefectDojo Pro-only feature and is currently in BETA. + +A quick reference for the statuses, actions, and limits you'll encounter while using Sensei. + +## Repository statuses + +The status shown for an onboarded repository on the Sensei hub: + +| Status | Meaning | +|--------|---------| +| **Active** | Onboarded and ready to scan. | +| **Pull Request Open** | Sensei has an open pull request against the repository. | +| **Pull Request Closed** | A Sensei pull request was closed. | +| **Error** | The last operation failed: check Scan Activity for the root cause. | +| **Not Configured** | The repository is connected but not yet configured. | + +## Candidate and fix statuses + +Auto-fix candidates and fix records move through these states: + +| Status | Meaning | +|--------|---------| +| **Candidate** | Staged by a scan's auto-fix criteria. Nothing runs until you approve. | +| **In Progress** | Approved: Sensei is generating the fix and will open a pull request. | +| **PR Open** | A fix pull request is open; the badge links to it. | +| **Failed** | The fix could not be completed; it stays listed so it doesn't disappear silently. | + +## Repository row actions + +Each onboarded repository has a row-actions menu on the Sensei hub: + +![Repository row actions](images/repo_row_menu.png) + +- **Scan now:** start an on-demand scan (opens the branch picker). +- **Scan history:** view this repository's past scans. +- **Configure:** reopen the configuration form (PR reporting, automated fixes, product linkage). +- **Re-stage candidates:** re-evaluate the repository's findings against the auto-fix criteria and stage fresh candidates. +- **Delete:** remove the repository from Sensei. This stops scanning it; it does not delete the underlying asset or findings. + +## Quotas and metering + +Sensei is metered against your DefectDojo Pro license, shown as meters at the top of the hub: + +- **Fixes:** remediations applied against your prepaid limit. Approving a candidate or triggering a fix consumes from this quota; when it is exhausted, further fixes are blocked (a warning banner appears) until the limit is raised. +- **Onboarded Repositories:** repositories onboarded against your repository limit. When it is reached, onboarding new repositories is blocked. + +To raise a limit, contact your DefectDojo account team. + +## GitLab specifics + +GitLab is supported alongside GitHub (gitlab.com and self-managed). The scan-and-fix behavior is identical; these are the GitLab-specific details: + +- **Connection:** a **project or group access token** (role **Developer**, or **Maintainer** if push rules require it) with the **`api`** and **`write_repository`** scopes, not a GitHub App. See [Set up Sensei](/sensei/setup_sensei/#connect-gitlab). +- **Webhook:** each onboarded project needs a webhook to `…/sensei/gitlab/webhooks` (with the connection's secret) subscribed to **Push**, **Merge request**, and **Comment** events. Adding a webhook requires **Maintainer**/**Owner** on the project. +- **Merge requests, not pull requests:** fixes open a **merge request** against the default branch; the `/fix` comment works on merge-request notes. +- **Commit-status gate:** the PR status check is a GitLab **commit status** on the merge request's head commit: `running` while scanning, then `success` or `failed` (fail-on-new). GitLab has no *neutral* state, so a **non-gating** scan that still has findings shows a **green** status; the summary note carries the finding details. +- **Self-managed:** point the **GitLab Base URL** at your instance; DefectDojo clones and calls the API against that host. + +## Bitbucket specifics + +Bitbucket **Cloud** and **Server/Data Center** are supported. The scan-and-fix behavior is identical; these are the Bitbucket-specific details: + +- **Connection:** **OAuth** (recommended), an Atlassian **API token** (used with your account email), or a repository/workspace **access token**. See [Set up Sensei](/sensei/setup_sensei/#connect-bitbucket). App passwords are deprecated and not supported. +- **Workspace scoping (Cloud):** API/access tokens are workspace-bound, so a **workspace** is required for Cloud; OAuth is user-context and discovers accessible workspaces automatically. +- **Webhook:** each onboarded repository needs a webhook to `…/sensei/bitbucket/webhooks` (with the connection's secret, verified via HMAC-SHA256 `X-Hub-Signature`) subscribed to **Push**, **Pull request** (created/updated/merged/declined), and **Pull request comment** events. +- **Build-status gate:** the PR status check is posted as a Bitbucket **build status** on the head commit (`INPROGRESS` → `SUCCESSFUL`/`FAILED`). Bitbucket has no *neutral* state, so a non-gating scan maps to `SUCCESSFUL` and the summary comment carries the detail. The build-status link must be a public URL, so it uses your DefectDojo host. +- **Repository names:** `workspace/repo` (Cloud) or `PROJECTKEY/repo` (Server/Data Center). +- **Server/Data Center:** set the **Base URL** to your host; DefectDojo uses the v1.0 REST API and `/scm/…` git paths. + +## Azure DevOps specifics + +Azure DevOps Repos are supported via a **Personal Access Token**. The scan-and-fix behavior is identical; these are the Azure-specific details: + +- **Connection:** a **PAT** with the **Code (Read, Write, & Manage)** scope, plus the **organization**. Azure DevOps OAuth apps are being retired, so a PAT is the recommended credential. See [Set up Sensei](/sensei/setup_sensei/#connect-azure-devops). +- **Webhook:** Azure **Service Hooks** authenticate with HTTP **Basic** (not an HMAC) and use **one subscription per event**. Create subscriptions to `…/sensei/azure/webhooks` for **Code pushed** and **Pull request created/updated/merged**, with the connection's Basic username/password. +- **Commit-status gate:** the PR status check is posted as a Git **commit status** on the head commit. +- **Repository names:** `project/repo` (the organization is stored on the connection). +- **Azure DevOps Server:** set the **Base URL** to your on-prem collection URL. + +## GitHub Enterprise Server specifics + +GitHub Enterprise Server uses the **same GitHub App** model as github.com; only the host differs: + +- **Connection:** because the App-manifest auto-create flow is github.com-only, create the App **manually** on your GHES host and enter its credentials plus the **Enterprise host** via **Set up manually**. See [Connect GitHub Enterprise Server](/sensei/setup_sensei/#connect-github-enterprise-server). DefectDojo derives the API (`/api/v3`) and web origins from the host. +- **Coexistence:** a github.com App connection and a GHES App connection can be configured on the same instance; each repository resolves to the connection it was onboarded through. +- **Reachability:** DefectDojo must reach the GHES API host, and GHES must reach DefectDojo's `…/sensei/webhooks` endpoint (internal hosts are fine if both sides can connect). + +## Troubleshooting + +- **The Sensei button on a finding says "Configure Product."** The finding's product isn't onboarded. Click it to onboard a repository for that product, then return to the finding. +- **A fix shows "Failed" in Auto-fix Candidates or Scan Activity.** Open **Scan Activity** and check the **Root Cause** / **Details** for that run. Failed fixes remain listed so they don't disappear before producing a PR; you can re-stage and retry. +- **A repository isn't listed when onboarding.** Only repositories the connection can access are shown. On **GitHub**, confirm the App is installed on the correct organization and its repository access includes the repository. On **GitLab**, confirm the access token's scope covers the project. On **Bitbucket Cloud**, confirm the **workspace** is set (tokens are workspace-scoped). On **Azure DevOps**, confirm the PAT's organization matches and its **Code** scope is granted. +- **Scans or fixes never start after a webhook.** Confirm the repository's webhook points at the provider's receiver (`…/sensei/{gitlab,bitbucket,azure}/webhooks`, or `…/sensei/webhooks` for GitHub) with the correct secret/credentials, and subscribes to push + pull-request (+ comment) events. The provider's **recent deliveries** should show `HTTP 200`. Webhook-driven runs fire only for repositories onboarded in **hosted** mode; a push to a non-default branch is scanned via its pull request, not on its own. +- **Nothing is happening after a scan.** Check that automated fixes are enabled (and your severity/risk thresholds match findings) on the repository's configuration, and that your **Fixes** quota isn't exhausted. + +> **🔎 Still in BETA:** Sensei is evolving quickly. If behavior doesn't match this guide, check the [Pro changelog](/releases/pro/changelog/) for recent changes. diff --git a/docs/content/sensei/setup_sensei.md b/docs/content/sensei/setup_sensei.md new file mode 100644 index 00000000000..5b4898dd9a8 --- /dev/null +++ b/docs/content/sensei/setup_sensei.md @@ -0,0 +1,237 @@ +--- +title: "Set Up Sensei" +description: "Connect GitHub, GitLab, Bitbucket, or Azure DevOps, and onboard a repository for hosted scanning" +draft: false +audience: pro +weight: 2 +--- +Note: Sensei is a DefectDojo Pro-only feature and is currently in BETA. + +Setting up Sensei has two parts: **connect a source-control provider**, then **onboard the repositories** you want to scan. You need a global **Maintainer** or **Owner** role to do this. Sensei supports: + +- **GitHub** — a GitHub App (github.com or **GitHub Enterprise Server**). +- **GitLab** — an access token (gitlab.com or self-managed). +- **Bitbucket** — Cloud or Server/Data Center, via OAuth (recommended), an Atlassian API token, or an access token. +- **Azure DevOps** — a Personal Access Token. + +Onboarding, configuration, scanning, and fixing are the same for every provider; only the initial connection differs. This page covers [connecting a GitHub App](#connect-a-github-app), [GitHub Enterprise Server](#connect-github-enterprise-server), [GitLab](#connect-gitlab), [Bitbucket](#connect-bitbucket), and [Azure DevOps](#connect-azure-devops); the [Select repositories](#select-repositories) step onward is shared. + +## Connections + +A **connection** is one configured source-control identity — a GitHub App installation group, a GitLab token, a Bitbucket workspace, or an Azure DevOps organization. You onboard repositories from a connection, and manage or disconnect it, from the **Connections** page (the **Connections** button on the Sensei hub). + +![Sensei Connections](images/connections.png) + +The table lists each connection's label, identity, number of onboarded repos, creation date, and provider. Use the row actions (the menu on the left of each row) to manage the connection on its provider, add repositories from that connection, or disconnect it. + +> **⚠️ Disconnecting is destructive:** disconnecting a connection removes it **and every repository onboarded through it**. This cannot be undone. + +## Choose a source-control provider + +From the Sensei hub, choose **Add Repositories** (or **Connect** on the Connections page) to open **Set Up Sensei**, then pick your source-control provider — **GitHub** (including GitHub Enterprise Server), **GitLab**, **Bitbucket**, or **Azure DevOps**. Each provider's connect flow is described below. + +![Choose a source-control provider](images/setup_providers.png) + +## Connect a GitHub App + +Sensei runs entirely through a GitHub App. Install it on your org/account and DefectDojo uses short-lived tokens to open PRs, scan, and apply fixes. Nothing to paste, nothing to rotate. + +From the Sensei hub, choose **Add Repositories** (or **Connect** on the Connections page) to open **Set Up Sensei**. + +### Step 1: Create the App + +Enter the **organization** that owns the repositories you want to scan (leave blank to create the App on your personal account), then click **Create GitHub App**. GitHub pre-fills the app name, URLs, and permissions; you just review and confirm. + +![Create the GitHub App](images/setup_create_app.png) + +GitHub opens a confirmation page. Click **Create GitHub App for ``** to register the app under that organization. + +![Confirm app creation on GitHub](images/github_create_app.png) + +> **🔑 Tip:** Create the App on the same organization that owns the repositories you plan to scan. The App owner is set at creation time. + +### Step 2: Install the App + +Back in DefectDojo, the app shows as *configured*. Click **Install on GitHub** to install it on your organization. + +![App created, install it](images/setup_install_app.png) + +On GitHub, confirm the installation location (your organization), choose **All repositories** or **Only select repositories**, and review the requested permissions. Sensei needs read access to actions, issues, and metadata, and read/write access to checks, code, pull requests, secrets, and workflows so it can scan and open fix PRs. Click **Install**. + +![Install the App on your organization](images/github_install_app.png) + +## Connect GitLab + +Sensei also supports **GitLab**, both **gitlab.com** and **self-managed** instances. Instead of a GitHub App, GitLab connects with a **project or group access token** plus a webhook; Sensei uses that token to scan, open merge requests, and apply fixes. + +From the Sensei hub, choose **Add Repositories** (or **Connect** on the Connections page) to open **Set Up Sensei**, then select **GitLab** as the source-control provider. + +### Step 1: Create an access token + +In GitLab, open the project (or group) you want to scan and go to **Settings → Access tokens → Add new token**: + +- **Role:** **Developer**, enough to push fix branches and open merge requests. Choose **Maintainer** if the project's push rules require it. +- **Scopes:** **`api`** and **`write_repository`**. + +Create the token and copy the generated `glpat-…` value (GitLab shows it only once). + +> **🔑 Tip:** A **group** access token onboards any project in that group; a **project** access token is scoped to the single project. + +### Step 2: Connect + +Back in **Set Up Sensei** with **GitLab** selected, fill in: + +- **GitLab Base URL:** `https://gitlab.com`, or your self-managed instance URL (for example `https://gitlab.example.com`). +- **Access Token:** the `glpat-…` token from Step 1. +- **Webhook Secret:** leave blank to auto-generate (recommended). You'll add this secret to the webhook in the next step. + +Click **Connect GitLab**. DefectDojo validates the token, stores it encrypted, and can then list projects, open merge requests, and run scans. + +### Step 3: Add the webhook + +So DefectDojo receives push, merge-request, and comment events, add a webhook to **each** GitLab project you plan to onboard (**Settings → Webhooks → Add new webhook**): + +- **URL:** the webhook URL shown on the Set Up Sensei page (`https:///sensei/gitlab/webhooks`). +- **Secret token:** the webhook secret from Step 2. +- **Trigger events:** enable **Push events**, **Merge request events**, and **Comments**. + +Leave SSL verification enabled, click **Add webhook**, then use **Test → Push events** to confirm DefectDojo responds with **HTTP 200**. + +After connecting, click **Choose projects** and continue with [Select repositories](#select-repositories); onboarding, configuration, and scanning work the same as GitHub. + +> **GitLab equivalents:** where this guide says *pull request*, GitLab uses a **merge request**; the pull-request **status check** is posted as a GitLab **commit status** on the merge request's head commit. + +## Connect GitHub Enterprise Server + +Sensei works with **GitHub Enterprise Server (GHES)** using the same GitHub App model as github.com — only the host differs. Because the App-manifest auto-create flow is github.com-only, on GHES you **create the App manually** on your enterprise host and then enter its credentials plus the host in DefectDojo. + +### Step 1: Create the App on your GHES host + +On your GitHub Enterprise Server instance, go to **Settings → Developer settings → GitHub Apps → New GitHub App** and create an App with the same permissions Sensei uses on github.com: read for actions, issues, and metadata, and read/write for checks, code, pull requests, secrets, and workflows. Point its webhook at `https:///sensei/webhooks`. Generate and download a **private key**, and note the **App ID** (and the OAuth **Client ID/Secret** if you set them). + +### Step 2: Connect manually + +In **Set Up Sensei** with **GitHub** selected, click **Set up manually instead** and fill in: + +- **App ID** and **Private Key (PEM)** from Step 1 (plus Client ID/Secret and Webhook Secret if configured). +- **GitHub Enterprise host:** your instance host, for example `https://github.example.com`. DefectDojo derives the API (`/api/v3`) and web origins from it. Leave blank for github.com. + +Click **Save App credentials**. DefectDojo validates them against your enterprise host, then install the App and continue with [Select repositories](#select-repositories). + +> **🔑 Tip:** The host must be reachable from DefectDojo (and DefectDojo reachable from GHES for webhooks). Internal-only hosts are fine as long as both can reach each other on your network. + +## Connect Bitbucket + +Sensei supports **Bitbucket Cloud** (`bitbucket.org`) and **Bitbucket Server / Data Center** (self-hosted). Three non-deprecated auth methods are offered; **OAuth is recommended**. + +From the Sensei hub, choose **Add Repositories** (or **Connect** on the Connections page), then select **Bitbucket** and your **deployment** (Cloud or Server/Data Center) and **authentication** type. + +### Step 1: Create the credential + +**OAuth (recommended)** — in Bitbucket, open **Workspace settings → OAuth consumers → Add consumer**: + +- **Callback URL:** the one shown on the Set Up Sensei page (`https:///sensei/bitbucket/oauth/callback`). +- **Permissions:** **Account: Read**, **Repositories: Read + Write**, **Pull requests: Read + Write** (add **Webhooks: Read + Write** if you'll manage webhooks via the API). + +Save it, then copy the consumer's **Key** (Client ID) and **Secret**. + +**API token** — create an Atlassian **API token** at `id.atlassian.com` (Account settings → Security → API tokens). Use it with your **Atlassian account email**. + +**Access token** — create a repository or workspace **Access Token** in Bitbucket and use it as a bearer credential. + +### Step 2: Connect + +Back in **Set Up Sensei** with **Bitbucket** selected: + +- **OAuth:** paste the **Client ID** and **Client Secret**, then click **Connect with Bitbucket**. Approve the consent screen; DefectDojo stores the resulting tokens encrypted and refreshes them automatically. +- **API token / Access token:** enter your **Workspace** (Cloud), your **email** (API-token auth only), and the **token**. For Server/Data Center, enter your host **Base URL**. + +DefectDojo validates the credential and can then list repositories, open pull requests, and run scans. + +### Step 3: Add the webhook + +Add a webhook to **each** Bitbucket repository (**Repository settings → Webhooks → Add webhook**): + +- **URL:** the webhook URL shown on the Set Up Sensei page (`https:///sensei/bitbucket/webhooks`). +- **Secret:** the webhook secret shown on the page (used for HMAC-SHA256 `X-Hub-Signature` verification). +- **Triggers:** **Repository push**, **Pull request** (created, updated, merged, declined), and **Pull request comment created** (for `/fix` comments). + +After connecting, click **Choose repositories** and continue with [Select repositories](#select-repositories). + +> **Bitbucket specifics:** repositories are addressed as `workspace/repo` (Cloud) or `PROJECTKEY/repo` (Server). The pull-request **status check** is posted as a Bitbucket **build status** on the head commit. OAuth is the recommended method because it is user-context (no workspace/username quirks) and refreshes automatically; app passwords are deprecated and not supported. + +## Connect Azure DevOps + +Sensei supports **Azure DevOps Repos** using a **Personal Access Token (PAT)**. Repositories live in an **organization → project → repository** hierarchy. + +From the Sensei hub, choose **Add Repositories** (or **Connect** on the Connections page), then select **Azure DevOps**. + +### Step 1: Create a PAT + +In Azure DevOps, open **User settings → Personal access tokens → New Token**: + +- **Organization:** the organization whose repositories you want to scan. +- **Scopes:** **Code (Read, Write, & Manage)** — covers cloning, pushing fix branches, and opening pull requests. + +Create the token and copy it (Azure DevOps shows it only once). + +### Step 2: Connect + +Back in **Set Up Sensei** with **Azure DevOps** selected, fill in: + +- **Base URL:** `https://dev.azure.com`, or your Azure DevOps **Server** collection URL. +- **Organization:** your organization name. +- **Personal Access Token:** the token from Step 1. + +Click **Connect**. DefectDojo validates the PAT against `…/_apis/projects`, stores it encrypted, and can then list repositories, open pull requests, and run scans. + +### Step 3: Add the service hook + +Azure DevOps authenticates its **Service Hooks** with HTTP Basic, and uses **one subscription per event type**. In **Project settings → Service hooks → Create subscription → Web Hooks**, create a subscription for each of **Code pushed**, **Pull request created**, **Pull request updated**, and **Pull request merged**, all with: + +- **URL:** the webhook URL shown on the Set Up Sensei page (`https:///sensei/azure/webhooks`). +- **Basic authentication username / password:** the values shown on the page. + +After connecting, click **Choose repositories** and continue with [Select repositories](#select-repositories). + +> **Azure DevOps specifics:** repositories are addressed as `project/repo` (the organization is stored on the connection). The pull-request **status check** is posted as a Git **commit status** on the head commit. + +## Select repositories + +After the App is installed, DefectDojo shows the repositories it can access. Only repositories Sensei has **push access** to are listed; remediation works by pushing a branch and opening a pull request, so repositories without push access are hidden. A pull request is opened against each repository's **default branch**. + +![Select repositories to onboard](images/setup_repo_picker.png) + +Use **Add** to select one or more repositories, then click **Configure N repo(s)**. + +## Configure a repository + +The **Configure Repository** form controls how Sensei scans and reports on the repository. + +![Configure a repository](images/repo_config.png) + +- **Scanning Mode (DefectDojo-hosted):** scans run in DefectDojo. Nothing is added to your repository; trigger scans on demand or automatically via the GitHub App. +- **PR Reporting:** choose what Sensei posts back on pull requests: + - Post a status check on the pull request. + - Fail the check when net-new findings are introduced. + - Post a results summary comment on each commit. + - Auto-create the base-branch baseline on the first PR. +- **Automated Fixes:** enable *Stage matching findings for one-click auto-fix after each scan* to have Sensei stage candidates automatically (see below). + +### Automated fix criteria + +When automated fixes are enabled, findings that meet your criteria are staged as **candidates** on the Sensei page after each scan. Nothing runs (and no LLM cost is incurred) until you approve, unless you enable automatic remediation. + +![Automated fix criteria and advanced options](images/repo_config_advanced.png) + +- **Severity threshold:** findings at or above this severity qualify (choose *Any* to gate on risk only). +- **Risk threshold:** findings at or above this risk level also qualify (combined with severity using OR). +- **Open fix PRs against branch:** the branch auto-fix pull requests target; overridable per fix when you approve individually. +- **Exclude findings tagged:** skip findings carrying the tags you list (e.g. `no-fix`). +- **Automatically remediate candidates:** when enabled, a background check (about every 5 minutes) opens fix pull requests for this repo's staged candidates without waiting for approval, until your fix quota is reached. Leave off to review and approve each candidate yourself. + +Under **Advanced options** you can link the repository to an existing product/asset or create a new one, set the organization, and set a minimum severity below which findings are neither reported nor used in the merge gate. + +## Onboard + +Click **Onboard for hosted scanning**. The repository appears on the Sensei hub with a status of **Active**, ready to scan. From here, continue to [Fixing findings with Sensei](/sensei/fixing_findings/). diff --git a/docs/content/supported_tools/parsers/file/fortify.md b/docs/content/supported_tools/parsers/file/fortify.md index 9fede87689d..794aff2407c 100644 --- a/docs/content/supported_tools/parsers/file/fortify.md +++ b/docs/content/supported_tools/parsers/file/fortify.md @@ -5,6 +5,9 @@ toc_hide: true You can either import the findings in .xml or in .fpr file format.
If you import a .fpr file, the parser will look for the file 'audit.fvdl' and analyze it. An extracted example can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/fortify/audit.fvdl). The optional `audit.xml` is also parsed. All vulnerabilities marked with `suppressed="true"` will be marked as false positive. +### Fortify Scan v2 +The `Fortify Scan v2` scan type behaves identically to `Fortify Scan` except for .fpr imports: findings store the line number Fortify reports for the vulnerability (the FVDL `SourceLocation` line) instead of the first line of the surrounding code snippet, which includes up to 3 leading context lines. Use `Fortify Scan v2` when deduplicating on file path + line number, especially across tools. The two scan types produce different hashcodes for .fpr findings, so keep using `Fortify Scan` on existing products if you want to preserve deduplication history. + ### Sample Scan Data Sample Fortify scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/fortify). diff --git a/docs/content/supported_tools/parsers/file/generic.md b/docs/content/supported_tools/parsers/file/generic.md index 33374a11dc2..a4073fa5259 100644 --- a/docs/content/supported_tools/parsers/file/generic.md +++ b/docs/content/supported_tools/parsers/file/generic.md @@ -68,7 +68,7 @@ The list of supported fields in JSON format: - under_review: Bool - is_mitigated: Bool - thread_id: String -- mitigated: Bool +- mitigated: Date - numerical_severity: Int - param: String - payload: String diff --git a/docs/content/triage_findings/epss_kev.md b/docs/content/triage_findings/epss_kev.md new file mode 100644 index 00000000000..e2a1a31a8d0 --- /dev/null +++ b/docs/content/triage_findings/epss_kev.md @@ -0,0 +1,80 @@ +--- +title: "EPSS / KEV" +description: "How DefectDojo Pro enriches Findings with EPSS and CISA KEV data, when it syncs, and how it drives priority" +audience: pro +weight: 6 +--- + +DefectDojo Pro automatically enriches your Findings with two external threat-intelligence sources — **EPSS** and **CISA KEV** — so that prioritization reflects how likely a vulnerability is to be exploited, not just its CVSS severity. Both sources match to Findings by **CVE**, refresh on a **daily schedule**, and feed directly into each Finding's computed **priority** score. + +On DefectDojo Cloud, enrichment is fully managed: DefectDojo maintains the underlying threat-intelligence data and delivers it to your instance. There is nothing to install, no feed URLs to configure, and no daily job to schedule — it runs for you. + +## The two sources + +### EPSS — Exploit Prediction Scoring System + +[EPSS](https://www.first.org/epss/) is a data-driven model published by FIRST that estimates the probability a given CVE will be exploited in the wild in the next 30 days. DefectDojo Pro stores two EPSS values on each matching Finding: + +| Field | Meaning | +| --- | --- | +| **EPSS Score** | Probability of exploitation in the next 30 days, from `0.0` to `1.0` (e.g. `0.94` = 94%). | +| **EPSS Percentile** | Where this CVE ranks against all scored CVEs, from `0.0` to `1.0` (e.g. `0.99` = in the top 1% most likely to be exploited). | + +When a single Finding carries **multiple CVEs**, DefectDojo keeps the **highest EPSS score** among them and pairs it with that CVE's percentile. + +### KEV — CISA Known Exploited Vulnerabilities + +The [CISA KEV catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) is the U.S. government's authoritative list of vulnerabilities that are confirmed to have been exploited in the wild. Unlike EPSS (a prediction), KEV is a statement of observed, real-world exploitation. DefectDojo Pro stores three KEV values on each matching Finding: + +| Field | Meaning | +| --- | --- | +| **Known Exploited** | `True` when the CVE appears in the CISA KEV catalog. | +| **Ransomware Used** | `True` when CISA notes the CVE has been leveraged in ransomware campaigns. | +| **KEV Date** | The date the vulnerability was added to the KEV catalog. | + +When a Finding carries **multiple CVEs**, it is marked **Known Exploited** if **any** of its CVEs is in the catalog, **Ransomware Used** if any qualifies, and the **KEV Date** is the earliest catalog-add date among them. + +> **Findings without a CVE are not enriched.** Both sources match strictly on CVE identifiers (`CVE-YYYY-NNNNN`). A Finding with no CVE — or with only a vendor-specific or GHSA-style identifier — receives no EPSS or KEV data. + +## When it syncs + +Enrichment runs **once per day, automatically**, against your **entire** Finding population — not just newly imported Findings. On each run, every Finding that has at least one CVE is re-matched against the latest EPSS and KEV data, its enrichment fields are refreshed, and its priority is recomputed. + +A few consequences worth understanding: + +- **Imports are not enriched at import time.** A CVE Finding imported today will show EPSS/KEV values after the next daily enrichment cycle, not the instant it lands. Expect up to roughly a day of lag on brand-new Findings. +- **Values are kept current, not frozen.** Because the whole population is reprocessed daily, EPSS scores drift as the model updates, and a CVE that gets added to the KEV catalog will flip an existing Finding to **Known Exploited** on the next run — no re-import required. +- **KEV removals are respected.** If a Finding's CVEs are no longer KEV-listed, the daily run clears the stale **Known Exploited** / **Ransomware Used** / **KEV Date** values rather than leaving them set. + +## How it impacts priority and risk + +EPSS and KEV are not just informational badges — they are direct inputs to the DefectDojo Pro **prioritization engine**. Each Finding's `priority` score combines several components (severity, exposure, asset context, and more); EPSS and KEV drive the **external score** component, which rewards vulnerabilities that are likely to be — or are known to be — exploited. + +The external score is derived from whichever of the following signals is **strongest**: + +- **EPSS** contributes in proportion to its score — a higher probability of exploitation contributes more. +- **KEV listing** contributes a fixed weight: being **Known Exploited** *or* used in **ransomware** applies a meaningful boost, and a CVE that is **both** Known Exploited **and** used in ransomware applies the largest boost. + +The larger of the two signals wins, so a Finding gets full credit for either a high EPSS score or a KEV listing without being penalized for lacking the other. This external score is then blended into the Finding's overall priority alongside its severity and exposure. The net effect: **a KEV-listed or high-EPSS Finding rises above an otherwise-comparable Finding that has neither**, focusing remediation on what is genuinely most likely to be attacked. + +This flows automatically — priority is recomputed for exactly the Findings updated at the end of each daily enrichment run, so prioritization stays in step with the latest threat intelligence. + +> **Note:** EPSS and KEV influence the **priority** score. They do not change a Finding's **Severity** field or its **SLA** clock, which remain severity- and age-driven. + +## Filtering and viewing enriched Findings + +Once Findings are enriched, the EPSS and KEV values are available throughout the Pro UI: + +- **On the Finding** — EPSS score, EPSS percentile, Known Exploited, Ransomware Used, and KEV Date all display on the Finding detail. +- **Sorting** — Finding tables can be ordered by EPSS score / percentile to surface the most likely-to-be-exploited Findings first. +- **Filtering** — the Findings list offers **Known Exploited** and **Ransomware Used** filters, so you can build views or reports scoped to confirmed real-world-exploited vulnerabilities. + +A common workflow is to filter to **Known Exploited = true**, then sort by priority, to produce a "fix these first" queue backed by confirmed exploitation. + +## Configuration + +On **DefectDojo Cloud**, EPSS and KEV enrichment is enabled and maintained for you — there are no source toggles, feed URLs, or thresholds to set, and the daily sync is managed by DefectDojo. The weightings that translate EPSS and KEV into priority are built into the prioritization engine. + +If EPSS or KEV data is not appearing on Findings you expect it to (and those Findings do carry CVEs), contact DefectDojo support — the enrichment pipeline reports source-level health, and support can confirm whether the daily sync is delivering data to your instance. + +> *On-premise installations* configure enrichment differently — each source can be enabled or disabled and pointed at a custom feed URL under the Tuner's finding-enrichment settings. That configuration does not apply to Cloud, where the data is delivered by DefectDojo. diff --git a/docs/layouts/home.html b/docs/layouts/home.html index 72641e2efda..c639318dc7e 100644 --- a/docs/layouts/home.html +++ b/docs/layouts/home.html @@ -68,7 +68,7 @@

Triage Findings

- +
diff --git a/dojo/__init__.py b/dojo/__init__.py index 6497cb6a512..be940b9ea35 100644 --- a/dojo/__init__.py +++ b/dojo/__init__.py @@ -4,6 +4,6 @@ # Django starts so that shared_task will use this app. from .celery import app as celery_app # noqa: F401 -__version__ = "3.1.200" +__version__ = "3.1.300" __url__ = "https://github.com/DefectDojo/django-DefectDojo" __docs__ = "https://documentation.defectdojo.com" diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index ff60bbf22a6..3f4818f4cd8 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -18,11 +18,13 @@ from rest_framework.exceptions import NotFound from rest_framework.exceptions import ValidationError as RestFrameworkValidationError +from dojo.endpoint.queries import get_authorized_endpoints from dojo.importers.auto_create_context import AutoCreateContextManager from dojo.importers.base_importer import BaseImporter from dojo.importers.default_importer import DefaultImporter from dojo.importers.default_reimporter import DefaultReImporter from dojo.location.models import Location +from dojo.location.queries import get_authorized_locations from dojo.models import ( DEDUPLICATION_EXECUTION_MODE_CHOICES, IMPORT_ACTIONS, @@ -514,14 +516,26 @@ class CommonImportScanSerializer(serializers.Serializer): # TODO: Delete this after the move to Locations def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # Scope endpoint_to_add to the locations/endpoints the requesting user is authorized for. + user = getattr(self.context.get("request"), "user", None) + # An unauthenticated user (e.g. AnonymousUser during OpenAPI schema + # generation, where the request has no real user) is truthy but cannot + # be authorization-scoped; treat it as no user so the field falls back to + # an empty queryset instead of raising on the AnonymousUser instance. + if user is not None and not user.is_authenticated: + user = None if not settings.V3_FEATURE_LOCATIONS: # TODO: why do we allow only existing endpoints? self.fields["endpoint_to_add"] = serializers.PrimaryKeyRelatedField( - queryset=Endpoint.objects.all(), + queryset=get_authorized_endpoints("view", user=user) if user else Endpoint.objects.none(), required=False, default=None, help_text="Enter the ID of an Endpoint that is associated with the target Product. New Findings will be added to that Endpoint.", ) + else: + self.fields["endpoint_to_add"].queryset = ( + get_authorized_locations("view", user=user) if user else Location.objects.none() + ) def get_importer( self, diff --git a/dojo/db_migrations/0280_vulnerability_id_upper_index.py b/dojo/db_migrations/0280_vulnerability_id_upper_index.py new file mode 100644 index 00000000000..c6a0ea3a8b0 --- /dev/null +++ b/dojo/db_migrations/0280_vulnerability_id_upper_index.py @@ -0,0 +1,43 @@ +"""Add a case-insensitive expression index on Vulnerability_Id.vulnerability_id. + +Enables fast UPPER(vulnerability_id) → finding lookups for CVE-keyed enrichment/threat +intel. Built CONCURRENTLY (non-atomic migration) so it does not lock dojo_vulnerability_id +on large instances. CREATE ... IF NOT EXISTS makes it idempotent and safe to co-exist with +any downstream (e.g. Pro) migration that created the same-named index first. +""" +from django.db import migrations, models +from django.db.models.functions import Upper + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("dojo", "0279_jira_project_transition_fields"), + ] + + operations = [ + # RunSQL (IF NOT EXISTS) does the idempotent DB work; state_operations keeps Django's + # model state in sync with the Meta index so future makemigrations is a no-op. + migrations.SeparateDatabaseAndState( + database_operations=[ + migrations.RunSQL( + sql=( + "CREATE INDEX CONCURRENTLY IF NOT EXISTS dojo_vulnid_upper_idx " + "ON dojo_vulnerability_id (UPPER(vulnerability_id), finding_id)" + ), + reverse_sql="DROP INDEX CONCURRENTLY IF EXISTS dojo_vulnid_upper_idx", + ), + ], + state_operations=[ + migrations.AddIndex( + model_name="vulnerability_id", + index=models.Index( + Upper("vulnerability_id"), + "finding", + name="dojo_vulnid_upper_idx", + ), + ), + ], + ), + ] diff --git a/dojo/finding/api/serializer.py b/dojo/finding/api/serializer.py index 360f08b7926..f70494dc716 100644 --- a/dojo/finding/api/serializer.py +++ b/dojo/finding/api/serializer.py @@ -15,6 +15,7 @@ import dojo.finding.helper as finding_helper from dojo.authorization.authorization import user_has_permission from dojo.celery_dispatch import dojo_dispatch_task +from dojo.endpoint.queries import get_authorized_endpoints from dojo.finding.helper import ( save_endpoints_template, save_vulnerability_ids, @@ -24,6 +25,7 @@ from dojo.jira import services as jira_services from dojo.jira.api.serializers import JIRAIssueSerializer from dojo.location.models import LocationFindingReference +from dojo.location.queries import get_authorized_location_finding_reference from dojo.models import ( SEVERITIES, Development_Environment, @@ -349,9 +351,25 @@ def get_fields(self): # TODO: Delete this after the move to Locations def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + # Scope the endpoints field to the references the requesting user is authorized + # for, mirroring the scoping the finding UI form already applies. + user = getattr(self.context.get("request"), "user", None) + # An unauthenticated user (e.g. AnonymousUser during OpenAPI schema + # generation, where the request has no real user) is truthy but cannot + # be authorization-scoped; treat it as no user so the field falls back to + # an empty queryset instead of raising on the AnonymousUser instance. + if user is not None and not user.is_authenticated: + user = None if not settings.V3_FEATURE_LOCATIONS: self.fields["endpoints"] = serializers.PrimaryKeyRelatedField( - many=True, required=False, queryset=Endpoint.objects.all(), + many=True, required=False, + queryset=get_authorized_endpoints("view", user=user) if user else Endpoint.objects.none(), + ) + else: + self.fields["endpoints"] = serializers.PrimaryKeyRelatedField( + source="locations", many=True, required=False, + queryset=get_authorized_location_finding_reference("view", user=user) + if user else LocationFindingReference.objects.none(), ) def get_accepted_risks(self, obj): diff --git a/dojo/finding/models.py b/dojo/finding/models.py index 7442eb1155e..4cf3e71382c 100644 --- a/dojo/finding/models.py +++ b/dojo/finding/models.py @@ -14,6 +14,7 @@ from django.contrib.postgres.search import SearchVector from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models +from django.db.models.functions import Upper from django.urls import reverse from django.utils import timezone from django.utils.functional import cached_property @@ -1385,6 +1386,15 @@ class Meta: name="dojo_vulnerability_id_fts_gin", ), GinIndex(fields=["vulnerability_id"], opclasses=["gin_trgm_ops"], name="dojo_vuln_id_trgm"), + # Case-insensitive vulnerability-id → finding lookups (e.g. threat-intel / + # exploit-enrichment keyed by CVE). Scanner-provided ids are not case-normalized, + # so the index and any query must both fold with UPPER(). finding_id is included + # so the changed-key → finding fan-out is an index-only scan. + models.Index( + Upper("vulnerability_id"), + "finding", + name="dojo_vulnid_upper_idx", + ), ] def __str__(self): diff --git a/dojo/management/commands/migrate_endpoints_to_locations.py b/dojo/management/commands/migrate_endpoints_to_locations.py index 081f2077024..6ae1996de52 100644 --- a/dojo/management/commands/migrate_endpoints_to_locations.py +++ b/dojo/management/commands/migrate_endpoints_to_locations.py @@ -5,13 +5,14 @@ from collections import defaultdict from django.core.management.base import BaseCommand -from django.db import connection +from django.db import connection, transaction from django.db.models import Prefetch from django.utils import timezone from dojo.location.models import Location, LocationFindingReference, LocationProductReference from dojo.location.status import FindingLocationStatus, ProductLocationStatus from dojo.models import DojoMeta, Endpoint, Endpoint_Status, Product +from dojo.tags.utils import bulk_add_tag_mapping from dojo.url.models import URL logger = logging.getLogger(__name__) @@ -45,7 +46,7 @@ def _suspend_auto_now_add(model, field_name: str): PHASES = ( "fetch_endpoint", # iterator yields the next endpoint "url_create", # URL.get_or_create_from_values + Location side-effect - "tags", # endpoint tag copy onto the location + "tags", # batched endpoint tag copy onto locations "meta", # DojoMeta copy onto the location "finding_refs", # LocationFindingReference creation per Endpoint_Status "product_refs", # LocationProductReference creation @@ -108,11 +109,11 @@ def _track_product_location(self, product: Product, location: Location) -> None: The migration creates locations that may be linked to multiple products (via the endpoint's own product and via each finding's product). We collect every contributing product per location so the post-pass can - call ``LocationManager(product)._bulk_inherit_tags(locations)`` once - per product group — covering the case where a location is shared - across products with differing ``enable_product_tag_inheritance`` - flags (the helper short-circuits via its own diff check on repeat - visits, so redundancy is safe). + call ``apply_inherited_tags_for_locations`` once per product group — + covering the case where a location is shared across products with + differing ``enable_product_tag_inheritance`` flags (the helper + short-circuits via its own diff check on repeat visits, so redundancy + is safe). """ if product is None or product.id is None: return @@ -122,6 +123,79 @@ def _track_product_location(self, product: Product, location: Location) -> None: self.product_obj_by_id.setdefault(product.id, product) self.location_obj_by_id.setdefault(location.id, location) + # -- Endpoint tag batching ----------------------------------------------- + + def _queue_location_tags( + self, + endpoint: Endpoint, + location: Location, + tag_names: set[str], + ) -> None: + """Queue endpoint tags for a batched write to their destination Location.""" + if endpoint.id is None or location.id is None: + return + for tag_name in tag_names: + # Multiple legacy Endpoints may normalize to the same Location. + # Deduplicate by Location id so the through row and Tagulous count + # are each updated exactly once. + self.pending_tag_locations[tag_name][location.id] = location + self.pending_endpoint_tags[endpoint.id] = (location, tag_names) + + def _record_endpoint_failure(self, endpoint_id: int | None, exc: Exception) -> None: + """Record an Endpoint once even if more than one migration phase fails.""" + if endpoint_id in self.failed_endpoint_ids: + return + self.failed_endpoint_ids.add(endpoint_id) + self.failed_endpoints.append((endpoint_id, str(exc))) + + def _flush_location_tags(self) -> None: + """Persist queued tags, retrying per Endpoint if the batch write fails.""" + if not self.pending_tag_locations: + return + + tag_to_locations = { + tag_name: list(locations_by_id.values()) + for tag_name, locations_by_id in self.pending_tag_locations.items() + } + t = self._bench_start() + try: + try: + # bulk_add_tag_mapping creates tags, through rows, and updates + # Tagulous counters in separate steps. The outer transaction + # makes the complete batch atomic if any step fails. + with transaction.atomic(): + bulk_add_tag_mapping(tag_to_locations, batch_size=self.batch_size) + except Exception: + endpoint_ids = list(self.pending_endpoint_tags) + logger.exception( + "Batched endpoint tag copy failed for %d tagged endpoint(s); " + "first endpoint ids=%s; retrying one endpoint at a time", + len(endpoint_ids), + endpoint_ids[:10], + ) + + # Preserve the command's documented per-row resilience. This + # slower path runs only after a failed batch and isolates a bad + # Endpoint without discarding valid tag writes for its peers. + for endpoint_id, (location, tag_names) in self.pending_endpoint_tags.items(): + endpoint_mapping = { + tag_name: [location] + for tag_name in tag_names + } + try: + with transaction.atomic(): + bulk_add_tag_mapping(endpoint_mapping, batch_size=self.batch_size) + except Exception as exc: + logger.exception( + "Failed to copy tags for endpoint id=%s; continuing", + endpoint_id, + ) + self._record_endpoint_failure(endpoint_id, exc) + finally: + self._bench_end("tags", t) + self.pending_tag_locations.clear() + self.pending_endpoint_tags.clear() + # -- Migration logic -------------------------------------------------- def _endpoint_to_url(self, endpoint: Endpoint) -> Location: @@ -139,13 +213,12 @@ def _endpoint_to_url(self, endpoint: Endpoint) -> Location: ) self._bench_end("url_create", t) - # Add the endpoint tags to the location tags. Read names from the - # prefetched `tags` manager and add them in a single splat call so we - # do one round-trip per endpoint instead of one per tag. + # Queue endpoint tags for one bulk write per migration batch instead + # of making Tagulous look up and attach tags once per endpoint. t = self._bench_start() tag_names = {tag.name for tag in endpoint.tags.all()} if tag_names: - url.location.tags.add(*tag_names) + self._queue_location_tags(endpoint, url.location, tag_names) self._bench_end("tags", t) # Add any metadata from the endpoint to the location. @@ -330,22 +403,21 @@ def _print_benchmark_summary(self, total_endpoints: int, total_seconds: float) - def _run_tag_inheritance(self) -> None: """ - Drive `LocationManager._bulk_inherit_tags` once per contributing product. + Apply inherited tags once per contributing product. - Each `LocationManager` call is wrapped in its own try/except so a + Each product batch is wrapped in its own try/except so a failure on one product group doesn't prevent the rest from running — - same philosophy as the per-endpoint loop. Tag inheritance is a - purely additive post-pass; the underlying location/reference rows - are already committed by the main loop, so partial failure here - leaves a consistent (if incomplete-inheritance) state that a - targeted re-run can finish. + same philosophy as the per-endpoint loop. The underlying + location/reference rows are already committed by the main loop, so + partial failure here leaves a consistent (if incompletely reconciled) + inheritance state that a targeted re-run can finish. """ if not self.locations_by_product_id: return - # Lazy import: dojo.importers.* pulls in a lot of modules and we - # don't want it loaded at management-command discovery time. - from dojo.importers.location_manager import LocationManager # noqa: PLC0415 + # Lazy import: the inheritance module imports the full model layer, so + # keep it out of management-command discovery. + from dojo.tags import inheritance as tag_inheritance # noqa: PLC0415 t0 = time.time() n_products = len(self.locations_by_product_id) @@ -356,7 +428,10 @@ def _run_tag_inheritance(self) -> None: product = self.product_obj_by_id[prod_id] locations = [self.location_obj_by_id[lid] for lid in loc_ids] try: - LocationManager(product)._bulk_inherit_tags(locations) + tag_inheritance.apply_inherited_tags_for_locations( + locations, + product=product, + ) except Exception: logger.exception( "Tag inheritance pass failed for product id=%s " @@ -391,16 +466,22 @@ def handle(self, *args, **options): # `locations_by_product_id` maps product.id -> set of location.ids # contributed by that product (via endpoint.product OR finding.test. # engagement.product). We hold the Product/Location objects in - # parallel maps so the post-pass can hand them directly to - # `LocationManager(product)._bulk_inherit_tags(locations)` without - # extra DB lookups. + # parallel maps so the post-pass can hand them directly to the bulk + # inheritance helper. self.locations_by_product_id: dict[int, set[int]] = defaultdict(set) self.product_obj_by_id: dict[int, Product] = {} self.location_obj_by_id: dict[int, Location] = {} + # Endpoint tags are copied to Locations once per migration batch. + # The nested Location-id mapping prevents duplicate through rows and + # tag-count drift when multiple Endpoints normalize to one Location. + self.pending_tag_locations: dict[str, dict[int, Location]] = defaultdict(dict) + self.pending_endpoint_tags: dict[int, tuple[Location, set[str]]] = {} + # Collected per-endpoint failures so a single bad row doesn't abort # a multi-hour migration. Each entry is (endpoint_id, exception_str). self.failed_endpoints: list[tuple[int | None, str]] = [] + self.failed_endpoint_ids: set[int | None] = set() if self.query_count: connection.force_debug_cursor = True @@ -473,8 +554,12 @@ def handle(self, *args, **options): except Exception as exc: endpoint_id = getattr(endpoint, "id", None) logger.exception("Failed to migrate endpoint id=%s; continuing", endpoint_id) - self.failed_endpoints.append((endpoint_id, str(exc))) - continue + self._record_endpoint_failure(endpoint_id, exc) + + # Flush independently of per-endpoint success so a failing + # endpoint at a batch boundary cannot leave the queue growing. + if i % self.batch_size == 0: + self._flush_location_tags() # Progress report every --progress-every endpoints if i % self.progress_every == 0: @@ -487,6 +572,9 @@ def handle(self, *args, **options): queries_at_chunk_start = 0 self._log_progress(i, endpoint_count, run_t0, queries_in_chunk) + # Persist the final partial batch before reporting completion. + self._flush_location_tags() + elapsed = time.time() - run_t0 successful = i - len(self.failed_endpoints) self.stdout.write(self.style.SUCCESS( @@ -506,7 +594,7 @@ def handle(self, *args, **options): # product or system-wide) the migrated Locations would otherwise # not pick up inherited product tags. We grouped (product, # location) pairs during the main loop and now drive - # `LocationManager._bulk_inherit_tags` once per contributing + # `apply_inherited_tags_for_locations` once per contributing # product. The helper rediscovers each location's full product # set via LocationProductReference/LocationFindingReference and # diff-checks before writing, so revisits of shared locations diff --git a/dojo/product_type/api/serializer.py b/dojo/product_type/api/serializer.py index fc09e6e7100..aa569a8c392 100644 --- a/dojo/product_type/api/serializer.py +++ b/dojo/product_type/api/serializer.py @@ -1,4 +1,5 @@ from rest_framework import serializers +from rest_framework.exceptions import PermissionDenied from dojo.product_type.models import Product_Type @@ -7,3 +8,44 @@ class ProductTypeSerializer(serializers.ModelSerializer): class Meta: model = Product_Type fields = "__all__" + + def validate(self, data): + self._validate_authorized_users_change(data) + return data + + def _validate_authorized_users_change(self, data): + """ + Changing ``authorized_users`` requires ``Product_Type_Manage_Members``; + all other fields on this serializer require ``Product_Type_Edit``. No-op + when the field is absent or unchanged (replay-safe). Mirrors + dojo.product.api.serializer.ProductSerializer. + """ + if "authorized_users" not in data: + return + + # Field-level validation has already resolved the payload to Dojo_User + # instances at this point. + from dojo.authorization.authorization import ( # noqa: PLC0415 -- lazy import, avoids circular dependency + user_has_permission, + ) + from dojo.authorization.roles_permissions import ( # noqa: PLC0415 -- lazy import, avoids circular dependency + Permissions, + ) + + new_ids = sorted(user.pk for user in (data.get("authorized_users") or [])) + current_ids = ( + sorted(self.instance.authorized_users.values_list("pk", flat=True)) + if self.instance is not None + else [] + ) + if new_ids == current_ids: + return + + request = self.context.get("request") + request_user = getattr(request, "user", None) + if not ( + request_user + and user_has_permission(request_user, self.instance, Permissions.Product_Type_Manage_Members) + ): + msg = "You do not have permission to manage authorized users for this product type." + raise PermissionDenied(msg) diff --git a/dojo/static/dojo/css/classic/dojo.css b/dojo/static/dojo/css/classic/dojo.css index 9fec865c0d4..17b75a948d6 100644 --- a/dojo/static/dojo/css/classic/dojo.css +++ b/dojo/static/dojo/css/classic/dojo.css @@ -1161,6 +1161,51 @@ div.custom-search-form { margin-top: 8px; } +/* OS message banner dismiss (×): inline, grouped with the headline/caret + (not floated into the empty right side). Scoped so it only affects the + OS promo banner, which is the only banner carrying these classes. */ +/* Lay out the OS promo banner as a single centered row so the dismiss button, + headline, and expand caret line up vertically. Scoped to data-source="os" + so the other banners are untouched. */ +.announcement-banner[data-source="os"] { + display: flex; + align-items: center; + flex-wrap: wrap; +} + +.announcement-banner[data-source="os"] .banner-expanded { + flex-basis: 100%; /* expanded text drops to its own row */ +} + +.announcement-banner .os-message-dismiss-form { + display: inline-flex; + margin-right: 10px; +} + +.announcement-banner .os-message-dismiss { + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + padding: 0; + background: transparent; + border: 1px solid rgba(0, 0, 0, 0.3); + border-radius: 3px; + color: inherit; + cursor: pointer; + font-size: 13px; + line-height: 1; + opacity: 0.7; +} + +.announcement-banner .os-message-dismiss:hover, +.announcement-banner .os-message-dismiss:focus { + opacity: 1; + background: rgba(0, 0, 0, 0.06); + outline: none; +} + @media (min-width: 795px) { div.custom-search-form { height: 21px; diff --git a/dojo/templates/dojo/endpoint_pdf_report.html b/dojo/templates/dojo/endpoint_pdf_report.html index 96d76d0b71f..84d152f97e5 100644 --- a/dojo/templates/dojo/endpoint_pdf_report.html +++ b/dojo/templates/dojo/endpoint_pdf_report.html @@ -285,7 +285,7 @@
Notes
{% endblock %} diff --git a/dojo/templates/dojo/engagement_pdf_report.html b/dojo/templates/dojo/engagement_pdf_report.html index 8b0dd906af2..8ae8c2d29a5 100644 --- a/dojo/templates/dojo/engagement_pdf_report.html +++ b/dojo/templates/dojo/engagement_pdf_report.html @@ -420,7 +420,7 @@
Notes
{% endblock %} diff --git a/dojo/templates/dojo/finding_pdf_report.html b/dojo/templates/dojo/finding_pdf_report.html index 5b1011fd178..c5667c1c37e 100644 --- a/dojo/templates/dojo/finding_pdf_report.html +++ b/dojo/templates/dojo/finding_pdf_report.html @@ -270,7 +270,7 @@
Notes
{% endblock %} diff --git a/dojo/templates/dojo/product_endpoint_pdf_report.html b/dojo/templates/dojo/product_endpoint_pdf_report.html index b86e2860267..77348e6eb3d 100644 --- a/dojo/templates/dojo/product_endpoint_pdf_report.html +++ b/dojo/templates/dojo/product_endpoint_pdf_report.html @@ -338,7 +338,7 @@
Notes
{% endblock metrics %} {% endblock %} diff --git a/dojo/templates/dojo/product_pdf_report.html b/dojo/templates/dojo/product_pdf_report.html index a25e0084192..9501b62afbd 100644 --- a/dojo/templates/dojo/product_pdf_report.html +++ b/dojo/templates/dojo/product_pdf_report.html @@ -391,7 +391,7 @@
Notes
{% endblock %} diff --git a/dojo/templates/dojo/product_type_pdf_report.html b/dojo/templates/dojo/product_type_pdf_report.html index 11eeb095e48..9848f8bc498 100644 --- a/dojo/templates/dojo/product_type_pdf_report.html +++ b/dojo/templates/dojo/product_type_pdf_report.html @@ -320,7 +320,7 @@
Notes
{% endblock %} diff --git a/dojo/templates/dojo/test_pdf_report.html b/dojo/templates/dojo/test_pdf_report.html index c86684da0c6..c0001a3c150 100644 --- a/dojo/templates/dojo/test_pdf_report.html +++ b/dojo/templates/dojo/test_pdf_report.html @@ -434,7 +434,7 @@
Notes
{% endblock %} diff --git a/dojo/templates_classic/dojo/endpoint_pdf_report.html b/dojo/templates_classic/dojo/endpoint_pdf_report.html index a1d6843c144..37076ef3e9c 100644 --- a/dojo/templates_classic/dojo/endpoint_pdf_report.html +++ b/dojo/templates_classic/dojo/endpoint_pdf_report.html @@ -287,7 +287,7 @@
Notes
{% endblock %} diff --git a/dojo/templates_classic/dojo/engagement_pdf_report.html b/dojo/templates_classic/dojo/engagement_pdf_report.html index 7b90548ae1b..b10b3293ecd 100644 --- a/dojo/templates_classic/dojo/engagement_pdf_report.html +++ b/dojo/templates_classic/dojo/engagement_pdf_report.html @@ -422,7 +422,7 @@
Notes
{% endblock %} diff --git a/dojo/templates_classic/dojo/finding_pdf_report.html b/dojo/templates_classic/dojo/finding_pdf_report.html index 86820527db2..d44a3b1092c 100644 --- a/dojo/templates_classic/dojo/finding_pdf_report.html +++ b/dojo/templates_classic/dojo/finding_pdf_report.html @@ -266,7 +266,7 @@
Notes
{% endblock %} diff --git a/dojo/templates_classic/dojo/product_endpoint_pdf_report.html b/dojo/templates_classic/dojo/product_endpoint_pdf_report.html index b5c67cf9b1c..63d91e77624 100644 --- a/dojo/templates_classic/dojo/product_endpoint_pdf_report.html +++ b/dojo/templates_classic/dojo/product_endpoint_pdf_report.html @@ -344,7 +344,7 @@
Notes
{% endblock metrics %} {% endblock %} diff --git a/dojo/templates_classic/dojo/product_pdf_report.html b/dojo/templates_classic/dojo/product_pdf_report.html index 347fa6f810e..3580801dfbe 100644 --- a/dojo/templates_classic/dojo/product_pdf_report.html +++ b/dojo/templates_classic/dojo/product_pdf_report.html @@ -397,7 +397,7 @@
Notes
{% endif %} {% endblock %} diff --git a/dojo/templates_classic/dojo/product_type_pdf_report.html b/dojo/templates_classic/dojo/product_type_pdf_report.html index 36a2f82a1b7..3c5234e6997 100644 --- a/dojo/templates_classic/dojo/product_type_pdf_report.html +++ b/dojo/templates_classic/dojo/product_type_pdf_report.html @@ -322,7 +322,7 @@
Notes
{% endblock %} diff --git a/dojo/templates_classic/dojo/test_pdf_report.html b/dojo/templates_classic/dojo/test_pdf_report.html index 7dd1e0f102c..0989b8796c0 100644 --- a/dojo/templates_classic/dojo/test_pdf_report.html +++ b/dojo/templates_classic/dojo/test_pdf_report.html @@ -436,7 +436,7 @@
Notes
{% endblock %} diff --git a/dojo/test/api/serializer.py b/dojo/test/api/serializer.py index 7ce8f5613c6..fdd022a1ae7 100644 --- a/dojo/test/api/serializer.py +++ b/dojo/test/api/serializer.py @@ -3,7 +3,6 @@ from dojo.models import ( Engagement, - Notes, Test, Test_Import, Test_Import_Finding_Action, @@ -65,11 +64,11 @@ class TestCreateSerializer(serializers.ModelSerializer): engagement = serializers.PrimaryKeyRelatedField( queryset=Engagement.objects.all(), ) + # notes are added through the notes endpoint, not at create time, so keep + # this read-only here (findings already do the same). notes = serializers.PrimaryKeyRelatedField( - allow_null=True, - queryset=Notes.objects.all(), many=True, - required=False, + read_only=True, ) class Meta: diff --git a/dojo/tools/fortify/fpr_parser.py b/dojo/tools/fortify/fpr_parser.py index b13689e565e..90d649fae04 100644 --- a/dojo/tools/fortify/fpr_parser.py +++ b/dojo/tools/fortify/fpr_parser.py @@ -333,3 +333,20 @@ def compute_line(self, vulnerability, snippet) -> str: if snippet and snippet.start_line: return snippet.start_line return vulnerability.source_location_line + + +class FortifyFPRParserV2(FortifyFPRParser): + + """ + FPR parser for the "Fortify Scan v2" scan type. + + Stores the line Fortify reports for the vulnerability itself (the SourceLocation + "line" attribute) instead of the snippet StartLine used by the v1 parser, which + includes leading context lines and does not point at the finding. Kept as a + separate scan type so hashcodes of existing "Fortify Scan" findings are unaffected. + """ + + def compute_line(self, vulnerability, snippet) -> str: + if vulnerability.source_location_line: + return vulnerability.source_location_line + return super().compute_line(vulnerability, snippet) diff --git a/dojo/tools/fortify/parser.py b/dojo/tools/fortify/parser.py index 7d2b15c0e25..322a6a40a05 100644 --- a/dojo/tools/fortify/parser.py +++ b/dojo/tools/fortify/parser.py @@ -1,4 +1,4 @@ -from dojo.tools.fortify.fpr_parser import FortifyFPRParser +from dojo.tools.fortify.fpr_parser import FortifyFPRParser, FortifyFPRParserV2 from dojo.tools.fortify.xml_parser import FortifyXMLParser @@ -19,3 +19,22 @@ def get_findings(self, filename, test): return FortifyFPRParser().parse_fpr(filename, test) msg = "Filename extension not recognized. Use .xml or .fpr" raise ValueError(msg) + + +class FortifyParserV2: + def get_scan_types(self): + return ["Fortify Scan v2"] + + def get_label_for_scan_types(self, scan_type): + return scan_type # no custom label for now + + def get_description_for_scan_types(self, scan_type): + return "Import Findings in FPR or XML file format. Unlike Fortify Scan, FPR findings use the line reported by Fortify rather than the start of the code snippet." + + def get_findings(self, filename, test): + if str(filename.name).endswith(".xml"): + return FortifyXMLParser().parse_xml(filename, test) + if str(filename.name).endswith(".fpr"): + return FortifyFPRParserV2().parse_fpr(filename, test) + msg = "Filename extension not recognized. Use .xml or .fpr" + raise ValueError(msg) diff --git a/dojo/tools/kics/parser.py b/dojo/tools/kics/parser.py index 878999ee70a..badee54c2b2 100644 --- a/dojo/tools/kics/parser.py +++ b/dojo/tools/kics/parser.py @@ -10,6 +10,7 @@ class KICSParser: # table to match KICS severity to DefectDojo severity SEVERITY = { + "CRITICAL": "Critical", "HIGH": "High", "MEDIUM": "Medium", "LOW": "Low", diff --git a/dojo/url/ui/views.py b/dojo/url/ui/views.py index 826c9329262..6c5f416a582 100644 --- a/dojo/url/ui/views.py +++ b/dojo/url/ui/views.py @@ -14,6 +14,7 @@ from django.utils import timezone from dojo.authorization.authorization import user_has_permission_or_403 +from dojo.authorization.roles_permissions import Permissions from dojo.endpoint.utils import endpoint_meta_import from dojo.forms import ( DeleteEndpointForm, @@ -24,6 +25,7 @@ from dojo.location.queries import annotate_location_counts_and_status, get_authorized_locations from dojo.location.status import FindingLocationStatus, ProductLocationStatus from dojo.models import DojoMeta, Finding, Product +from dojo.product.queries import get_authorized_products from dojo.reports.ui.views import generate_report from dojo.url.filters import URLFilter from dojo.url.models import URL @@ -536,13 +538,26 @@ def endpoint_bulk_update_all(request, product_id=None): f"Skipped mitigation of {skipped_location_count} locations because you are not authorized.", ) + # Scope the reference updates to the acting product (or, on the all-products + # route, the products the user may edit); get_authorized_locations above scopes + # only the Location rows, not their references. + if product_id is not None: + reference_products = Product.objects.filter(id=product_id) + else: + reference_products = get_authorized_products(Permissions.Product_Edit, request.user) # Bulk update the status of related FindingLocationStatus and ProductLocationStatus objects to 'Mitigated' - finding_update_counts = LocationFindingReference.objects.filter(location__in=locations).update( + finding_update_counts = LocationFindingReference.objects.filter( + location__in=locations, + finding__test__engagement__product__in=reference_products, + ).update( status=FindingLocationStatus.Mitigated, auditor=request.user, audit_time=timezone.now(), ) - product_update_counts = LocationProductReference.objects.filter(location__in=locations).update( + product_update_counts = LocationProductReference.objects.filter( + location__in=locations, + product__in=reference_products, + ).update( status=ProductLocationStatus.Mitigated, ) # Total number of updated statuses for reporting diff --git a/dojo/user/api/serializer.py b/dojo/user/api/serializer.py index 20acec33493..8bf4b471d94 100644 --- a/dojo/user/api/serializer.py +++ b/dojo/user/api/serializer.py @@ -155,6 +155,46 @@ def validate(self, data): msg = "Only superusers are allowed to add or edit staff users." raise ValidationError(msg) + # Configuration permissions are privilege-bearing: they grant the + # ability to manage users, groups, tool configurations, and so on. + # Only superusers may assign or change them, which keeps the set of + # grantable capabilities under superuser control and mirrors the + # is_staff / is_superuser guards above. The "configuration_permissions" + # field maps to the "user_permissions" source. + if "user_permissions" in data and not self.context["request"].user.is_superuser: + requested_permissions = set(data.get("user_permissions") or []) + if self.instance is not None: + allowed_configuration_permissions = set( + self.fields["configuration_permissions"].child_relation.queryset.all(), + ) + current_permissions = ( + set(self.instance.user_permissions.all()) + & allowed_configuration_permissions + ) + else: + current_permissions = set() + if requested_permissions != current_permissions: + msg = "Only superusers are allowed to change configuration permissions." + raise ValidationError(msg) + + # Identity fields (username/email) determine who an account is and drive + # the email-based password-reset flow. A non-superuser holding user-management + # permissions must not be able to change the identity of *another* account: + # changing a victim's email and then triggering a password reset is an + # account-takeover vector. Users may still edit their own identity fields, + # and superusers retain full control. Mirrors the is_superuser / is_staff / + # configuration_permissions guards above. + request_user = self.context["request"].user + if ( + self.instance is not None + and not request_user.is_superuser + and self.instance.pk != request_user.pk + ): + for identity_field in ("username", "email"): + if identity_field in data and data[identity_field] != getattr(self.instance, identity_field): + msg = "Only superusers are allowed to change the username or email of another user." + raise ValidationError(msg) + if self.context["request"].method in {"PATCH", "PUT"} and "password" in data: msg = "Update of password though API is not allowed" raise ValidationError(msg) diff --git a/helm/defectdojo/Chart.yaml b/helm/defectdojo/Chart.yaml index ca248f20480..5cf2d9faffe 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "3.1.200" +appVersion: "3.1.300" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.9.38 +version: 1.9.39 icon: https://defectdojo.com/hubfs/DefectDojo_favicon.png maintainers: - name: madchap @@ -34,4 +34,4 @@ dependencies: # description: Critical bug annotations: artifacthub.io/prerelease: "false" - artifacthub.io/changes: "- kind: changed\n description: Bump DefectDojo to 3.1.200\n" + artifacthub.io/changes: "- kind: changed\n description: Bump DefectDojo to 3.1.300\n" diff --git a/helm/defectdojo/README.md b/helm/defectdojo/README.md index 58a02f7a045..91163734226 100644 --- a/helm/defectdojo/README.md +++ b/helm/defectdojo/README.md @@ -511,7 +511,7 @@ The HELM schema will be generated for you. # General information about chart values -![Version: 1.9.38](https://img.shields.io/badge/Version-1.9.38-informational?style=flat-square) ![AppVersion: 3.1.200](https://img.shields.io/badge/AppVersion-3.1.200-informational?style=flat-square) +![Version: 1.9.39](https://img.shields.io/badge/Version-1.9.39-informational?style=flat-square) ![AppVersion: 3.1.300](https://img.shields.io/badge/AppVersion-3.1.300-informational?style=flat-square) A Helm chart for Kubernetes to install DefectDojo diff --git a/readme-docs/RELEASING.md b/readme-docs/RELEASING.md index c8fc6f89a56..c1b7d94ef79 100644 --- a/readme-docs/RELEASING.md +++ b/readme-docs/RELEASING.md @@ -34,7 +34,7 @@ The steps are identical for both release types, unless specified otherwise below - Merge the PRs ### Always -- Make sure there's a section in [upgrading.md](../docs/content/en/open_source/upgrading) about any specific instructions when upgrading to this new release. +- Make sure there's a section in [os_upgrading](../docs/content/releases/os_upgrading) about any specific instructions when upgrading to this new release. - Remove existing draft releases with the same version number Due to the release drafter being a non-perfect match for our git flow based release process, we have to delete any draft that has already been created by the release drafter if it has the same versio number. This is probably not needed if you're doing a bugfix release. diff --git a/tests/report_builder_test.py b/tests/report_builder_test.py index 7f11eb93fec..0ce5bb08110 100644 --- a/tests/report_builder_test.py +++ b/tests/report_builder_test.py @@ -3,6 +3,7 @@ from base_test_class import BaseTestCase from product_test import ProductTest +from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions @@ -30,6 +31,40 @@ def enter_values(self, driver): if "wysiwyg-content" in class_names: widget.find_element(By.CLASS_NAME, "editor").send_keys("wysiwyg") + # Regression guard for the report-chart / table-of-contents race: the + # executive-summary charts are rendered from a ready handler, while the + # table-of-contents builder runs on window.onload and reassigns + # #contents.innerHTML. That reassignment re-parses the subtree and + # destroys any chart canvases painted before it (Chart.js in the new UI, + # Flot in the classic UI). With executive summary AND table of contents + # both enabled, every chart must end up painted once the page settles. + def assert_report_charts_painted(self, chart_ids): + driver = self.driver + charts_painted = ( + "return arguments[0].map(function (id) {" + " var el = document.getElementById(id);" + " if (!el) { return false; }" + " var canvases = el.querySelectorAll('canvas');" + " for (var i = 0; i < canvases.length; i++) {" + " if (!canvases[i].width || !canvases[i].height) { continue; }" + " var ctx = canvases[i].getContext('2d');" + " var data = ctx.getImageData(0, 0, canvases[i].width, canvases[i].height).data;" + " for (var j = 3; j < data.length; j += 64) {" + " if (data[j] > 0) { return true; }" + " }" + " }" + " return false;" + "});" + ) + try: + WebDriverWait(driver, 20).until(lambda d: all(d.execute_script(charts_painted, chart_ids))) + except TimeoutException: + self.fail( + "Executive-summary charts were not painted after page load " + f"({chart_ids} painted: {driver.execute_script(charts_painted, chart_ids)}); " + "the table-of-contents builder likely destroyed them.", + ) + def generate_HTML_report(self): driver = self.driver driver.get(self.base_url + "reports/builder") @@ -59,6 +94,10 @@ def test_product_type_report(self): driver.find_element(By.NAME, "_generate").click() + # opened_per_month_2 is only rendered when the product type has + # endpoint-per-month data, so only the unconditional chart is asserted. + self.assert_report_charts_painted(["open_findings"]) + def test_product_report(self): driver = self.driver self.goto_product_overview(driver) @@ -77,6 +116,8 @@ def test_product_report(self): driver.find_element(By.NAME, "_generate").click() + self.assert_report_charts_painted(["open_findings", "finding_age"]) + def test_engagement_report(self): driver = self.driver self.goto_product_overview(driver) @@ -97,6 +138,8 @@ def test_engagement_report(self): driver.find_element(By.NAME, "_generate").click() + self.assert_report_charts_painted(["open_findings", "finding_age"]) + def test_test_report(self): driver = self.driver self.goto_product_overview(driver) @@ -118,6 +161,8 @@ def test_test_report(self): driver.find_element(By.NAME, "_generate").click() + self.assert_report_charts_painted(["open_findings", "finding_age"]) + def test_product_endpoint_report(self): driver = self.driver self.goto_product_overview(driver) @@ -144,7 +189,14 @@ def test_product_endpoint_report(self): driver.find_element(By.NAME, "_generate").click() + self.assert_report_charts_painted(["accepted_findings", "open_findings", "closed_findings", "finding_age"]) + def test_product_list_report(self): + # Unlike the report tests above, this does not call + # assert_report_charts_painted(): it generates the Findings Report, + # whose view currently errors before the report (and its charts) + # render, so there are no chart canvases to assert on. This test only + # exercises the generate action until that view is fixed. driver = self.driver self.goto_product_overview(driver) driver.find_element(By.ID, "dropdownMenu1").click() diff --git a/unittests/test_apiv2_test_create_notes_readonly.py b/unittests/test_apiv2_test_create_notes_readonly.py new file mode 100644 index 00000000000..d76fd689e63 --- /dev/null +++ b/unittests/test_apiv2_test_create_notes_readonly.py @@ -0,0 +1,57 @@ +import logging + +from django.utils import timezone +from rest_framework.authtoken.models import Token + +from dojo.models import Engagement, Notes, Product, Product_Type, Test, Test_Type, User + +from .dojo_test_case import DojoAPITestCase + +logger = logging.getLogger(__name__) + + +class TestCreateNotesReadOnlyAPI(DojoAPITestCase): + + """notes should be read-only when creating a test (added through the notes endpoint instead).""" + + def setUp(self): + user, _ = User.objects.get_or_create(username="admin", defaults={"is_superuser": True, "is_staff": True}) + token, _ = Token.objects.get_or_create(user=user) + self.client.credentials(HTTP_AUTHORIZATION=f"Token {token.key}") + + product_type, _ = Product_Type.objects.get_or_create(name="notes-readonly-api") + product, _ = Product.objects.get_or_create( + name="TestCreateNotesReadOnlyAPI", + description="Test", + prod_type=product_type, + ) + self.engagement, _ = Engagement.objects.get_or_create( + name="notes readonly api", + product=product, + target_start=timezone.now(), + target_end=timezone.now(), + ) + self.test_type, _ = Test_Type.objects.get_or_create(name="Acunetix Scan") + # a Note row that already exists independently of the create request + self.existing_note = Notes.objects.create(entry="preexisting", author=user) + + def _payload(self, **overrides): + payload = { + "engagement": self.engagement.id, + "test_type": self.test_type.id, + "title": "notes readonly", + "target_start": "2026-07-01T00:00:00Z", + "target_end": "2026-07-02T00:00:00Z", + } + payload.update(overrides) + return payload + + def test_notes_supplied_on_create_are_ignored(self): + response = self.client.post("/api/v2/tests/", self._payload(notes=[self.existing_note.id]), format="json") + self.assertEqual(201, response.status_code, response.content) + body = response.json() + # read side unchanged: notes is still part of the representation ... + self.assertIn("notes", body) + # ... but read-only, so the supplied id is not adopted by the new Test + self.assertEqual([], body["notes"]) + self.assertEqual(0, Test.objects.get(id=body["id"]).notes.count()) diff --git a/unittests/test_apiv2_user.py b/unittests/test_apiv2_user.py index f6273fe9b47..cff1f93d9e4 100644 --- a/unittests/test_apiv2_user.py +++ b/unittests/test_apiv2_user.py @@ -314,6 +314,134 @@ def test_superuser_can_set_is_staff_via_api(self): self.assertEqual(r.status_code, 200, r.content[:1000]) self.assertTrue(User.objects.get(id=user_id).is_staff) + def test_non_superuser_cannot_grant_configuration_permissions_via_api(self): + """ + Only superusers may assign configuration permissions. A non-superuser, + even one holding the delegated change_user permission, must not be able + to grant configuration permissions to their own account or to another + user, whether on update or at create time. Configuration permissions + are privilege-bearing (managing users, groups, tool configurations, and + so on), so assigning them is a superuser-only action. + """ + password = "testTEST1234!@#$" + r = self.client.post(reverse("user-list"), { + "username": "api-cfgperm-mgr", + "email": "admin@dojo.com", + "password": password, + }, format="json") + self.assertEqual(r.status_code, 201, r.content[:1000]) + mgr = User.objects.get(username="api-cfgperm-mgr") + # Delegated user-manager: may change and create users, nothing more. + mgr.user_permissions.add( + Permission.objects.get(codename="change_user"), + Permission.objects.get(codename="add_user"), + ) + delete_user = Permission.objects.get(codename="delete_user") + add_group = Permission.objects.get(codename="add_group") + + token_resp = self.client.post(reverse("api-token-auth"), { + "username": "api-cfgperm-mgr", + "password": password, + }, format="json") + self.assertEqual(token_resp.status_code, 200, token_resp.content[:1000]) + mgr_client = APIClient() + mgr_client.credentials(HTTP_AUTHORIZATION="Token " + token_resp.json()["token"]) + + # Self-escalation: granting themselves additional configuration + # permissions must be rejected. + r = mgr_client.patch("{}{}/".format(reverse("user-list"), mgr.id), { + "configuration_permissions": [delete_user.id, add_group.id], + }, format="json") + self.assertEqual(r.status_code, 400, r.content[:1000]) + self.assertIn( + "Only superusers are allowed to change configuration permissions.", + r.content.decode("utf-8"), + ) + self.assertFalse(User.objects.get(id=mgr.id).has_perm("auth.delete_user")) + self.assertFalse(User.objects.get(id=mgr.id).has_perm("auth.add_group")) + + # Target-escalation: granting configuration permissions to another user + # must be rejected. + r = self.client.post(reverse("user-list"), { + "username": "api-cfgperm-target", + "email": "admin@dojo.com", + "password": password, + }, format="json") + self.assertEqual(r.status_code, 201, r.content[:1000]) + target_id = r.json()["id"] + + r = mgr_client.patch("{}{}/".format(reverse("user-list"), target_id), { + "configuration_permissions": [delete_user.id], + }, format="json") + self.assertEqual(r.status_code, 400, r.content[:1000]) + self.assertFalse(User.objects.get(id=target_id).has_perm("auth.delete_user")) + + # Create-time escalation must also be rejected. + r = mgr_client.post(reverse("user-list"), { + "username": "api-cfgperm-on-create", + "email": "admin@dojo.com", + "password": password, + "configuration_permissions": [delete_user.id], + }, format="json") + self.assertEqual(r.status_code, 400, r.content[:1000]) + self.assertFalse(User.objects.filter(username="api-cfgperm-on-create").exists()) + + def test_non_superuser_can_resend_unchanged_configuration_permissions(self): + """ + Negative control: the guard only fires when configuration permissions + actually change, so a delegated user-manager can still PATCH their own + account (including re-sending the configuration permissions they already + hold) without being blocked. + """ + password = "testTEST1234!@#$" + r = self.client.post(reverse("user-list"), { + "username": "api-cfgperm-mgr2", + "email": "admin@dojo.com", + "password": password, + }, format="json") + self.assertEqual(r.status_code, 201, r.content[:1000]) + mgr = User.objects.get(username="api-cfgperm-mgr2") + change_user = Permission.objects.get(codename="change_user") + mgr.user_permissions.add(change_user) + + token_resp = self.client.post(reverse("api-token-auth"), { + "username": "api-cfgperm-mgr2", + "password": password, + }, format="json") + self.assertEqual(token_resp.status_code, 200, token_resp.content[:1000]) + mgr_client = APIClient() + mgr_client.credentials(HTTP_AUTHORIZATION="Token " + token_resp.json()["token"]) + + # Re-sending the same configuration permission the user already holds is + # a no-op and must be allowed. + r = mgr_client.patch("{}{}/".format(reverse("user-list"), mgr.id), { + "configuration_permissions": [change_user.id], + }, format="json") + self.assertEqual(r.status_code, 200, r.content[:1000]) + + # Editing an unrelated field is likewise unaffected. + r = mgr_client.patch("{}{}/".format(reverse("user-list"), mgr.id), { + "first_name": "Renamed", + }, format="json") + self.assertEqual(r.status_code, 200, r.content[:1000]) + + def test_superuser_can_set_configuration_permissions_via_api(self): + """Positive control: a superuser may still assign configuration permissions.""" + r = self.client.post(reverse("user-list"), { + "username": "api-cfgperm-grantable", + "email": "admin@dojo.com", + "password": "testTEST1234!@#$", + }, format="json") + self.assertEqual(r.status_code, 201, r.content[:1000]) + user_id = r.json()["id"] + delete_user = Permission.objects.get(codename="delete_user") + + r = self.client.patch("{}{}/".format(reverse("user-list"), user_id), { + "configuration_permissions": [delete_user.id], + }, format="json") + self.assertEqual(r.status_code, 200, r.content[:1000]) + self.assertTrue(User.objects.get(id=user_id).has_perm("auth.delete_user")) + def test_user_reset_api_token_denies_global_owner_legacy(self): """ Legacy: Global_Role(role=Owner) is inert. Resetting another diff --git a/unittests/test_apiv2_user_identity_authz.py b/unittests/test_apiv2_user_identity_authz.py new file mode 100644 index 00000000000..c411eee1be1 --- /dev/null +++ b/unittests/test_apiv2_user_identity_authz.py @@ -0,0 +1,90 @@ +from django.contrib.auth.models import Permission +from django.urls import reverse +from rest_framework.authtoken.models import Token +from rest_framework.test import APIClient, APITestCase + +from dojo.models import User +from unittests.dojo_test_case import versioned_fixtures + + +@versioned_fixtures +class UserIdentityFieldAuthzTest(APITestCase): + + """ + A non-superuser holding the user-management configuration permissions must + not be able to change the identity fields (email/username) of another + account. Changing another user's email would enable account takeover via + the email-based password-reset flow. + """ + + fixtures = ["dojo_testdata.json"] + + def setUp(self): + # Non-superuser delegate with only the user-management config permissions. + self.delegate = User.objects.create_user( + username="identity_authz_delegate", + password="not-a-real-secret", # noqa: S106 - test fixture user + ) + self.delegate.user_permissions.add( + Permission.objects.get(codename="view_user", content_type__app_label="auth"), + Permission.objects.get(codename="change_user", content_type__app_label="auth"), + ) + self.target = User.objects.create_user( + username="identity_authz_target", + email="target@example.com", + password="not-a-real-secret", # noqa: S106 - test fixture user + ) + token = Token.objects.create(user=self.delegate) + self.client = APIClient() + self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key) + + def _user_url(self, user_id): + return f"{reverse('user-list')}{user_id}/" + + def test_delegate_cannot_change_another_users_email(self): + r = self.client.patch(self._user_url(self.target.id), {"email": "attacker@evil.example"}) + self.assertEqual(r.status_code, 400, r.content[:1000]) + self.target.refresh_from_db() + self.assertEqual(self.target.email, "target@example.com") + + def test_delegate_cannot_change_another_users_username(self): + r = self.client.patch(self._user_url(self.target.id), {"username": "hijacked"}) + self.assertEqual(r.status_code, 400, r.content[:1000]) + self.target.refresh_from_db() + self.assertEqual(self.target.username, "identity_authz_target") + + def test_delegate_can_change_own_email(self): + r = self.client.patch(self._user_url(self.delegate.id), {"email": "mynew@example.com"}) + self.assertEqual(r.status_code, 200, r.content[:1000]) + self.delegate.refresh_from_db() + self.assertEqual(self.delegate.email, "mynew@example.com") + + @staticmethod + def _perm_id(codename): + return Permission.objects.get(codename=codename, content_type__app_label="auth").id + + def test_delegate_cannot_self_grant_configuration_permissions(self): + # The delegate holds view_user + change_user; it must not be able to add + # further configuration permissions (here: add_user) to itself. + payload = {"configuration_permissions": [ + self._perm_id("view_user"), self._perm_id("change_user"), self._perm_id("add_user"), + ]} + r = self.client.patch(self._user_url(self.delegate.id), payload, format="json") + self.assertEqual(r.status_code, 400, r.content[:1000]) + self.assertFalse(self.delegate.user_permissions.filter(codename="add_user").exists()) + + def test_delegate_cannot_grant_configuration_permissions_to_another_user(self): + payload = {"configuration_permissions": [self._perm_id("add_user")]} + r = self.client.patch(self._user_url(self.target.id), payload, format="json") + self.assertEqual(r.status_code, 400, r.content[:1000]) + self.assertFalse(self.target.user_permissions.filter(codename="add_user").exists()) + + def test_superuser_can_grant_configuration_permissions(self): + admin = User.objects.get(username="admin") + admin_token, _ = Token.objects.get_or_create(user=admin) + admin_client = APIClient() + admin_client.credentials(HTTP_AUTHORIZATION="Token " + admin_token.key) + payload = {"configuration_permissions": [self._perm_id("add_user")]} + r = admin_client.patch(self._user_url(self.target.id), payload, format="json") + self.assertEqual(r.status_code, 200, r.content[:1000]) + self.assertTrue(self.target.user_permissions.filter(codename="add_user").exists()) diff --git a/unittests/test_migrate_endpoints_to_locations.py b/unittests/test_migrate_endpoints_to_locations.py new file mode 100644 index 00000000000..8079eb707c3 --- /dev/null +++ b/unittests/test_migrate_endpoints_to_locations.py @@ -0,0 +1,205 @@ +from io import StringIO +from unittest.mock import patch + +from django.core.management import call_command +from django.test import TestCase, override_settings + +from dojo.location.models import Location +from dojo.models import Endpoint, Product, Product_Type +from dojo.tags.utils import bulk_add_tag_mapping +from dojo.url.models import URL + + +@override_settings(V3_FEATURE_LOCATIONS=True) +class MigrateEndpointsToLocationsTest(TestCase): + def setUp(self): + product_type = Product_Type.objects.create(name="Endpoint migration product type") + self.product = Product.objects.create( + name="Endpoint migration product", + description="Test product", + prod_type=product_type, + ) + + def _make_endpoint(self, host, tags): + with Endpoint.allow_endpoint_init(): + endpoint = Endpoint.objects.create( + protocol="https", + host=host, + product=self.product, + ) + endpoint.tags.add(*tags) + return endpoint + + def test_endpoint_tags_are_copied_in_deduplicated_batches(self): + # Four legacy Endpoints resolve to one Location. With batches of three, + # at least one batch contains that Location more than once regardless of + # database row order, so this exercises in-batch deduplication without + # relying on an implicit queryset order. + # Seed the Product tag before enabling propagation so setup does not + # dispatch an unrelated asynchronous inheritance task. + self.product.tags.add("product-inherited") + self.product.enable_product_tag_inheritance = True + self.product.save(update_fields=["enable_product_tag_inheritance"]) + self._make_endpoint("shared.example.com", ["shared", "secondary"]) + self._make_endpoint("shared.example.com", ["shared"]) + self._make_endpoint("shared.example.com", ["shared"]) + self._make_endpoint("shared.example.com", ["shared"]) + self._make_endpoint("unique.example.com", ["shared"]) + + with patch( + "dojo.management.commands.migrate_endpoints_to_locations.bulk_add_tag_mapping", + wraps=bulk_add_tag_mapping, + ) as bulk_add: + call_command( + "migrate_endpoints_to_locations", + batch_size=3, + progress_every=100, + stdout=StringIO(), + ) + + self.assertEqual(bulk_add.call_count, 2) + shared_locations_written = sum( + len(call.args[0]["shared"]) for call in bulk_add.call_args_list + ) + self.assertEqual(shared_locations_written, 3) + + shared_location = URL.objects.get(host="shared.example.com").location + unique_location = URL.objects.get(host="unique.example.com").location + self.assertCountEqual( + [tag.name for tag in shared_location.tags.all()], + ["shared", "secondary", "product-inherited"], + ) + self.assertCountEqual( + [tag.name for tag in unique_location.tags.all()], + ["shared", "product-inherited"], + ) + self.assertEqual( + [tag.name for tag in shared_location.inherited_tags.all()], + ["product-inherited"], + ) + self.assertEqual( + [tag.name for tag in unique_location.inherited_tags.all()], + ["product-inherited"], + ) + + tag_model = Location.tags.tag_model + self.assertEqual(tag_model.objects.get(name="shared").count, 2) + self.assertEqual(tag_model.objects.get(name="secondary").count, 1) + self.assertEqual(tag_model.objects.get(name="product-inherited").count, 2) + + # The management command is designed to be safely rerunnable after a + # partial migration. Existing relationships and Tagulous counts must + # remain unchanged on a second pass. + call_command( + "migrate_endpoints_to_locations", + batch_size=3, + progress_every=100, + stdout=StringIO(), + ) + + self.assertEqual(Location.tags.through.objects.count(), 5) + self.assertEqual(Location.inherited_tags.through.objects.count(), 2) + self.assertEqual(tag_model.objects.get(name="shared").count, 2) + self.assertEqual(tag_model.objects.get(name="secondary").count, 1) + self.assertEqual(tag_model.objects.get(name="product-inherited").count, 2) + + def test_failed_batch_is_atomic_and_retried_per_endpoint(self): + self._make_endpoint("first.example.com", ["first-tag"]) + self._make_endpoint("second.example.com", ["second-tag"]) + + through_model = Location.tags.through + rows_after_batch_write = [] + rows_before_fallback = [] + call_count = 0 + + def write_then_fail_once(tag_to_locations, **kwargs): + nonlocal call_count + call_count += 1 + if call_count == 1: + bulk_add_tag_mapping(tag_to_locations, **kwargs) + rows_after_batch_write.append(through_model.objects.count()) + msg = "simulated failure after the batch write" + raise RuntimeError(msg) + rows_before_fallback.append(through_model.objects.count()) + return bulk_add_tag_mapping(tag_to_locations, **kwargs) + + with ( + patch( + "dojo.management.commands.migrate_endpoints_to_locations.bulk_add_tag_mapping", + side_effect=write_then_fail_once, + ), + self.assertLogs( + "dojo.management.commands.migrate_endpoints_to_locations", + level="ERROR", + ) as logs, + ): + call_command( + "migrate_endpoints_to_locations", + batch_size=2, + progress_every=100, + stdout=StringIO(), + ) + + # The outer transaction rolls back the completed batch before the + # command retries its two source Endpoints independently. + self.assertEqual(rows_after_batch_write, [2]) + self.assertEqual(rows_before_fallback[0], 0) + self.assertEqual(through_model.objects.count(), 2) + self.assertTrue(any("retrying one endpoint at a time" in line for line in logs.output)) + + tag_model = Location.tags.tag_model + self.assertEqual(tag_model.objects.get(name="first-tag").count, 1) + self.assertEqual(tag_model.objects.get(name="second-tag").count, 1) + + def test_failed_endpoint_tag_retry_is_reported_and_rerunnable(self): + failing_endpoint = self._make_endpoint("failing.example.com", ["failing-tag"]) + self._make_endpoint("healthy.example.com", ["healthy-tag"]) + + def fail_one_tag(tag_to_locations, **kwargs): + if "failing-tag" in tag_to_locations: + msg = "simulated invalid endpoint tag" + raise RuntimeError(msg) + return bulk_add_tag_mapping(tag_to_locations, **kwargs) + + stdout = StringIO() + with ( + patch( + "dojo.management.commands.migrate_endpoints_to_locations.bulk_add_tag_mapping", + side_effect=fail_one_tag, + ), + self.assertLogs( + "dojo.management.commands.migrate_endpoints_to_locations", + level="ERROR", + ), + ): + call_command( + "migrate_endpoints_to_locations", + batch_size=2, + progress_every=100, + stdout=stdout, + ) + + self.assertIn("Migrated 1/2 endpoints", stdout.getvalue()) + self.assertIn("1 endpoint(s) failed", stdout.getvalue()) + self.assertIn(str(failing_endpoint.id), stdout.getvalue()) + + failing_location = URL.objects.get(host="failing.example.com").location + healthy_location = URL.objects.get(host="healthy.example.com").location + self.assertEqual([tag.name for tag in failing_location.tags.all()], []) + self.assertEqual([tag.name for tag in healthy_location.tags.all()], ["healthy-tag"]) + + # A clean rerun fills only the previously failed relationship and does + # not increment the existing healthy relationship's Tagulous count. + rerun_stdout = StringIO() + call_command( + "migrate_endpoints_to_locations", + batch_size=2, + progress_every=100, + stdout=rerun_stdout, + ) + + self.assertNotIn("endpoint(s) failed", rerun_stdout.getvalue()) + self.assertEqual([tag.name for tag in failing_location.tags.all()], ["failing-tag"]) + tag_model = Location.tags.tag_model + self.assertEqual(tag_model.objects.get(name="failing-tag").count, 1) + self.assertEqual(tag_model.objects.get(name="healthy-tag").count, 1) diff --git a/unittests/test_product_type_authorized_users_api_authz.py b/unittests/test_product_type_authorized_users_api_authz.py new file mode 100644 index 00000000000..e0f3e8c1818 --- /dev/null +++ b/unittests/test_product_type_authorized_users_api_authz.py @@ -0,0 +1,127 @@ +from django.urls import reverse +from rest_framework.authtoken.models import Token +from rest_framework.test import APIClient + +from dojo.models import Dojo_User, Product_Type, User +from unittests.dojo_test_case import DojoAPITestCase, versioned_fixtures + + +@versioned_fixtures +class ProductTypeAuthorizedUsersApiPermissionTest(DojoAPITestCase): + + """ + Authorization coverage for the ``authorized_users`` field on the Product + Type API: changing it requires ``Product_Type_Manage_Members``; all other + fields require ``Product_Type_Edit``; no-op submissions are unaffected. + Mirrors unittests.test_product_authorized_users_api_authz. + """ + + fixtures = ["dojo_testdata.json"] + + @classmethod + def setUpTestData(cls): + cls.product_type = Product_Type.objects.create(name="PTAU-Perm PT") + + # Alice: holds Product_Type_Edit on the product type (via authorized_users membership). + cls.alice = User.objects.create_user( + username="ptau_alice", + password="not-a-real-secret", # noqa: S106 - test fixture user + is_staff=False, + ) + cls.product_type.authorized_users.add(Dojo_User.objects.get(pk=cls.alice.pk)) + + # Bob: another user, not initially a member of the product type. + cls.bob = User.objects.create_user( + username="ptau_bob", + password="not-a-real-secret", # noqa: S106 - test fixture user + is_staff=False, + ) + + # Staff user, who holds Product_Type_Manage_Members. + cls.admin = User.objects.create_user( + username="ptau_admin", + password="not-a-real-secret", # noqa: S106 - test fixture user + is_staff=True, + ) + + cls.detail_url = reverse("product_type-detail", args=[cls.product_type.id]) + + def _client_for(self, user): + token, _ = Token.objects.get_or_create(user=user) + client = APIClient() + client.credentials(HTTP_AUTHORIZATION="Token " + token.key) + return client + + def test_product_type_edit_cannot_add_authorized_users(self): + client = self._client_for(self.alice) + + # Product_Type_Edit alone permits ordinary field updates. + response = client.patch( + self.detail_url, {"description": "edited via product type edit"}, format="json", secure=True, + ) + self.assertEqual(200, response.status_code, response.content[:500]) + + # Adding a user to authorized_users requires Product_Type_Manage_Members. + self.assertFalse(self.product_type.authorized_users.filter(pk=self.bob.pk).exists()) + response = client.patch( + self.detail_url, + {"authorized_users": [self.alice.pk, self.bob.pk]}, + format="json", secure=True, + ) + self.assertEqual(403, response.status_code, response.content[:500]) + + self.product_type.refresh_from_db() + self.assertFalse(self.product_type.authorized_users.filter(pk=self.bob.pk).exists()) + self.assertTrue(self.product_type.authorized_users.filter(pk=self.alice.pk).exists()) + + def test_product_type_edit_cannot_replace_authorized_users(self): + # PATCH replaces the M2M via .set(); replacing the list is also a + # member-management change and requires Product_Type_Manage_Members. + client = self._client_for(self.alice) + response = client.patch( + self.detail_url, {"authorized_users": [self.bob.pk]}, format="json", secure=True, + ) + self.assertEqual(403, response.status_code, response.content[:500]) + + self.product_type.refresh_from_db() + self.assertTrue(self.product_type.authorized_users.filter(pk=self.alice.pk).exists()) + self.assertFalse(self.product_type.authorized_users.filter(pk=self.bob.pk).exists()) + + def test_product_type_edit_unchanged_authorized_users_is_allowed(self): + # Replay-safe: re-submitting the current membership set unchanged is not + # a member-management change and is accepted. + client = self._client_for(self.alice) + response = client.patch( + self.detail_url, {"authorized_users": [self.alice.pk]}, format="json", secure=True, + ) + self.assertEqual(200, response.status_code, response.content[:500]) + + def test_non_member_cannot_change_authorized_users(self): + # A user who is not a member of the product type cannot retrieve it (the + # viewset queryset is scoped to authorized product types), so the PATCH + # is rejected before the member-management check is reached. + client = self._client_for(self.bob) + + response = client.get(self.detail_url, secure=True) + self.assertIn(response.status_code, {403, 404}, response.content[:500]) + + response = client.patch( + self.detail_url, {"authorized_users": [self.bob.pk]}, format="json", secure=True, + ) + self.assertIn(response.status_code, {403, 404}, response.content[:500]) + + self.product_type.refresh_from_db() + self.assertFalse(self.product_type.authorized_users.filter(pk=self.bob.pk).exists()) + + def test_manage_members_permission_can_change_authorized_users(self): + # Product_Type_Manage_Members (staff) can update the membership list. + client = self._client_for(self.admin) + response = client.patch( + self.detail_url, + {"authorized_users": [self.alice.pk, self.bob.pk]}, + format="json", secure=True, + ) + self.assertEqual(200, response.status_code, response.content[:500]) + + self.product_type.refresh_from_db() + self.assertTrue(self.product_type.authorized_users.filter(pk=self.bob.pk).exists()) diff --git a/unittests/test_rest_framework.py b/unittests/test_rest_framework.py index c0ce346bfa4..892b6533dd4 100644 --- a/unittests/test_rest_framework.py +++ b/unittests/test_rest_framework.py @@ -4168,3 +4168,35 @@ def __init__(self, *args, **kwargs): self.test_type = TestType.STANDARD self.deleted_objects = 1 BaseClass.RESTEndpointTest.__init__(self, *args, **kwargs) + + +class OpenAPISchemaGenerationTest(DojoAPITestCase): + + """ + Render the full OpenAPI v3 schema and gate that generation is error-free. + + drf-spectacular does not raise when it fails to introspect a view or + serializer -- it logs an error and drops the operation from the schema, so a + broken endpoint (e.g. a serializer whose field querysets raise for the + unauthenticated request used at schema time) silently ships incomplete API + docs and can trip downstream postprocessing. Fail the build if schema + generation produces any error. + """ + + def test_schema_generation_produces_no_errors(self): + GENERATOR_STATS.reset() + generator = spectacular_settings.DEFAULT_GENERATOR_CLASS() + schema = generator.get_schema(request=None, public=True) + + # Structurally valid per the OpenAPI 3 spec ... + validate_schema(schema) + + # ... and generated without drf-spectacular logging any error (a logged + # error means an operation was silently dropped from the schema). + errors = list(GENERATOR_STATS._error_cache) + self.assertEqual( + errors, + [], + f"OpenAPI schema generation produced {len(errors)} error(s):\n" + + "\n".join(str(error) for error in errors), + ) diff --git a/unittests/test_v3_location_reference_authz.py b/unittests/test_v3_location_reference_authz.py new file mode 100644 index 00000000000..fc9dc7d6a37 --- /dev/null +++ b/unittests/test_v3_location_reference_authz.py @@ -0,0 +1,147 @@ +""" +Regression tests: the V3 location/endpoint reference write paths limit the +references a request may select to the objects the requesting user is authorized +for. Each test asserts an authorized reference is accepted and an unauthorized +one is rejected, for the Finding endpoints field, the import endpoint_to_add +field, and bulk-mitigate reference updates. +""" +from django.core.files.uploadedfile import SimpleUploadedFile +from django.urls import reverse +from django.utils.timezone import now +from rest_framework.exceptions import ValidationError as DRFValidationError +from rest_framework.test import APIRequestFactory + +from dojo.api_v2.serializers import ImportScanSerializer +from dojo.authorization.roles_permissions import Roles +from dojo.location.models import ( + LocationFindingReference, + LocationProductReference, +) +from dojo.location.status import FindingLocationStatus +from dojo.models import ( + Dojo_User, + Engagement, + Finding, + Product, + Product_Member, + Product_Type, + Role, + Test, + Test_Type, +) +from dojo.url.models import URL +from unittests.dojo_test_case import DojoAPITestCase, skip_unless_v3 + +GENERIC_FINDINGS = b'{"findings": [{"title": "probe", "severity": "Info", "description": "probe"}]}' + + +@skip_unless_v3 +class V3LocationReferenceAuthzTests(DojoAPITestCase): + + """Two products, two users; each user is authorized only for their own product.""" + + @classmethod + def setUpTestData(cls): + cls.pt = Product_Type.objects.create(name="v3_locref_pt") + cls.product_a = Product.objects.create(name="v3_locref_a", description="a", prod_type=cls.pt) + cls.product_b = Product.objects.create(name="v3_locref_b", description="b", prod_type=cls.pt) + + cls.user_a = Dojo_User.objects.create(username="v3_locref_user_a", is_active=True) + cls.user_b = Dojo_User.objects.create(username="v3_locref_user_b", is_active=True) + # Grant membership two ways so the test is authorization-backend agnostic: + # legacy authorized_users (honored by the OS backend) and a Product_Member + # Owner role (honored by the Pro backend). + owner = Role.objects.get(id=Roles.Owner) + for product, user in ((cls.product_a, cls.user_a), (cls.product_b, cls.user_b)): + product.authorized_users.add(user) + Product_Member.objects.create(product=product, user=user, role=owner) + + cls.test_type, _ = Test_Type.objects.get_or_create(name="v3_locref_scan") + + cls.finding_a = cls._make_finding(cls.product_a, cls.user_a, "finding_a") + cls.engagement_a = cls.finding_a.test.engagement + cls.finding_b = cls._make_finding(cls.product_b, cls.user_b, "finding_b") + + # A location on product_b (authorized only for user_b). + url_b = URL.get_or_create_from_object(URL.from_value("https://b.example.test/x")) + cls.lfr_b = url_b.location.associate_with_finding(cls.finding_b) + cls.location_b = url_b.location + + # A location on product_a (authorized for user_a) for the accepted-case controls. + cls.finding_a_extra = cls._make_finding(cls.product_a, cls.user_a, "finding_a_extra") + url_a = URL.get_or_create_from_object(URL.from_value("https://a.example.test/ok")) + cls.lfr_a = url_a.location.associate_with_finding(cls.finding_a_extra) + cls.location_a = url_a.location + + # A location associated with both products, with an active reference from product_b. + url_shared = URL.get_or_create_from_object(URL.from_value("https://shared.example.test/s")) + cls.location_shared = url_shared.location + cls.location_shared.associate_with_product(cls.product_a) + cls.lfr_shared_b = LocationFindingReference.objects.create( + location=cls.location_shared, finding=cls.finding_b, status=FindingLocationStatus.Active, + ) + + @classmethod + def _make_finding(cls, product, user, title): + engagement = Engagement.objects.create(name=title + "_eng", product=product, target_start=now(), target_end=now()) + test = Test.objects.create(engagement=engagement, test_type=cls.test_type, target_start=now(), target_end=now()) + return Finding.objects.create( + test=test, title=title, description="x", severity="High", + numerical_severity="S0", active=True, verified=True, reporter=user, + ) + + # Finding endpoints field + def test_finding_endpoints_rejects_unauthorized_reference(self): + self.client.force_authenticate(user=self.user_a) + response = self.client.patch( + reverse("finding-list") + f"{self.finding_a.id}/", + {"endpoints": [self.lfr_b.id], "push_to_jira": False}, format="json", secure=True, + ) + self.assertEqual(response.status_code, 400) + self.assertFalse( + LocationProductReference.objects.filter(location=self.location_b, product=self.product_a).exists(), + ) + + def test_finding_endpoints_allows_authorized_reference(self): + self.client.force_authenticate(user=self.user_a) + response = self.client.patch( + reverse("finding-list") + f"{self.finding_a.id}/", + {"endpoints": [self.lfr_a.id], "push_to_jira": False}, format="json", secure=True, + ) + self.assertEqual(response.status_code, 200) + + # import endpoint_to_add + def test_import_endpoint_to_add_rejects_unauthorized_reference(self): + self.client.force_authenticate(user=self.user_a) + response = self.client.post(reverse("importscan-list"), { + "engagement": self.engagement_a.id, + "scan_type": "Generic Findings Import", + "endpoint_to_add": self.location_b.id, + "file": SimpleUploadedFile("s.json", GENERIC_FINDINGS, content_type="application/json"), + }, secure=True) + self.assertEqual(response.status_code, 400) + self.assertFalse( + LocationProductReference.objects.filter(location=self.location_b, product=self.product_a).exists(), + ) + + def test_import_endpoint_to_add_allows_authorized_reference(self): + # Checked at the field level so the assertion is about the queryset scoping, + # not the downstream import pipeline. + request = APIRequestFactory().post(reverse("importscan-list")) + request.user = self.user_a + field = ImportScanSerializer(context={"request": request}).fields["endpoint_to_add"] + self.assertEqual(field.to_internal_value(self.location_a.id), self.location_a) + with self.assertRaises(DRFValidationError): + field.to_internal_value(self.location_b.id) + + # bulk mitigate reference updates + def test_bulk_mitigate_scopes_reference_updates(self): + self.client.force_login(self.user_a) + response = self.client.post( + reverse("endpoints_bulk_update_all_product", args=(self.product_a.id,)), + data={"endpoints_to_update": self.location_shared.id}, secure=True, + ) + self.assertIn(response.status_code, (200, 302)) + self.lfr_shared_b.refresh_from_db() + # A reference outside the acting product is left unchanged. + self.assertEqual(self.lfr_shared_b.status, FindingLocationStatus.Active) diff --git a/unittests/tools/test_fortify_parser.py b/unittests/tools/test_fortify_parser.py index cef5b56b1bd..c5cc9b12eca 100644 --- a/unittests/tools/test_fortify_parser.py +++ b/unittests/tools/test_fortify_parser.py @@ -1,6 +1,6 @@ from dojo.models import Test -from dojo.tools.fortify.parser import FortifyParser +from dojo.tools.fortify.parser import FortifyParser, FortifyParserV2 from unittests.dojo_test_case import DojoTestCase, get_unit_tests_scans_path @@ -189,3 +189,65 @@ def test_fortify_hello_world_fpr_rule_without_metainfo(self): self.assertEqual("D3166922519EDD92D132761602EB71B4", finding.unique_id_from_tool) self.assertEqual("src/main/java/hello/HelloWorld.java", finding.file_path) self.assertEqual(13, finding.line) + + +class TestFortifyV2Parser(DojoTestCase): + + """ + Regression: the v1 FPR parser stores the snippet context StartLine (Fortify's + reported line minus up to 3 context lines) instead of the reported SourceLocation + line, which breaks file_path + line deduplication against other tools. The + "Fortify Scan v2" scan type stores the reported line; v1 is intentionally left + unchanged so existing hashcodes are unaffected. + """ + + def test_fortify_v2_scan_type(self): + self.assertEqual(["Fortify Scan v2"], FortifyParserV2().get_scan_types()) + + def test_fortify_v2_fpr_uses_true_source_location_line(self): + with (get_unit_tests_scans_path("fortify") / "many_findings.fpr").open(encoding="utf-8") as testfile: + parser = FortifyParserV2() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(61, len(findings)) + self.validate_locations(findings) + finding = findings[0] + # control: the description records Fortify's reported line verbatim + self.assertIn("**SourceLocationLine:** 222", finding.description) + # the stored line must match the reported line, not the snippet start (219) + self.assertEqual( + 222, finding.line, + msg=f"expected line=222 (SourceLocationLine), stored line={finding.line}", + ) + self.assertEqual("Cross-Site Request Forgery - category.html: 222 (114E5A67-3446-4DD5-B578-D0E6FDBB304E)", finding.title) + + def test_fortify_v2_fpr_clamped_snippet(self): + with (get_unit_tests_scans_path("fortify") / "hello_world.fpr").open(encoding="utf-8") as testfile: + parser = FortifyParserV2() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(4, len(findings)) + with self.subTest(i=0): + finding = findings[0] + self.assertIn("**SourceLocationLine:** 8", finding.description) + self.assertEqual( + 8, finding.line, + msg=f"expected line=8 (SourceLocationLine), stored line={finding.line}", + ) + with self.subTest(i=2): + # near the top of a file the snippet start clamps to 1, so the v1 + # offset is not even constant: reported line 3, snippet start 1 + finding = findings[2] + self.assertIn("**SourceLocationLine:** 3", finding.description) + self.assertEqual( + 3, finding.line, + msg=f"expected line=3 (SourceLocationLine), stored line={finding.line}", + ) + + def test_fortify_v2_xml_unchanged_from_v1(self): + # the XML report path already uses the reported line in v1; v2 must match + with (get_unit_tests_scans_path("fortify") / "fortify_many_findings.xml").open(encoding="utf-8") as testfile: + parser = FortifyParserV2() + findings = parser.get_findings(testfile, Test()) + self.assertEqual(324, len(findings)) + finding = findings[0] + self.assertEqual("src/main/java/org/joychou/controller/XXE.java", finding.file_path) + self.assertEqual(81, finding.line)