diff --git a/components/package.json b/components/package.json
index fb47c025484..f960d3ac200 100644
--- a/components/package.json
+++ b/components/package.json
@@ -1,6 +1,6 @@
{
"name": "defectdojo",
- "version": "3.4.303",
+ "version": "3.4.304",
"license": "BSD-3-Clause",
"private": true,
"dependencies": {
diff --git a/docs/content/admin/feature_flags/PRO__feature_flags.md b/docs/content/admin/feature_flags/PRO__feature_flags.md
index 9911a0ea8dd..bb1561b4d0c 100644
--- a/docs/content/admin/feature_flags/PRO__feature_flags.md
+++ b/docs/content/admin/feature_flags/PRO__feature_flags.md
@@ -13,14 +13,23 @@ The Feature Flags page is visible to **superusers** only. Other users, including
Go to **Settings > Feature Flags** in the left sidebar.
-The page lists every optional feature with:
+The header shows how many features are on, for example **26 of 56 Enabled**, and holds a search box that filters every feature by name or description.
-* **Name** — the feature, with a **BETA** tag when it is still in beta
-* **Description** — what the feature does
+Features are grouped by area, one card per area: **Findings and Triage**, **Assets and Locations** (or **Products and Locations** when relabeling is off), **Compliance and Regulatory**, **Connectors and Ingestion**, **AI and MCP**, **Interface and Workspace**, and **System and Support**. Each card shows how many of its features are listed, and the **MCP Server** row closes the **AI and MCP** card with its toolsets under it.
+
+Each feature row shows:
+
+* **Name**: the feature, with **NEW**, **BETA** and warning tags such as **Restart Recommended** where they apply
* **Documentation link**: a link to that feature's documentation. Every feature on the page has one, and the [Feature reference](#feature-reference) below lists them all.
-* **Toggle** — whether the feature is currently on
+* **Description**: what the feature does, on one line. Hover the row, or move keyboard focus into it, to read the whole description in place. On a touch screen the whole description always shows.
+* **Toggle**: whether the feature is currently on
+
+The column beside the cards has two lists:
+
+* **Areas** lists every area with how many of its features are on. Click an area to scroll to its card.
+* **Show** narrows every card to **All**, **On**, **Off**, **New**, **Beta**, or **With Warnings** (features that carry a warning tag, such as **Restart Recommended** or **Experimental**). Each choice shows how many features it matches, and it combines with the search box. While a choice other than **All** is active, its name shows in the **Show** header.
-Use the search box to filter the list by feature name or description.
+On a narrow screen the two lists sit above the cards and start folded, so the features come first.
### Features that are not listed
@@ -132,6 +141,7 @@ Features under **MCP** appear on the Feature Flags page beneath the MCP server s
| **AI Inventory and Governance** (BETA) | Inventory the AI in every scanned repository: coding assistants and their rules files, MCP servers, models, AI packages, AI service endpoints, agent skills and AI provider keys. Policies mark each component authorized, unauthorized or needs review, unauthorized components become Findings, and each Asset can export an AI bill of materials. Inventories that arrive while this is off are still stored and appear when it is turned on. Requires **Locations**. | Off | [Docs](/asset_modelling/locations/pro__ai_inventory/) |
| **AI Threat Modeling** | Generate a STRIDE threat model, attacker profiles, attack paths and security requirements from a feature design with Sensei. | On | [Docs](/sensei/threat_modeling/) |
| **Airgapped instance** | This instance has no route off its network, so it never contacts DefectDojo. The version check, external tools, announcements, threat intel downloads and error and performance diagnostics stop, and both support pages show the DefectDojo support address instead. Load threat intel bundles from a file. The DD_AIRGAPPED environment variable turns this on for the whole deployment. | Off | [Docs](/get_started/pro/onprem/airgapped/) |
+| **Asset Discovery** (BETA) | List the devices your EDR sees on the network that have no agent of its own, as CrowdStrike Falcon Discover and Microsoft Defender for Endpoint report them. Turn it on per connection with Report Unmanaged Devices. The devices are candidates: none becomes an Asset, and none counts toward Total Findings. | Off | [Docs](/asset_modelling/pro__unmanaged_devices/) |
| **Asset Exposure** (BETA) | Compute whether an asset is reachable from outside, and whether a finding's code is actually deployed in production, from the evidence connectors already collect. Both feed prioritization; exposure can also drive the FedRAMP VDR SLA tiers. | Off | [Docs](/triage_findings/finding_scoring/asset_exposure/) |
| **Asset Identity** | Let connectors find each Asset by the identifier its source uses instead of by name, and list every identifier it is known by on its Identity card. Records that are already mapped stay where they are. Turning this off goes back to matching by name; the identifiers are kept. | Off | [Docs](/asset_modelling/engagements_tests/pro__assets/#asset-identity-aliases) |
| **Calendar** | Show the Calendar in the sidebar. The calendar plots engagements and tests on a month view. Turning this off removes the entry; it does not change any engagement or test dates. | On | [Docs](/navigation/pro__sidebar/) |
@@ -146,6 +156,7 @@ Features under **MCP** appear on the Feature Flags page beneath the MCP server s
| **Customizable Page Layouts** | Customizable widget-grid layouts for the Organization, Asset, Engagement, Test, Finding, and Risk Acceptance view pages. | On | [Docs](/pro/page_layouts/) |
| **Dashboards 2.0** | The redesigned customizable widget dashboard. | Off | [Docs](/metrics_reports/dashboards/custom-dashboards/) |
| **EU Evidence Packs** (BETA) | Assess product evidence against authored CRA and DORA obligation catalogs, preserve assessor overrides, and generate dated evidence packs. Requires **Compliance**. | Off | [Docs](/federal_compliance/eu_evidence_packs/) |
+| **External Asset Discovery** (BETA) | Show the domains, IP addresses and certificates your scans and connectors report as Locations, on the location map and in the location lists. Locations that arrive while this is off are still stored and appear when it is turned on. Requires **Locations**. | Off | [Docs](/asset_modelling/locations/pro__attack_surface_locations/) |
| **FDA Cyber Device Pack** (BETA) | Assess a cyber device against an authored catalog for section 524B of the Federal Food, Drug, and Cosmetic Act, and generate an evidence pack carrying the component inventory, the lifecycle metrics and the exported bill of materials. Requires **Compliance**. | Off | [Docs](/federal_compliance/fda_cyber_device_pack/) |
| **Framework Presets** (BETA) | Apply a published framework's patch windows and scan cadence as one preset: preview the plan, apply it, revert it, and see when the applied settings drift. Ships with UK Cyber Essentials and the Australian Essential Eight. | Off | [Docs](/asset_modelling/pro_hierarchy/framework_presets/) |
| **Global Component Deduplication** | Deduplicate Components globally across the instance instead of per Asset. | Off | [Docs](/triage_findings/finding_deduplication/pro__global_component_deduplication/) |
@@ -176,7 +187,6 @@ Features under **MCP** appear on the Feature Flags page beneath the MCP server s
| **Smart Upload** | Upload scan results without naming the asset or engagement first, and triage what could not be matched from the Unassigned Findings queue. Turning this off removes Smart Upload and Unassigned Findings from the Connect menu. It does not delete anything, and any findings already imported through Smart Upload are unaffected. | On | [Docs](/import_data/pro/specialized_import/smart_upload/) |
| **Support** | Open a support request, track its status, and vote on what other customers ask for. When off, the Support pages are hidden and this instance sends no support requests to DefectDojo. | On | [Docs](/navigation/pro__support/) |
| **Triage Engine (formerly Rules Engine 2.0)** (BETA) | Build automation rules as visual node graphs that react to Finding events, with per-run traces and a delivery outbox. | Off | [Docs](/automation/triage_engine/about/) |
-| **Unmanaged Devices** (BETA) | List the devices your EDR sees on the network that have no agent of its own, as CrowdStrike Falcon Discover and Microsoft Defender for Endpoint report them. Turn it on per connection with Report Unmanaged Devices. The devices are candidates: none becomes an Asset, and none counts toward Total Findings. | Off | [Docs](/asset_modelling/pro__unmanaged_devices/) |
| **Vulnerability Response Policies** (BETA) | Read the response-policy matrix, the unevaluated queue and the escalation queue, and export machine-readable response evidence. | Off | [Docs](/asset_modelling/pro_hierarchy/priority_sla/#vulnerability-response-policies) |
## Feature availability
diff --git a/docs/content/admin/notifications/about_notifications.md b/docs/content/admin/notifications/about_notifications.md
index 51a63c39617..d5c90deff85 100644
--- a/docs/content/admin/notifications/about_notifications.md
+++ b/docs/content/admin/notifications/about_notifications.md
@@ -127,6 +127,18 @@ None of these messages arrives once per run. A Connector with a dead credential
The notification is on **🔔 Alerts** by default. Set it to Email, Slack or Teams on your notification settings page, under **Connections**. A Connector that is set up correctly but has never seen any data is a separate case, and it warns you when you save it rather than later.
+## Ingestion Health Notifications (Pro)
+
+With [Ingestion Health](/connectors/upstream/ingestion_health/) turned on, the **Ingestion Health Warning** notification covers the ways a source falls short that no other message reports:
+
+* **A Connector's or API import stream's volume drops.** A run succeeds and imports Findings, but far fewer than the source's 30-day baseline.
+* **An API import stream returns no Findings.** Its last import submitted none, against a baseline above zero.
+* **A source goes stale.** A Connector's or scheduled Triage Engine rule's schedule fired three times in a row without it running, or an API import stream has gone far longer than usual without a successful import (its imports stopped arriving, or keep failing).
+
+It is sent once per anomaly, never once per check, to superusers and to everyone who can see that source (the Connector View or Rule View permission, or for an API import stream, access to its Asset together with Connector View, which opens the page the message links to). A Connector's zero results and failed runs stay on the **Connector Health Warning** above, so no condition sends two messages.
+
+The notification is on **🔔 Alerts** by default. Set it to Email, Slack, Teams or a webhook on your notification settings page, under **Connections**; the setting is shown while Ingestion Health is on.
+
## Open-Source Considerations
### Specific overrides
diff --git a/docs/content/admin/sso/PRO__saml.md b/docs/content/admin/sso/PRO__saml.md
index e2171811c00..2c76f858576 100644
--- a/docs/content/admin/sso/PRO__saml.md
+++ b/docs/content/admin/sso/PRO__saml.md
@@ -48,9 +48,9 @@ A few things to know about this endpoint:
- **SAML Attribute** is free text and must match the attribute name your IdP actually emits. Some IdPs (e.g. Entra ID / Azure AD) send fully qualified claim URIs such as `http://schemas.microsoft.com/identity/claims/emailaddress` rather than friendly names. If you are not sure what your IdP sends, enable **Enable SAML Debugging** (see [Troubleshooting](#troubleshooting)) and inspect the assertion in the logs.
- **DefectDojo Field** is chosen from a list: **Username**, **First Name**, **Last Name** and **Email**.
- - At minimum, map the attribute that corresponds to **Username**. DefectDojo looks users up by username when matching SAML logins to existing accounts.
- - Mapping an attribute to **Email** is strongly recommended: DefectDojo uses the email address for notifications, and to match an incoming login to an existing account by email.
- - The same attribute may feed more than one field — for example an email claim used for both **Email** and **Username**. The reverse is not allowed: each DefectDojo field may be mapped from only one attribute.
+ - A mapping fills user fields. It never decides which account a login lands on: DefectDojo always signs the login in to the account whose username equals the SAML `NameID` (see [Which account a login lands on](#which-account-a-login-lands-on)). A **Username** mapping is not used for that.
+ - Mapping an attribute to **Email** is strongly recommended: DefectDojo uses the email address for notifications.
+ - The same attribute may feed more than one field. The reverse is not allowed: each DefectDojo field may be mapped from only one attribute.
- A row with only one half filled in is rejected on save, and the offending cell is highlighted. Rows you add but never fill in are discarded rather than treated as errors.
8. **IdP metadata** — supply one of the two, not both:
@@ -102,7 +102,7 @@ IAM Identity Center can act as the SAML IdP through a customer managed SAML 2.0
1. In the IAM Identity Center console, add a **customer managed** application of type **SAML 2.0**.
2. Under **IAM Identity Center metadata**, download the **IAM Identity Center SAML metadata file**. Upload it to DefectDojo as **Local SAML Metadata** (step 8 of [Setup](#setup)), which works whether or not the instance can reach AWS.
3. Under **Application metadata**, enter the values manually: set **Application ACS URL** to DefectDojo's [ACS URL](#acs-url-assertion-consumer-service) and **Application SAML audience** to the **Entity ID** you set in DefectDojo.
-4. Under **Attribute mappings**, map the subject and the attributes DefectDojo reads. For example, send `${user:email}` as the subject and as an `email` attribute, and `${user:givenName}` and `${user:familyName}` as first and last name. Then map those attribute names to **Username**, **Email**, **First Name** and **Last Name** in DefectDojo's **Attribute Mapping**.
+4. Under **Attribute mappings**, map the subject and the attributes DefectDojo reads. For example, send `${user:email}` as the subject and as an `email` attribute, and `${user:givenName}` and `${user:familyName}` as first and last name. Then map those attribute names to **Email**, **First Name** and **Last Name** in DefectDojo's **Attribute Mapping**. The subject is the `NameID`, so it decides which DefectDojo account each person signs in to.
5. For group mapping, add an attribute such as `groups` mapped to `${user:groups}`, and enter the same name as DefectDojo's **Group Name Attribute**. DefectDojo names its groups after the exact values in the assertion, so turn on **Enable SAML Debugging** for a test login and check what Identity Center sends before you write the **Group Limiter Regex Expression**.
6. Assign the users or groups who should have access to the application in IAM Identity Center.
@@ -140,6 +140,14 @@ DefectDojo Cloud does not have the same level of SAML customization as DefectDoj
If you need to match users on an attribute other than NameID (such as `uid` or `email`), configure your Identity Provider to send the desired value as the NameID rather than adjusting DefectDojo settings.
+### Which account a login lands on
+
+A SAML login lands on the DefectDojo account whose username equals the `NameID` in the assertion. A mapped attribute never changes that, including one mapped to **Username**: the Attribute Mapping only fills fields such as the email address and the names.
+
+- Have your IdP send each person's DefectDojo username as the `NameID`. Many organizations use the email address as the DefectDojo username and send the email as the `NameID`.
+- Use a persistent `NameID` format. **Transient** issues a new `NameID` on every login, so it never matches an existing account.
+- With **Create Unknown User** on, a `NameID` that matches no username creates a new account. A `NameID` that differs from the username a person already has therefore creates a second account rather than signing them in to the first.
+
## Troubleshooting
### SAML Debugging output
@@ -158,12 +166,12 @@ Turn this option **off** after you've finished troubleshooting — SAML debug lo
If the SAML assertion parses successfully (no XML or signature errors) but DefectDojo refuses the login, the most common cause is a **username mismatch** between the IdP and DefectDojo.
-DefectDojo looks up the user **by username** when matching a SAML login to an existing account. If the value your IdP sends as the `username` attribute does not match an existing DefectDojo user's username, the lookup fails — even though the rest of the assertion is valid.
+DefectDojo looks up the user **by the SAML `NameID`**, which must equal an existing DefectDojo username (see [Which account a login lands on](#which-account-a-login-lands-on)). If the `NameID` your IdP sends does not match, the lookup fails even though the rest of the assertion is valid. Mapping a different attribute to **Username** does not help: a mapped attribute is never used to find the account.
Two remedies, pick whichever fits your environment:
-- **Drop `username` from the Attribute Mapping** and let DefectDojo fall back to using the SAML `NameID` as the username instead. This is appropriate if your DefectDojo usernames already match the NameID format your IdP emits.
-- **Align the usernames.** Make sure the usernames in DefectDojo are exactly what your IdP sends in the `username` claim. For most organizations the easiest convention is to make DefectDojo usernames equal to the user's email address, and have the IdP send the email as the `username` claim.
+- **Change what the IdP sends as the `NameID`** so it equals the DefectDojo username, for example the user's email address.
+- **Align the usernames.** Make sure the usernames in DefectDojo are exactly the `NameID` values your IdP sends. For most organizations the easiest convention is to make DefectDojo usernames equal to the user's email address, and have the IdP send the email as the `NameID`.
If you're not sure what the IdP is actually sending, enable **Enable SAML Debugging** (above) and inspect the parsed attributes in the logs.
diff --git a/docs/content/admin/user_management/PRO__custom_rbac_roles.md b/docs/content/admin/user_management/PRO__custom_rbac_roles.md
index d025b642053..a3a61781b5e 100644
--- a/docs/content/admin/user_management/PRO__custom_rbac_roles.md
+++ b/docs/content/admin/user_management/PRO__custom_rbac_roles.md
@@ -109,6 +109,22 @@ Permissions are grouped into three tables plus a checklist.
| Mitigation Policy | Edit |
| Audit Log, Metering | View |
+**Configuration Permissions** open settings pages and instance-wide catalogs. Like the Global Feature Permissions, **they only take effect when the role is held as a Global Role**, and **only a Superuser can grant them**: a Global Owner can edit a custom role, but not add a row of this table to it, clone a role that carries one, or set such a role as a Global Role. The built-in roles carry none of them.
+
+| Row | Available actions |
+| --- | --- |
+| Users, Finding Templates, Note Types, Tool Types, Tool Configurations, SLA Configurations, Language Types, CI/CD Infrastructure, Jira Instances, Questionnaires, Prioritization Engines, Asset Platforms, Asset Lifecycles, Asset Origins | View, Add, Edit, Delete |
+| Groups | View, Add |
+| Environments, Regulations | Add, Edit, Delete |
+| Test Types | Add, Edit |
+| Questions | View, Add, Edit |
+| GitHub Configurations | View, Add, Delete |
+| Scheduling Service Schedules | View, Edit, Delete |
+| PSIRT | View, Edit |
+| Announcement, Login Banner | Edit |
+
+A row whose feature is turned off on your instance (Jira, GitHub, questionnaires, the Scheduling Service, PSIRT) is still listed, with a note that the grant has no effect until the feature is enabled. The [Configuration Permission Chart](../user_permission_chart/#configuration-permission-chart) says what each action opens.
+
**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.
@@ -130,7 +146,9 @@ Permissions are grouped into three tables plus a checklist.
| 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.
+In each row of the Object, Group & Member and Global Feature tables, 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.
+
+The **Configuration Permissions** table has no gate. Each cell is its own grant, so a role can carry **Add** on Users without **View**, and clearing one cell leaves the rest of the row as it is.
## Editing, cloning, and deleting
@@ -148,8 +166,8 @@ 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/). |
+| **Global Role on a user** | The **Global Roles** field on the user's form; a user may hold several, and they add up. Superusers only. See [Set a User's permissions](../set_user_permissions/). |
+| **Global Role on a group** | The **Global Roles** 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. |
@@ -170,6 +188,32 @@ Parts of the UI are gated on a minimum Global Role rather than on an individual
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/).
+## Configuration Permissions
+
+A user's Configuration Permissions are the union of what their own Global Roles carry and what the Global Roles of their Groups carry. The **Configuration Permissions** card on a User page or a Group page lists the result and is read-only. To grant a settings page to someone:
+
+1. Create a role for the settings pages (for example **Users Manager**), or open one that already carries them.
+2. Enable the rows you want in the **Configuration Permissions** table and save.
+3. Add the role under **Global Roles** on the **Edit User** form, beside the Global Role the user already holds, or under a Group's Global Roles on the **Edit Group** form, so every member holds it.
+
+Global Roles add up, so the role that opens the settings pages sits beside **Reader** or **Maintainer** rather than replacing it. One **Users Manager** role can serve every user who needs that page, whatever else they hold.
+
+Upgrading from a version where Configuration Permissions were set on the User and Group pages creates these roles for you: for every distinct set of Configuration Permissions someone held, one custom role carrying exactly that set, named after the areas it covers, such as **Users Manager** or **Configuration Viewer**, with a description listing them. Every user and every group that held the set gets that role added to their Global Roles; the Global Role they had before (Reader, Maintainer, Owner, or none) is left exactly as it was. The roles are ordinary custom roles: rename them, merge them, or give several holders one role, as you prefer. The old per-user and per-group permission forms no longer change anything and say so.
+
+An instance with its own authentication backend (a subclass of Django's `ModelBackend` named in `AUTHENTICATION_BACKENDS`) must give that class the `RoleAwarePermissionsMixin` from `pro.authorization.backends`, or `manage.py check` refuses to start: without it the backend would keep answering settings-page checks from the old per-user rows.
+
+## Seeing what a user or group holds
+
+Because Global Roles add up, the permissions someone actually holds can come from several roles at once, some of them through a Group. The **Permissions** column on the **All Users** and **All Groups** tables shows the result. Select **View** on a row to open the permission grid for that user or group, read-only:
+
+* Each Global Role the user or group holds is listed above the grid with a letter (**A**, **B**, **C** and on), and with how it is held: **Assigned directly**, or **Through group** followed by the group's name.
+* Every granted permission shows the letter of each role that grants it, side by side, so a permission that three roles grant shows three letters. Hovering it names every role behind it, and the group a role comes through.
+* An empty box is a permission none of the roles grant.
+
+The grid covers Global Roles only. A role held on a single Organization or Asset applies to that record alone and is listed on its Permissions dialog instead. A superuser holds every permission whatever roles they have, so their dialog says so instead of drawing the grid. An inactive user's dialog notes that none of the permissions apply until the user is reactivated.
+
+The column is offered to superusers and Global Owners, the same people who can read and assign Global Roles.
+
## 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.
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 dc75c659acd..1856d24964a 100644
--- a/docs/content/admin/user_management/about_perms_and_roles.md
+++ b/docs/content/admin/user_management/about_perms_and_roles.md
@@ -82,15 +82,21 @@ By default, the first account created on a new DefectDojo instance will have Sup
## Configuration Permissions
-Configuration Permissions, although similar, are not related to Assets or Roles. They must be assigned separately from Roles. **Regular** **users do not have any Configuration Permissions by default, and assigning these configuration permissions should be done carefully.**
+Configuration Permissions open settings pages and instance-wide catalogs (Users, Groups, Finding Templates, Tool Types, SLA Configurations and so on). They are not related to Assets or Organizations: a user can hold them without any Asset or Organization membership, and holding them gives no access to Assets, Organizations or Findings. **Regular users do not have any Configuration Permissions by default, and assigning them should be done carefully.**
-Users can have Configuration Permissions assigned in different ways:
+Configuration Permissions are granted through Roles. A Role carries them in the **Configuration Permissions** table of the role editor, and they take effect when that Role is held as a **Global Role**:
-1. Users can be assigned Configuration Permissions directly. Specific permissions can be configured directly on a User page.
+1. A user holds the Configuration Permissions of their own Global Role.
-2. User Groups can be assigned Configuration Permissions. As with Roles, specific Configuration Permissions can be added to Groups, which will give all Group members these permissions.
+2. A user also holds the Configuration Permissions of the Global Role of every Group they belong to, which is how a whole team is given access at once.
-Superusers have all Configuration Permissions, so they do not have a Configuration Permission section on their User page.
+Only a Superuser can add a Configuration Permission to a Role, clone a Role that carries one, or set such a Role as a Global Role. Superusers hold every Configuration Permission themselves.
+
+The **Configuration Permissions** card on a User page and on a Group page lists what the user or group holds and where it comes from; it is read-only. To change it, edit a custom Role (or clone one) on the **Settings > Users & Permissions > Roles** page and set it as the Global Role. See [Custom Roles](../pro__custom_rbac_roles/#configuration-permissions).
+
+The built-in Roles (Reader, Writer, Maintainer, Owner, API Importer) carry no Configuration Permissions. A Global Reader, for example, does not see the Users page until a Role that carries **Users: View** is their Global Role.
+
+A Global **Owner** keeps the built-in Owner Role, because the Owner-only abilities (managing Roles among them) belong to that one Role and a custom Role cannot stand in for it. To give an Owner a Configuration Permission, put the grant on a custom Role and set that Role as the Global Role of a Group the Owner belongs to: a user holds the Configuration Permissions of their own Global Role and of every Group's Global Role. When DefectDojo Pro upgrades an instance that still used the old per-user and per-group Configuration Permissions, every holder gets an equivalent custom Role as their Global Role, and an Owner gets a Group named **Configuration: ...** that carries their former set; a member added to an Owner Group later joins that Group too, or gets a Role of their own, and a member removed from the Owner Group keeps the set until they are removed from the **Configuration: ...** Group as well.
### Group Configuration Permissions
diff --git a/docs/content/admin/user_management/create_user_group.md b/docs/content/admin/user_management/create_user_group.md
index ca6a878e992..f0eb83c06dc 100644
--- a/docs/content/admin/user_management/create_user_group.md
+++ b/docs/content/admin/user_management/create_user_group.md
@@ -14,8 +14,8 @@ If you have a significant number of DefectDojo users, you may want to create one
Groups can work in multiple ways:
* Set one, or many different Asset or Organization level Roles for all Group Members, allowing specific control over which Assets or Organizations can be accessed and edited by the Group.
-* Set a Global Role for all Group Members, giving them visibility and access to all Asset or Organizations.
-* Set Configuration Permissions for a Group, allowing them to change specific functionality around DefectDojo.
+* Set Global Roles for all Group Members, giving them visibility and access to all Asset or Organizations.
+* Give a Group a Global Role that carries Configuration Permissions, beside its other Global Roles, allowing its Members to reach specific settings pages in DefectDojo.
For more information on Roles, please refer to our **Introduction To Roles** article.
@@ -32,17 +32,13 @@ Users with the **View Group** configuration permission see every Group. A Member
## Viewing A Group
-Viewing a group displays all Group information, such as ID, name, description, global role, etc. The Group Members, Organizations, and Assets associated with the group are also displayed. Additionally, configuration permissions tied to a Group can be updated directly from the “View Group” page.
+Viewing a group displays all Group information, such as ID, name, description, global roles, etc. The Group Members, Organizations, and Assets associated with the group are also displayed.

-* All configuration permissions are displayed in a dropdown which is grouped into subcategories. If the selection of configuration permissions is different from their current value, an “Update Configuration Permissions” button is displayed.
+* The **Configuration Permissions** card lists the Configuration Permissions the Group's Global Roles carry, grouped by area. It is read-only: to change them, a Superuser edits the Role on the **Roles** page or changes the Group's Global Roles.
* A Group's history, opened from the ⚙️ menu, is shown only to Users with the **View Group** configuration permission, because it also lists the Group's past Asset and Organization Roles.
-
-
-* Once a few additional permissions have been selected, the user will be asked to confirm they would like to update the permissions for the selected group before an update is made.
-
## Create a User Group
1. Navigate to **Settings > Users & Permissions > Groups** on the sidebar.
@@ -51,7 +47,7 @@ Viewing a group displays all Group information, such as ID, name, description, g
3. In the **New Group** window, set the Name for this Group, and add a Description if you wish.
- You can also select a Global Role that you wish to apply to this Group. Adding a Global Role to the Group will give all Group Members access to all DefectDojo data, along with a limited amount of edit access depending on the Global Role you choose. See our **Introduction To Roles** article for more information.
+ You can also select the Global Roles that you wish to apply to this Group. Adding a Global Role to the Group will give all Group Members access to all DefectDojo data, along with a limited amount of edit access depending on the Global Role you choose; several Global Roles add up. See our **Introduction To Roles** article for more information.
4. Click **Submit**.
@@ -59,7 +55,7 @@ Viewing a group displays all Group information, such as ID, name, description, g
The account that initially creates a Group will have an Owner Role for the Group by Default.
-To change a Group's Name, Description, email address or Global Role later, open the Group page, click the ⚙️ button in the top right corner, and select **Edit Group**.
+To change a Group's Name, Description, email address or Global Roles later, open the Group page, click the ⚙️ button in the top right corner, and select **Edit Group**.
### Set an email address to receive reports
@@ -130,7 +126,7 @@ In the **Members** table of the **Permissions for Group** window:
## Manage a Group's Permissions
-Note that only Superusers can edit a Group's permissions (Asset / Organization, or Configuration).
+Note that only Superusers can edit a Group's permissions (Asset / Organization, or the Global Roles that carry its Configuration Permissions).
### Add Asset Roles or Organization Roles for a Group
@@ -156,6 +152,9 @@ As with Group Members, a Group's Role on an Asset or Organization can be changed
### Assign Configuration Permissions to a Group
-If you want the Members in your Group to access Configuration functions, and control certain aspects of DefectDojo, you can assign these responsibilities from the Group page, using the **Configuration Permissions** drop\-down described in [Viewing A Group](#viewing-a-group).
+If you want the Members in your Group to access Configuration functions, and control certain aspects of DefectDojo, give the Group a Global Role that carries those Configuration Permissions. Every Member of the Group then holds them.
+
+1. On the **Settings > Users & Permissions > Roles** page, create a Role for the settings pages (or open one that already carries them) and enable the rows you want in its **Configuration Permissions** table. See [Custom Roles](../pro__custom_rbac_roles/#configuration-permissions).
+2. Open the Group, choose **Edit Group** from the ⚙️ menu, and add that Role under the Group's **Global Roles**. The Group's other Global Roles stay as they are.
-Select the permissions you want from the drop\-down, click **Update Configuration Permissions**, and confirm the change.
+The Group page's **Configuration Permissions** card shows the result. Only a Superuser can grant Configuration Permissions this way.
diff --git a/docs/content/admin/user_management/pro_permissions_overhaul.md b/docs/content/admin/user_management/pro_permissions_overhaul.md
index 65e8dab3892..71e5b102a32 100644
--- a/docs/content/admin/user_management/pro_permissions_overhaul.md
+++ b/docs/content/admin/user_management/pro_permissions_overhaul.md
@@ -47,8 +47,8 @@ When looking at Organization or Asset, you can open the Permissions window to se
2. Here, you can select a user or group to add to the Asset, and select the [Role](../about_perms_and_roles) that you want that user to have.
3. On the lower table, you can see a list of all users or groups who have access to this object. You can also quickly assign a new role for one of these users or groups from the drop-down menu.
-## Setting Configuration Permissions through the User view
+## Reading Configuration Permissions on the User view
-A user's configuration permissions can now be set in a more user-friendly approach. From the Users View, all configuration permissions are displayed in a dropdown, then grouped by the permission type. If the selection of configuration permissions is different from their current value, an “Update Configuration Permissions” button is displayed. When clicked, the user will be asked to confirm they would like to update the permissions for the selected group before an update is made.
+A user's Configuration Permissions come from Roles: the user's own Global Role and the Global Roles of their Groups. The **Configuration Permissions** card on the User page lists what the user holds, grouped by area, and says where it comes from. The card is read-only. To change what a user holds, a Superuser edits or clones a Role on the **Roles** page, enables rows in its **Configuration Permissions** table, and sets that Role as the user's Global Role. See [Custom Roles](../pro__custom_rbac_roles/#configuration-permissions).

diff --git a/docs/content/admin/user_management/set_user_permissions.md b/docs/content/admin/user_management/set_user_permissions.md
index 575cf3a5838..bcbd80c2a49 100644
--- a/docs/content/admin/user_management/set_user_permissions.md
+++ b/docs/content/admin/user_management/set_user_permissions.md
@@ -49,7 +49,7 @@ By default, the first account created on a new DefectDojo instance will have Sup
For Superuser Status, check off the ☑️ Superuser Status box, located in the user's Default Information.
-To assign a Global Role, select one from the dropdown Global Role menu at the bottom of the page.
+To assign Global Roles, pick them in the **Global Roles** field at the bottom of the page. A user may hold several, and they add up: a Maintainer who also holds a Role carrying Configuration Permissions keeps everything Maintainer grants and reaches the settings pages as well.

@@ -116,7 +116,7 @@ However, this does not work in reverse. If a User has an Organization Role or Gl
Many configuration dialogues and API endpoints can be enabled for users or groups of users, regardless of their superuser status. These Configuration Permissions allow regular users to access and contribute to parts of DefectDojo outside of their standard Asset or Asset Role assignment.
-Configuration Permissions are not related to a specific Asset or Organization \- users can have configuration permissions assigned without the need for other statuses or Asset / Organization Membership.
+Configuration Permissions are not related to a specific Asset or Organization \- users can hold them without the need for other statuses or Asset / Organization Membership. They are granted through Roles: a Role carries them in its **Configuration Permissions** table, and they take effect when that Role is held as a Global Role, by the user or by a Group the user belongs to.
### List of Configuration Permissions
@@ -129,7 +129,8 @@ Configuration Permissions are not related to a specific Asset or Organization \-
* **Login Banner**: Edit the **Settings > System > Login Banner** page
* **Announcements**: Access **Settings > System > Announcement Banner**
* **Note Types:** Access the **Settings > Configuration > Note Types** page
-* **Organizations:** n/a
+* **Organizations:** Create a new Organization (the **Add** action of the Organizations row in the role editor's Object Permissions table)
+* **Asset Platforms, Asset Lifecycles, Asset Origins:** Access the **Settings > Configuration > Asset Attributes** pages
* **Questionnaires**: Access the **Act > Explore > Surveys > Surveys** page
* **Questions**: Access the **Act > Explore > Surveys > Questions** page
* **Regulations**: Access the **Settings > Configuration > Regulations** page
@@ -141,16 +142,16 @@ Configuration Permissions are not related to a specific Asset or Organization \-
### Add Configuration Permissions to a User
-**Only Superusers can add Configuration Permissions to a User**.
+**Only Superusers can grant Configuration Permissions.** They are granted on a Role, and the Role is then added to the user's Global Roles (or to the Global Roles of a Group the user belongs to), beside whatever Global Role the user already holds.
-1. Navigate to the **Settings > Users & Permissions > Users** page on the sidebar. You will see a list of all registered accounts on DefectDojo, along with each account's Active status, Global Roles, and other relevant User data.
-
-
-
-2. Click the name of the account that you wish to edit.
+1. Navigate to **Settings > Users & Permissions > Roles** and create a Role for the settings pages (for example **Users Manager**), or open one that already carries them. See [Custom Roles](../pro__custom_rbac_roles/).
-3. Navigate to the Configuration Permissions List. This is located on the right\-hand side of the User Page.
+2. In the role editor, enable the rows you want in the **Configuration Permissions** table (for example **Users: View** and **Users: Add**) and save the Role.
-4. Select the User Configuration Permissions you wish to add.
+3. Navigate to **Settings > Users & Permissions > Users**, open the account you wish to edit, and add the Role under **Global Roles** on the **Edit User** form. The user's other Global Roles stay as they are.
+The **Configuration Permissions** card on the User page then lists what the user holds and where it comes from. It is read-only: to change it, edit the Role.
+
+To grant the same permissions to a whole team, add the Role to the Global Roles of a Group instead; every member of the Group holds them.
+
For a detailed breakdown of User Configuration Permissions, please refer to our [Permission Chart](../user_permission_chart/).
diff --git a/docs/content/admin/user_management/user_permission_chart.md b/docs/content/admin/user_management/user_permission_chart.md
index 363a82f8472..50685b6c0eb 100644
--- a/docs/content/admin/user_management/user_permission_chart.md
+++ b/docs/content/admin/user_management/user_permission_chart.md
@@ -58,11 +58,11 @@ The five roles below are DefectDojo Pro's **built-in roles**. They are locked pr
Each Configuration Permission refers to a particular function in the software, and has an associated set of actions a user can perform related to this function.
-The majority of Configuration Permissions give users access to certain pages in the UI.
+The majority of Configuration Permissions give users access to certain pages in the UI. They are granted on a Role, in the **Configuration Permissions** table of the role editor, and take effect when that Role is held as a Global Role (see [Custom Roles](../pro__custom_rbac_roles/#configuration-permissions)). Only a Superuser can grant them.
| **Configuration Permission** | **View ☑️** | **Add ☑️** | **Edit ☑️** | **Delete ☑️** |
| --- | --- | --- | --- | --- |
-| Credential Manager | Access the Credential Manager page | Add new entries to the Credential Manager | Edit Credential Manager entries | Delete Credential Manager entries |
+| Asset Platforms, Asset Lifecycles, Asset Origins | Access the **Settings > Configuration > Asset Attributes** pages | Add a new attribute option | Edit an existing attribute option | Delete an attribute option |
| Development Environments | n/a | Add new Development Environments to the **Settings > Configuration > Environments** list | Edit Development Environments in the **Settings > Configuration > Environments** list | Delete Development Environments from the **Settings > Configuration > Environments** list |
| Finding Templates¹ | Access the **Act > Explore > Findings > Templates** page | Add a Finding Template | Edit a Finding Template | Delete a Finding Template |
| Groups | Access the **Settings > Users & Permissions > Groups** page | Add a new User Group | Superuser only | Superuser only |
diff --git a/docs/content/asset_modelling/PRO__unmanaged_devices.md b/docs/content/asset_modelling/PRO__unmanaged_devices.md
index d87bcd0df85..4750bd8292b 100644
--- a/docs/content/asset_modelling/PRO__unmanaged_devices.md
+++ b/docs/content/asset_modelling/PRO__unmanaged_devices.md
@@ -1,13 +1,13 @@
---
-title: "Unmanaged Devices"
+title: "Asset Discovery"
description: "Devices your EDR sees on the network that have no agent of its own, reported by the CrowdStrike Falcon and Microsoft Defender connectors"
weight: 12
audience: pro
---
-**Unmanaged Devices** lists the devices your endpoint security tool sees on your network that do not run its agent: printers, phones, cameras, network gear, and servers and laptops nobody onboarded. Your EDR already finds them, because its agents watch the network around them. DefectDojo collects those lists from the connectors you already run, so you can see the gap in one place.
+**Asset Discovery** lists the devices your endpoint security tool sees on your network that do not run its agent: printers, phones, cameras, network gear, and servers and laptops nobody onboarded. Your EDR already finds them, because its agents watch the network around them. DefectDojo collects those lists from the connectors you already run, so you can see the gap in one place.
-> Unmanaged Devices is a beta feature. Turn on **Unmanaged Devices** on the [Feature Flags page](/admin/feature_flags/pro__feature_flags/), then turn on **Report Unmanaged Devices** on each connection that should send its list.
+> Asset Discovery is a beta feature. Turn on **Asset Discovery** on the [Feature Flags page](/admin/feature_flags/pro__feature_flags/), then turn on **Report Unmanaged Devices** on each connection that should send its list.
## Where the devices come from
diff --git a/docs/content/asset_modelling/engagements_tests/PRO__assets.md b/docs/content/asset_modelling/engagements_tests/PRO__assets.md
index df637341a82..72ead5323e8 100644
--- a/docs/content/asset_modelling/engagements_tests/PRO__assets.md
+++ b/docs/content/asset_modelling/engagements_tests/PRO__assets.md
@@ -88,7 +88,7 @@ Asset views contain a variety of tables and charts to interpret an Asset’s sta
- **Endpoints**: vulnerable (an open Finding still affects it), not vulnerable (everything else, including Endpoints no Finding has touched), and those still affected by an active Critical Finding. With Locations enabled, the card counts active, mitigated, and critical Locations instead.
- Every figure counts only what you have permission to view.
- **Open Finding Severity**
- - A list of open Findings within the Asset, grouped by severity
+ - One bar of the Asset's open Findings split by severity, with each severity's count and share underneath. The panel header shows the total open count, and each severity links to the Asset's open Findings at that severity.
- **Asset Overview**
- A breakdown of various features of the Asset, including Description, Components, Contacts, [User Groups](/admin/user_management/create_user_group/
), Members, Technologies, and Regulations.
diff --git a/docs/content/asset_modelling/locations/PRO__attack_surface_locations.md b/docs/content/asset_modelling/locations/PRO__attack_surface_locations.md
new file mode 100644
index 00000000000..177f3237918
--- /dev/null
+++ b/docs/content/asset_modelling/locations/PRO__attack_surface_locations.md
@@ -0,0 +1,60 @@
+---
+title: "Domain, IP Address and Certificate Locations"
+description: "Domain, IP address and certificate locations describe an organization's internet-facing attack surface: the DNS names, addresses and TLS certificates that parsers and connectors report"
+weight: 11
+audience: pro
+---
+
+Three location types describe what an organization exposes to the internet: **Domains**, **IP Addresses** and **Certificates**. They sit beside URLs, Dependencies, Source Code, Cloud Resources and Container Images, and parsers and connectors that support them report them.
+
+> These location types require the Locations feature and are enabled as the **External Asset Discovery** flag (beta) on the [Feature Flags page](/admin/feature_flags/pro__feature_flags/). Domains, IP addresses and certificates reported while the flag is off are still stored, and they appear as soon as the flag is turned on.
+
+## What Each Type Records
+
+### Domains
+
+A domain location is a DNS name: an apex domain such as `example.com`, a subdomain such as `api.example.com`, or a wildcard such as `*.example.com`.
+
+- **Identity** is the name in lowercase, without a trailing dot, with internationalized names in their ASCII form. `WWW.Example.com.` and `www.example.com` are one location, and so are `bücher.example` and `xn--bcher-kva.example`.
+- A wildcard is its own location. A certificate for `*.example.com` covers something different from one for `example.com`.
+- **Apex domain** is the registrable domain the name sits under. When the reporting source knows it, that value is used. Otherwise DefectDojo takes the last two labels of the name, or the last three under a country code such as `co.uk`. The apex groups names on the location map and does not decide ownership.
+- An IP address is never stored as a domain.
+
+### IP Addresses
+
+An IP location is a single IPv4 or IPv6 address, with no port or scheme.
+
+- **Identity** is the address in canonical form, so `2001:DB8::1` and `2001:db8:0:0::1` are one location. An IPv4 address written in IPv6 form, such as `::ffff:198.51.100.7`, is stored as the IPv4 address.
+- Loopback, link-local, unspecified and multicast addresses are not stored, because they do not name a machine anyone can reach.
+- Each address records whether it is **internet routable**. Private, reserved, documentation and shared address ranges are not. When the source knows them, each address also records the **ASN** that announces it, the ASN's **organization**, the registry **network** it belongs to, and the cloud or CDN **provider** that serves it. These details change when a provider reassigns an address, so the most recent report that includes a detail replaces the stored value.
+
+A web service on an address is still a URL location. An IP location is for sources that report bare addresses, such as cloud inventories, DNS answers and recon tools.
+
+### Certificates
+
+A certificate location is one X.509 certificate.
+
+- **Identity** is the SHA-256 fingerprint of the certificate, shown as `sha256:` followed by 64 hex characters. Fingerprints with colons or in upper case are accepted and stored in one form.
+- Each certificate records its **subject common name**, **issuer**, **serial number**, **validity window** and **subject alternative names**. Validity times without a time zone are read as UTC. Subject alternative names are kept once each, up to 253 characters per name and 500 names per certificate.
+- A certificate does not change, so a later report only fills in details an earlier one did not have.
+
+## How They Relate to Other Locations and Assets
+
+- **Domains and hosts.** A host location is a machine known by the name it reports, stored exactly as reported. A domain location is a DNS name, normalized. The same text can be both, and DefectDojo does not merge them.
+- **IP addresses and URLs.** A URL keeps its own host. DefectDojo does not create an IP location for every URL host.
+- **Assets.** Connectors that model each internet-facing host or domain as its own asset keep doing so. These location types describe the names, addresses and certificates themselves, and an asset can carry any number of them.
+
+## Where to Find Them
+
+- **Domains**, **IP Addresses** and **Certificates** under **Explore > Attack Surface** in the sidebar each list the locations of one type.
+- **All Locations** lists domains, IP addresses and certificates with their type, alongside every other location.
+- The asset **location map** draws three regions: **Domains** grouped by apex domain, **IP Addresses** grouped by network, and **Certificates** grouped by issuer and named by their common name. A group the source did not report, such as an address with no known network, collects under a placeholder.
+- The general locations resource returns them at `/api/v3/locations?type=domain`, `?type=ip` and `?type=cert`.
+
+These locations are created by imports and connectors. There is no form to add one by hand.
+
+## Reporting Them from a Parser or Connector
+
+Parsers build them with `LocationData.domain()`, `LocationData.ip()` and `LocationData.cert()`, and a finding carries them like any other location. Connectors send typed location items of type `domain`, `ip` or `cert`, either on a finding or as standalone inventory for an Asset; the standalone inventory also needs the **Connector Location Inventory** flag. See [Connector Location Inventory](/connectors/upstream/location_inventory/).
+
+A value that cannot name a location, such as a malformed fingerprint or an address in a domain field, is skipped and the rest of the import continues.
diff --git a/docs/content/asset_modelling/locations/PRO__location_map.md b/docs/content/asset_modelling/locations/PRO__location_map.md
index ef012cd8110..05c0bbe7951 100644
--- a/docs/content/asset_modelling/locations/PRO__location_map.md
+++ b/docs/content/asset_modelling/locations/PRO__location_map.md
@@ -29,6 +29,7 @@ Each dashed region holds one kind of location:
- Dependencies
- Cloud Resources
- Container Images, when the **Container Image Locations** flag is also on. See [Container Image Locations](/asset_modelling/locations/pro__container_image_locations/).
+- Domains, IP Addresses and Certificates, when the **External Asset Discovery** flag is also on. See [Domain, IP Address and Certificate Locations](/asset_modelling/locations/pro__attack_surface_locations/).
Chips at the top of the page count the locations in each type, for example "11 URLs".
diff --git a/docs/content/asset_modelling/locations/PRO__locations_overview.md b/docs/content/asset_modelling/locations/PRO__locations_overview.md
index 0e336f2e2bc..098fc81eca7 100644
--- a/docs/content/asset_modelling/locations/PRO__locations_overview.md
+++ b/docs/content/asset_modelling/locations/PRO__locations_overview.md
@@ -23,6 +23,7 @@ Locations fix all three by introducing a **base `Location` object** with a typed
- **Dependency Locations** — software libraries identified by [Package URL (pURL)](https://github.com/package-url/purl-spec), used to model SBOM contents.
- **[Source Code Locations](/asset_modelling/locations/pro__source_code_locations/)** — where a static-analysis finding lives in source, identified by file path and line number. Scan-managed, and the substrate for [tracking findings as their code moves](/triage_findings/finding_deduplication/pro__location_drift_matching/).
- **[Container Image Locations](/asset_modelling/locations/pro__container_image_locations/)**: the container image a finding was found in, identified by registry, repository and digest, together with the assets that run it and the repository that built it. Scan-managed, behind its own flag.
+- **[Domain, IP Address and Certificate Locations](/asset_modelling/locations/pro__attack_surface_locations/)**: the DNS names, IP addresses and TLS certificates that make up an organization's internet-facing attack surface. Scan-managed, behind the **External Asset Discovery** flag.
- **Host Locations**: a machine identified by its name (a system name, NetBIOS name or DNS name), stored exactly as the scanner reports it. Scan-managed. Qualys VMDR imports create one from each finding's Asset Name, and the Qualys connector creates one from each host's DNS or NetBIOS name when **Create Endpoints From Hosts** is on. The host's IP addresses stay URL Locations beside it.
Cloud provider resource IDs (AWS ARN, Azure Resource ID, GCP Full Resource Name) arrive as Cloud Resource locations from cloud posture scans, and from asset connectors when **Connector Location Inventory** is on. **Cloud Resources** under **Explore > Attack Surface** in the sidebar lists them. Container images arrive as [Container Image Locations](/asset_modelling/locations/pro__container_image_locations/).
diff --git a/docs/content/automation/triage_engine/runs.md b/docs/content/automation/triage_engine/runs.md
index 28e95c836a0..5a5fcd22dd8 100644
--- a/docs/content/automation/triage_engine/runs.md
+++ b/docs/content/automation/triage_engine/runs.md
@@ -94,6 +94,10 @@ Two windows apply, both configurable:
* A run that goes 30 minutes without a heartbeat is treated as abandoned, errored, and its lock released.
* A run is killed outright after six hours, as a guard against one that will never finish.
+## When a scheduled rule stops running
+
+A rule on a schedule that stops firing raises no error: there is simply no run. With [Ingestion Health](/connectors/upstream/ingestion_health/) turned on, every enabled rule that runs on a cron schedule is listed on **Settings > Operations > Ingestion Health** with its last scheduled run and its next expected one. It is flagged **Stale** once its schedule has fired three times since its last scheduled run (the number is a per-rule threshold), and the **Ingestion Health Warning** notification goes out once. Runs started by hand do not count toward this.
+
## Retention
Runs are kept for **180 days** by default, along with their per-node rows and their Finding provenance. Deliveries are kept for 180 days separately.
diff --git a/docs/content/connectors/toolreference/crowdstrike_falcon.md b/docs/content/connectors/toolreference/crowdstrike_falcon.md
index d095b6a17f8..513b2ece0f7 100644
--- a/docs/content/connectors/toolreference/crowdstrike_falcon.md
+++ b/docs/content/connectors/toolreference/crowdstrike_falcon.md
@@ -32,6 +32,6 @@ The assessment comes from the host's Spotlight vulnerabilities, so it needs the
#### Unmanaged Devices
-With [Unmanaged Devices](/asset_modelling/pro__unmanaged_devices/) turned on and **Report Unmanaged Devices** enabled on the connection, each sync also reads the hosts Falcon Discover classifies as unmanaged: devices Falcon sensors observed on the network that have no sensor of their own. They are listed under **Unmanaged Devices** with their hostname, addresses, OS, how Falcon found them (passive or active) and Falcon's confidence that each is real.
+With [Asset Discovery](/asset_modelling/pro__unmanaged_devices/) turned on and **Report Unmanaged Devices** enabled on the connection, each sync also reads the hosts Falcon Discover classifies as unmanaged: devices Falcon sensors observed on the network that have no sensor of their own. They are listed under **Asset Discovery** with their hostname, addresses, OS, how Falcon found them (passive or active) and Falcon's confidence that each is real.
This needs a Falcon Discover subscription and the **Assets: Read** scope on the API client. A client without them logs a warning and syncs everything else normally.
diff --git a/docs/content/connectors/toolreference/microsoft_defender.md b/docs/content/connectors/toolreference/microsoft_defender.md
index 9131035061a..ae8d50da203 100644
--- a/docs/content/connectors/toolreference/microsoft_defender.md
+++ b/docs/content/connectors/toolreference/microsoft_defender.md
@@ -64,6 +64,6 @@ The connector\-level **Minimum Severity** applies to every group. To use a diffe
#### Unmanaged Devices
-With [Unmanaged Devices](/asset_modelling/pro__unmanaged_devices/) turned on and **Report Unmanaged Devices** enabled on the connection, each sync also reads the machines Defender for Endpoint discovered on your network whose onboarding status is not **Onboarded**. They are listed under **Unmanaged Devices** with their name, addresses, OS and Defender's onboarding status (**Can Be Onboarded**, **Unsupported** or **Insufficient Info**). Machines Defender has merged into another record are skipped.
+With [Asset Discovery](/asset_modelling/pro__unmanaged_devices/) turned on and **Report Unmanaged Devices** enabled on the connection, each sync also reads the machines Defender for Endpoint discovered on your network whose onboarding status is not **Onboarded**. They are listed under **Asset Discovery** with their name, addresses, OS and Defender's onboarding status (**Can Be Onboarded**, **Unsupported** or **Insufficient Info**). Machines Defender has merged into another record are skipped.
This needs the **Machine.Read.All** application permission on the app registration, with admin consent granted, in addition to **Vulnerability.Read.All**. Without it the connection logs a warning and syncs everything else normally.
diff --git a/docs/content/connectors/upstream/ingestion_health.md b/docs/content/connectors/upstream/ingestion_health.md
index 477566efec8..2cf74c75b02 100644
--- a/docs/content/connectors/upstream/ingestion_health.md
+++ b/docs/content/connectors/upstream/ingestion_health.md
@@ -1,53 +1,114 @@
---
title: "Ingestion Health"
-description: "Flag a Connector whose Syncs keep succeeding with zero Findings against its usual volume, and acknowledge it with a reason"
+description: "See every Connector, scheduled Triage Engine rule and API import stream that feeds DefectDojo, flag the ones that go quiet, fall short or stop running, and acknowledge them with a reason"
pro-feature: true
---
Note: Upstream Connectors are a DefectDojo Pro-only feature. Ingestion Health is a Beta feature behind the **Ingestion Health** feature flag, which is off by default. Turn it on from [Feature Flags](/admin/feature_flags/pro__feature_flags/).
-A Connector that fails tells you so. A Connector that keeps syncing successfully while importing nothing does not: every Operation is green, and the only sign is Findings that stop arriving. The usual causes are a credential whose scope was narrowed, a filter in the tool that now excludes everything, or projects moved out of the Connector's reach.
+A Connector that fails tells you so. A Connector that keeps syncing successfully while importing nothing does not: every Operation is green, and the only sign is Findings that stop arriving. The usual causes are a credential whose scope was narrowed, a filter in the tool that now excludes everything, or projects moved out of the Connector's reach. A scheduled rule can go quiet the same way: it simply stops running, and nothing fails. So can a pipeline that uploads scans through the API: when it breaks or is switched off, imports simply stop arriving.
-Ingestion Health compares each Sync against what the Connector usually returns. When a Connector goes quiet against its baseline, its tile says so, with the number it normally imports, and an operator can acknowledge it with a reason that stays on record.
+Ingestion Health keeps a record for everything that feeds DefectDojo regularly, compares each run against what that source usually does, and raises an anomaly when it falls short. An operator can acknowledge an anomaly with a reason that stays on record.
-Ingestion Health only warns. It never pauses a Connector, changes its schedule, or blocks a Sync.
+Ingestion Health only warns. It never pauses a Connector or a rule, changes a schedule, or blocks a Sync.
-## What you see on the Connector
+## The Ingestion Health page
+
+**Settings > Operations > Ingestion Health** lists every source: each Upstream Connector, each [Triage Engine](/automation/triage_engine/about/) rule that is enabled and runs on a cron schedule, and each [API import stream](#api-import-streams). The **Ingestion Health** button on **Connections > Upstream** opens the same page. You need the **Connector View** permission to open it; scheduled rules are listed for users who also hold **Rule View**, and an API import stream is listed for users who can see its Asset (Product).
+
+Each row shows:
+
+* **Source**: the Connector or the rule, linked to its records and operations or to the rule editor. An API import stream is shown as its Asset and scan type, such as **Payments API / Trivy Scan**, linked to the Asset.
+* **Scan Type**: an API import stream's scan type, on its own so a long Asset name does not hide it. Empty for Connectors and rules.
+* **Kind**: Connector, Scheduled Rule or API Import.
+* **Status**: the worst anomaly still going on (Auth Failed, then Stale, Zero Result and Volume Drop), **Acknowledged** once every anomaly still going on has been acknowledged, **Healthy**, or **Never Ran** for a source with no run on record yet.
+* **Last Run** and **Last Run Status**: when the source last ran and how that run ended.
+* **Last Result**: the Findings the last settled run submitted: a Connector's Sync, or an API import stream's import.
+* **Baseline**: the median Findings per run over the last 30 days, for Connectors and API import streams. Hover it to see how many runs it is based on.
+* **Next Expected**: when the schedule fires next, or for an API import stream, when its next import is due going by its usual interval.
+* **Open Anomalies**: anomalies nobody has acknowledged yet.
+
+Filter and sort by any of them. The gear menu on each row has **View Source**, **View Anomalies** and, for users who can edit that kind of source, **Thresholds**.
+
+The page is refreshed by an hourly check. Opening it starts a check when none ran in the last hour, so a newly configured Connector appears at once and fills in shortly after.
-When every actively mapped record of a Connector has returned zero Findings for two Syncs in a row, and the Connector has a baseline above zero, its tile on **Connections > Upstream** shows an orange line:
+## Anomaly kinds
-**Returned 0 Findings Against a Baseline of *n***
+* **Zero Result** (Connectors and API import streams). For a Connector, every actively mapped record has returned zero Findings for two Syncs in a row, and the Connector has a baseline above zero. This is the same condition as the [Zero Results](/connectors/upstream/manage_operations/#zero-results) chip. For an API import stream, its last import submitted no Findings while its baseline is above zero.
+* **Volume Drop** (Connectors and API import streams). A successful run submitted some Findings, but fewer than half of the source's baseline (the share is a [threshold](#thresholds-and-snooze)). A Connector's Sync is judged once it has been finished for an hour, so a large import that is still landing its last records is not mistaken for a drop.
+* **Stale** (every kind). A Connector or scheduled rule missed three scheduled runs in a row: its schedule fired that many times since it last ran. Counting the times the schedule actually fired means a weekday-only schedule never goes stale over a weekend. An API import stream is stale when it has gone without a successful import for longer than three of its usual intervals and longer than any gap it has had in the last 30 days, whether its imports stopped arriving or keep arriving and failing. A Connector with no sync schedule never goes stale, and a source that has never run shows **Never Ran** rather than Stale.
+* **Auth Failed** (Connectors). A Discover or Sync failed because the tool refused the Connector's credential (HTTP 401 or 403).
-Hover the line to see when the quiet period began and what to check in the tool. This line takes the place of the **Zero Results** chip described in [Managing Operations](/connectors/upstream/manage_operations/#zero-results); the rules for when a Sync counts as empty are the same.
+A source has at most one anomaly of each kind going on at a time.
+
+## What you see on the Connector
+
+A Connector's tile on **Connections > Upstream** shows its most urgent anomaly in place of the **Zero Results** chip, in orange:
+
+* **Returned 0 Findings Against a Baseline of *n*** for a zero result. Hover it to see when the quiet period began and what to check in the tool.
+* **Returned *n* Findings Against a Baseline of *n*** for a volume drop.
+* **Missed *n* Scheduled Syncs** for a stale Connector.
+* **The Tool Refused the Credential** for an authentication failure.
## How the baseline is computed
The baseline is the median number of Findings the Connector returned per Sync over the last 30 days.
-* **One Sync is one sample.** A Sync covers every record of the Connector, so the Findings of all its records are added up first. A Connector with nine quiet projects and one busy one still has a baseline above zero.
+* **One Sync is one sample.** A Sync covers every record of the Connector, so the Findings of all its records are added up first. A Connector with nine quiet projects and one busy one still has a baseline above zero. Findings from a large import that finished after its Sync closed still count toward that Sync.
* **Findings are counted as the tool returned them**, before deduplication. A tool that returns nothing is the signal, and deduplication must not hide it.
-* **Three Syncs are needed.** Below three Syncs in the last 30 days there is no baseline, and no anomaly is raised. A new Connector, or one that only recently started syncing, never alerts until it has a history. The **Zero Results** chip still shows in that case.
-* **The median, not the average**, so one large backfill does not set the bar.
+* **Three Syncs are needed.** Below three Syncs in the last 30 days there is no baseline, and no zero-result or volume-drop anomaly is raised. A new Connector, or one that only recently started syncing, never alerts until it has a history. The **Zero Results** chip still shows in that case.
+* **The median, not the average**, so one large backfill does not set the bar. A Sync being judged for a volume drop is left out of its own baseline.
+
+## API import streams
+
+Scans uploaded through the API (`/api/v2/import-scan/`, `/api/v2/reimport-scan/` and their background variants, from a CI pipeline or a script) are tracked as streams: one stream per Asset and scan type. A stream gets a row once it has an import in the last 30 days, and its row goes away after 30 days without one, unless it has an anomaly still going on.
+
+* **Imports close together are one run.** A pipeline that uploads one scan per repository into the same Asset writes several imports within seconds. Imports of one stream less than 15 minutes apart count as a single run, and their Findings are added up, so one quiet repository does not read as a zero result. A run is judged once 15 minutes have passed with no further import.
+* **The interval is observed.** Nothing schedules an import, so a stream's usual interval is the median time between its runs over the last 30 days (at least three runs, and never less than 15 minutes). **Next Expected** is the last run plus that interval.
+* **A failed import** (one the API refused or could not parse) shows as an error in **Last Run Status** and counts toward the usual interval, but never toward the baseline, and it does not keep the stream from going stale: a stream whose imports all fail goes stale like one whose imports stopped.
+* Imports made by Connectors are not streams: they belong to the Connector's own row.
## Acknowledging an anomaly
-Users who can edit Connectors see an **Acknowledge** button beside the line. It opens a form with one required field, **Reason**: why the quiet period is expected or already handled, such as a project that was retired or a scanner that was paused.
+Users who can edit Connectors (or rules, for a scheduled rule, or the Asset, for an API import stream) can acknowledge an open anomaly, from the **Acknowledge** button on the Connector's tile or from **View Anomalies** on the Ingestion Health page. The form has one required field, **Reason**: why the anomaly is expected or already handled, such as a project that was retired or a scanner that was paused.
-After you submit, the tile shows a gray **Acknowledged** tag. Hover it to see who acknowledged the anomaly, when, and the start of the reason. Acknowledgments are kept in full in the audit history, so they can be reviewed after the anomaly resolves.
+After you submit, the anomaly shows a gray **Acknowledged** tag. Hover it on the tile to see who acknowledged it, when, and the start of the reason; **View Anomalies** shows the reason in full. Acknowledgments are kept in the audit history, so they can be reviewed after the anomaly resolves.
-If someone else acknowledged the anomaly first, or a Sync resolved it while the form was open, nothing is saved: a message says so, the first reason is kept, and the tile updates to show the anomaly as it stands.
+If someone else acknowledged the anomaly first, or a later run resolved it while the form was open, nothing is saved: a message says so, the first reason is kept, and the page updates to show the anomaly as it stands.
-Users who can only view Connectors see the line and the tag, but not the button.
+Users who can only view a source see its anomalies and their reasons, but cannot acknowledge them.
## How an anomaly resolves
-The next Sync that imports any Findings, from any record of the Connector, resolves the anomaly, whether or not it was acknowledged. The tile goes back to its usual lines. If the Connector goes quiet again later, a new anomaly is raised; each quiet period is recorded on its own.
+Each kind resolves when the source behaves normally again, whether or not it was acknowledged:
+
+* **Zero Result**: the next Sync that imports any Findings, from any record of the Connector. Mapping a new record, or setting a quiet record to Ignored, imports nothing, so it does not resolve the anomaly. For an API import stream, the next import that submits any Findings.
+* **Volume Drop**: a later settled Sync that reaches the threshold again.
+* **Stale**: the source runs again.
+* **Auth Failed**: the next successful Discover or Sync.
+
+If the source falls short again later, a new anomaly is raised; each period is recorded on its own.
+
+## Thresholds and snooze
-Nothing else resolves an anomaly. Mapping a new record, or setting a quiet record to Ignored, changes which records are counted but imports nothing, so an anomaly, and an acknowledgment on it, stays until Findings arrive.
+**Thresholds** in a source's gear menu changes when that source alerts. It needs the permission to edit that kind of source.
+
+* **Volume Drop Below** (Connectors and API import streams, default 50%): a run that submits fewer Findings than this share of the baseline opens a volume drop.
+* **Stale After Missed Runs** (default 3, from 2 to 10): how many scheduled runs in a row the source may miss. For an API import stream, each of its usual intervals that passes with no successful import is a missed run.
+* **Snooze Until**: the source raises no new anomaly through this date. Anomalies already going on stay, and a later run still resolves them.
+* **Alert on Zero Results** (Connectors and API import streams, on by default): turn it off for a source that is expected to return nothing for long stretches. On a Connector, the **Zero Results** chip still shows.
## Notifications
-Ingestion Health does not add a separate message. The **Connector Health Warning** notification already goes out when a Connector's Syncs return zero results, once per day while the condition lasts, and Ingestion Health records on the anomaly that it was sent. See [Connector health notifications](/admin/notifications/about_notifications/#connector-health-notifications-pro) for where the message goes and how to route it to Email, Slack or Teams.
+The **Ingestion Health Warning** notification goes out once per anomaly when a Connector's or API import stream's volume drops, an API import stream returns no Findings, or any source goes stale. It reaches superusers and everyone who holds the view permission for that kind of source (for an API import stream, everyone who can see its Asset and holds **Connector View**, so the page the message links to opens for them), on **🔔 Alerts** by default. A Connector's zero results and failed runs, refused credentials included, keep the **Connector Health Warning** that already covers them, so no condition sends two messages. See [Notifications](/admin/notifications/about_notifications/#ingestion-health-notifications-pro) for where each message goes and how to route it to Email, Slack or Teams.
+
+## Dashboard widget
+
+The **Ingestion Health Status** widget, under **Numbers** when you add a widget to a dashboard, shows how many of the sources you can see need attention (anomalous or stale), with a count for each status. Each count opens the Ingestion Health page filtered to the sources it counts. It is offered to users who can open the Ingestion Health page (the **Connector View** permission).
+
+## API
+
+`GET /api/v2/ingestion_health/sources/` lists the sources you can see, with the same fields and filters as the page, and `GET /api/v2/ingestion_health/sources/{id}/anomalies/` lists a source's anomalies, newest first. Both accept an API token. Thresholds and acknowledgments are set in the UI.
## Turning it off
-With the **Ingestion Health** flag off, no baseline is computed, no anomaly is raised, and the tile shows the **Zero Results** chip as before. Anomalies already raised are kept and show again if the flag is turned back on.
+With the **Ingestion Health** flag off, nothing is checked, no anomaly is raised, the Ingestion Health page and its menu entry are hidden, and the Connector tiles show the **Zero Results** chip as before. Sources and anomalies already recorded are kept and show again if the flag is turned back on.
diff --git a/docs/content/connectors/upstream/manage_operations.md b/docs/content/connectors/upstream/manage_operations.md
index 696d3a89e2f..802b77fc4e0 100644
--- a/docs/content/connectors/upstream/manage_operations.md
+++ b/docs/content/connectors/upstream/manage_operations.md
@@ -31,6 +31,12 @@ Each entry on the Operations Page's table is a record of an operation event, wit
* **Trigger** describes how the event was triggered \- was it a **Scheduled** operation which ran automatically, or a **Manual** operation which was triggered by a DefectDojo user?
* The **Start \& End Time** of each operation is recorded here, along with the **Duration**.
+## When the next run is scheduled
+
+The Connector's tile on the **Upstream Connectors** page and its **Manage Records and Operations** page show when the next Discover and Sync are scheduled, in your browser's time zone: **Next discovery scheduled for** and **Next sync scheduled for** on the tile, and each one under the card it updates (Sync under **Mapped Records**, Discover under **Unmapped Records**).
+
+The time comes from the Connector's schedule, so it says when the run is scheduled, not that it has started. A scheduled run can start a little later than that while other Connectors' runs finish. While the tile shows **Sync Stalled** or **Discovery Stalled**, the next run is not shown, because the schedule cannot promise it. If you would rather not wait for the next scheduled run, use [Run Discover Manually](#run-discover-manually) or run a Sync from the **Mapped Records** card.
+
## When a Connector runs but imports nothing
An operation that succeeds is not the same as an operation that found something. If the account
diff --git a/docs/content/get_started/pro/onprem/sandbox_mode.md b/docs/content/get_started/pro/onprem/sandbox_mode.md
index 23b41b16c1c..d6088472a85 100644
--- a/docs/content/get_started/pro/onprem/sandbox_mode.md
+++ b/docs/content/get_started/pro/onprem/sandbox_mode.md
@@ -28,7 +28,7 @@ DefectDojo's database user needs two attributes to create the sandbox:
* `CREATEROLE`, to create the sandbox's two login roles;
* `CREATEDB`, so the sandbox's admin role can create and reset the sandbox databases.
-The database user of a new Docker Compose installation with the bundled database has both. On a managed database (Amazon RDS, Google Cloud SQL, Azure Database for PostgreSQL), the administrator user the provider creates has both; a user created with SQL may not. To check, connect as DefectDojo's database user and run:
+The database user of a new Docker Compose installation with the bundled database has both. The Helm chart's bundled PostgreSQL gives DefectDojo's user `CREATEDB` only, so with the chart's bundled database the sandbox is provisioned through `sandbox.provisionUrl`, a connection URL for a role that has both, such as the bundled PostgreSQL's superuser (see below). If your chart version does not set it for you, set it yourself. On a managed database (Amazon RDS, Google Cloud SQL, Azure Database for PostgreSQL), the administrator user the provider creates has both; a user created with SQL may not. To check, connect as DefectDojo's database user and run:
```sql
SELECT rolcreaterole, rolcreatedb FROM pg_roles WHERE rolname = current_user;
diff --git a/docs/content/metrics_reports/dashboards/PRO__custom_dashboards.md b/docs/content/metrics_reports/dashboards/PRO__custom_dashboards.md
index b1fbadc4221..c8400e6ac8e 100644
--- a/docs/content/metrics_reports/dashboards/PRO__custom_dashboards.md
+++ b/docs/content/metrics_reports/dashboards/PRO__custom_dashboards.md
@@ -112,17 +112,17 @@ Each widget opens a configuration dialog tailored to its type. Common settings i
### Step 4: Arrange, then save
-Drag widgets to rearrange them and drag a corner to resize. Use the gear icon on a widget to reconfigure it, the duplicate icon to copy it, and the trash icon to remove it. Position and size changes are saved automatically as you go. Click **Done** to leave edit mode.
+Drag widgets to rearrange them and drag a corner to resize. Each widget's title bar holds a **Widget Actions** menu (the three dots beside the refresh button). While editing it offers **Edit Widget** to reconfigure the widget, **Title Style** to set the title's weight and size, **Duplicate Widget** to copy it, and **Remove Widget**, which asks for confirmation. Position and size changes are saved automatically as you go. Click **Done** to leave edit mode.
-Duplicating a tile is the fastest way to build a row of related tiles: the copy keeps the original's type, filters, size, title style, and refresh cadence, lands in the next free space on the grid, and gets a **(Copy)** suffix on its title. Open its gear icon to rename it and change the one filter that differs.
+Duplicating a tile is the fastest way to build a row of related tiles: the copy keeps the original's type, filters, size, title style, and refresh cadence, lands in the next free space on the grid, and gets a **(Copy)** suffix on its title. Choose **Edit Widget** from its menu to rename it and change the one filter that differs.
-A **Section Break** is the exception: its header already labels it, so a copy keeps the original's title as it is, with no **(Copy)** suffix. A Section Break's title is optional. Clear the **Title** field in its gear icon to leave the widget's title bar blank.
+A **Section Break** is the exception: its header already labels it, so a copy keeps the original's title as it is, with no **(Copy)** suffix. A Section Break's title is optional. Clear the **Title** field in its **Edit Widget** form to leave the widget's title bar blank.
### Putting a widget into a report
Customizable Dashboards and the [Report Builder](../../reports/report-builder/) share one widget catalog, so a figure your team reads on a dashboard can go straight into a document you send out.
-Click the export icon on a widget and choose **Add to Report**. Name the block, optionally pick a report Template to append it to, and it is created carrying the widget's current filters. The action appears on widgets a report can draw, when Reporting is enabled and you have permission to add report templates.
+Open a widget's **Widget Actions** menu and choose **Add to Report**. Name the block, optionally pick a report Template to append it to, and it is created carrying the widget's current filters. The action appears on widgets a report can draw, when Reporting is enabled and you have permission to add report templates; a widget that offers nothing else shows no menu while you are viewing.
This **copies** the widget rather than linking to it. Editing the dashboard widget later does not change the report block, and the reverse is also true, so a shared dashboard never depends on who can see which report block.
diff --git a/docs/content/navigation/language_and_localization.md b/docs/content/navigation/language_and_localization.md
index e1fb91af1af..30d3e3165ba 100644
--- a/docs/content/navigation/language_and_localization.md
+++ b/docs/content/navigation/language_and_localization.md
@@ -44,6 +44,10 @@ separately from the open source ones.
**Translated**: menus, buttons, headings, form labels, help text, confirmation dialogs, table
column headers and the messages DefectDojo shows you.
+In DefectDojo Pro, a dashboard tile keeps its default title until someone types another, and that
+default is shown in the language of whoever is looking, so one shared dashboard reads in each
+viewer's language. A title someone typed is their own text and stays exactly as written.
+
Notifications are translated too, and in **your** language rather than the language of whoever
caused them. If a colleague reading DefectDojo in German does something that notifies you, the
email you receive is in the language you chose.
diff --git a/docs/content/releases/pro/changelog.md b/docs/content/releases/pro/changelog.md
index cd31b4264b0..c0ef69cbde8 100644
--- a/docs/content/releases/pro/changelog.md
+++ b/docs/content/releases/pro/changelog.md
@@ -16,10 +16,33 @@ You can subscribe to these release notes with the [RSS feed](/releases/pro/chang
-For Open Source release notes, please see the [Releases page on GitHub](https://github.com/DefectDojo/django-DefectDojo/releases), or alternatively consult the Open Source [upgrade notes](/releases/os_upgrading/upgrading_guide/).
+For Open Source release notes, please see the [Releases page on GitHub](https://github.com/DefectDojo/django-Defe ctDojo/releases), or alternatively consult the Open Source [upgrade notes](/releases/os_upgrading/upgrading_guide/).
## October 2026: v3.4
+### October 29, 2026: v3.4.303
+
+New features:
+* **(Compliance)** A Finding's FedRAMP Potential Agency Impact (PAIN) rating can be set, changed or cleared by hand: on **Edit Finding**, in **Bulk Edit**, and through `pain_rating` on the Findings API, which also accepts it when a Finding is created. The Findings list gains a **PAIN** column and a filter that offers **Unrated**. Rating needs the new **Rate Potential Agency Impact (PAIN)** permission, which every role that can edit Findings receives on upgrade. See [Potential Agency Impact (PAIN) Ratings](/federal_compliance/pain_ratings/#rating-findings-by-hand).
+* **(Findings)** My Work is one **Up Next** queue of everything waiting on you, grouped by deadline into Overdue, Due in 7 Days, Later, No Due Date and Waiting on Others: assigned Findings, reviews to claim or clear, Risk Acceptances about to expire, and remediation actions assigned to you. Each item has a next-step button that opens the matching form, and the queue shows as a list, a grid or a board. See [My Work](/metrics_reports/dashboards/pro__my_work/).
+* **(Sensei)** Ask Sensei answers "what should I fix first?" with fixes rather than Findings: the Root Causes that clear the most, exploited ones first, each with a **Fix With Sensei** button that shows the plan before Sensei opens pull requests. The panel can expand to full width or open as its own page, and a new conversation opens on a home view of what changed since your last visit and the top fixes. Ranked fixes need Root Cause Correlation. See [Ask Sensei](/sensei/ask_sensei/#fixes-not-findings).
+
+Enhancements:
+* **(Deduplication)** Matching Configuration shows each tool's default algorithm and hash fields, and **Reset to Default** loads them into the form, to be saved through **Review Impact** like any other matching change. See [Deduplication Tuning](/triage_findings/finding_deduplication/pro__deduplication_tuning/#resetting-a-tool-to-its-default).
+* **(Reporting)** On Organization, Asset, Engagement, Test and Finding blocks, the **Fields** and **Order By** pickers list their fields under the same headings as that list's **Add Filter**. See [Report Builder](/metrics_reports/reports/report-builder/#blocks).
+* **(Navigation)** Each control in the sidebar footer has its own shape: **What's New** is one button naming the release and its unseen count, the Production/Sandbox switch is a full-width button, and the account row shows your username and email beside the alerts bell. **Ask Sensei** moved into the search field as an **Ask** button. See [The Sidebar Menu](/navigation/pro__sidebar/#searching).
+* **(Settings)** When the running release has been revoked, users who can see the Version Manager get a banner with the reason and the release to upgrade to, and the Version Manager never offers a revoked release as the latest version. See [Revoked releases](/get_started/pro/onprem/upgrading/#revoked-releases).
+* **(API)** The Findings API takes a `has_sla` filter: `true` keeps Findings that have an SLA date, `false` keeps those without one.
+
+Bug fixes:
+* **(Findings)** SLA status agrees on every page. Today is the viewer's date, an open Finding due today is within SLA and reads **Due Today**, and a mitigated Finding is judged by the date it was mitigated, so the Findings list, Finding Groups, the Insights counts and charts, and the **Mitigated Within SLA** column put a Finding on the same side of its due date. An open Finding Group's SLA comes from its open Findings only. An owner assignment completes when its Finding closes, so closed Findings no longer count as open work, and a closed Finding cannot be assigned. See [How SLA days are counted](/asset_modelling/pro_hierarchy/priority_sla/#how-sla-days-are-counted) and [Peer Review & Claiming](/triage_findings/findings_workflows/pro__peer_review/#when-a-finding-closes).
+* **(Deduplication)** **Mark as Duplicate** in the Similar Findings menu refuses to mark an open Finding as a duplicate of a mitigated one, and shows one message with the reason. Consolidating historical churn no longer merges distinct Findings: it orders Findings by import instead of by timestamp, so backdated scan dates cannot chain Findings that were open at the same time, and it leaves out false positive, risk accepted and out of scope Findings. See [Similar Findings](/triage_findings/finding_deduplication/pro__similar_findings/#actions) and [Location Drift Matching](/triage_findings/finding_deduplication/pro__location_drift_matching/#consolidating-historical-churn).
+* **(Tables)** **Sort** offers custom fields on the Assets, Organizations, Engagements and Tests lists, as the Findings list already did, so the grid view can sort by them; multi-select custom fields are not offered on any list. A column filter that has not been applied yet is left alone when settings sync in the background. Toolbar icon buttons match the height of the fields beside them, toolbar labels give way to icons instead of overlapping on narrow windows, and a Tags cell shows the whole tags that fit and folds the rest into **+N**. See [Customizing Tables](/navigation/pro__table_customization/#sorting-columns).
+* **(Dashboards)** A dashboard tile's header stays on one row with a readable title, while viewing and while editing, including beside a stale or failed freshness stamp. See [Customizable Dashboards](/metrics_reports/dashboards/custom-dashboards/).
+* **(Locations)** Sorting the Locations list by **Status**, including in a Finding's affected-locations view, works instead of failing to load. See [Locations Overview](/asset_modelling/locations/pro__locations_overview/).
+* **(Importers)** **Reimport Scan** on a Test keeps **Submit** disabled until the Test's details load, so a quick submit no longer fails validation on fields that cannot be edited. See [Tests](/asset_modelling/engagements_tests/pro__tests/#reimporting-scan-results-ui).
+* **(Settings)** The Version Manager answers normally on a new instance's first page load instead of returning a server error. See [Upgrading DefectDojo Pro (On-Premise)](/get_started/pro/onprem/upgrading/).
+
### October 28, 2026: v3.4.302
New features:
diff --git a/docs/content/supported_tools/parsers/file/generic.md b/docs/content/supported_tools/parsers/file/generic.md
index 8324fa8d075..456dd7795bb 100644
--- a/docs/content/supported_tools/parsers/file/generic.md
+++ b/docs/content/supported_tools/parsers/file/generic.md
@@ -156,8 +156,11 @@ Every finding must have `title`, `severity` and `description`. A finding with a
| `payload` | String
`"' OR 1=1 --"` | The input that triggers the issue. |
| `line` | Integer
`42`, `"42"` | Line number in `file_path`. |
| `file_path` | String (max 4000 characters)
`"src/auth/login.py"` | Setting this marks a new finding as static (see `static_finding`). |
-| `component_name` | String (max 500 characters)
`"xz-utils"` | The affected package or library. A blank or whitespace-only value is stored as empty. With Locations enabled, together with `component_version` and `file_path` it also creates a dependency location on the finding. |
+| `component_name` | String (max 500 characters)
`"xz-utils"` | The affected package or library. A blank or whitespace-only value is stored as empty. In DefectDojo Pro with Locations enabled, together with `component_version` and `file_path` it also creates a dependency location on the finding. Send a Package URL in `purl`, not here (see [Package details](#package-details)). |
| `component_version` | String (max 100 characters)
`"5.6.0"` | The affected version of `component_name`. A blank or whitespace-only value is stored as empty. |
+| `purl` | String
`"pkg:maven/commons-collections/commons-collections@3.2.1"` | The component as a [Package URL](https://github.com/package-url/purl-spec). Fills in `component_name` (and `component_version`, for the same package) when they are not set. In DefectDojo Pro with Locations enabled, it also identifies the dependency location, after the normalization the Package URL specification defines. Takes precedence over `purl_type` and `namespace`. See [Package details](#package-details). |
+| `purl_type` | String
`"maven"`, `"npm"`, `"pypi"` | The package ecosystem of `component_name`, for a tool that reports the parts instead of a `purl`. Without it (and without `purl`) the dependency location gets the type `generic`. |
+| `namespace` | String
`"commons-collections"` | The package namespace of `component_name` (a Maven group ID, an npm scope), usually sent with `purl_type`. Without `purl_type` it is still used, under the type `generic`. |
| `static_finding` | Boolean
`true` | Defaults to `false`. When `file_path` is set, DefectDojo marks a new finding as static regardless of this value. |
| `dynamic_finding` | Boolean
`false` | Defaults to `true`. When `file_path` is set, DefectDojo can override this value for a new finding. |
| `scanner_confidence` | Integer
`3` | Confidence reported by the tool. |
@@ -208,6 +211,55 @@ Every finding must have `title`, `severity` and `description`. A finding with a
]
```
+#### Package details
+
+In DefectDojo Pro with Locations enabled, a finding that sets `component_name`, `component_version`, `file_path` or `purl` gets a dependency location, identified by a Package URL. The open-source edition stores no dependency locations; there, `purl` only fills in `component_name` and `component_version`. Without package details that Package URL is built from the component fields alone with the type `generic`: `"component_name": "commons-collections"` becomes `pkg:generic/commons-collections`.
+
+Send the package details in one of two ways:
+
+- A full Package URL in `purl`. Do not put a Package URL in `component_name`: it is read as a plain name and ends up inside another Package URL.
+- The parts, in `purl_type` and `namespace`, next to `component_name` and `component_version`.
+
+```JSON
+"findings": [
+ {
+ "title": "Vulnerable component: commons-collections",
+ "severity": "Critical",
+ "description": "Remote code execution in commons-collections.",
+ "file_path": "pom.xml",
+ "purl": "pkg:maven/commons-collections/commons-collections@3.2.1"
+ },
+ {
+ "title": "Vulnerable component: commons-collections",
+ "severity": "Critical",
+ "description": "Remote code execution in commons-collections.",
+ "file_path": "pom.xml",
+ "component_name": "commons-collections",
+ "component_version": "3.2.1",
+ "purl_type": "maven",
+ "namespace": "commons-collections"
+ }
+]
+```
+
+Both findings produce the dependency `pkg:maven/commons-collections/commons-collections@3.2.1`.
+
+A `purl` is normalized the way the Package URL specification defines before it is used, for both the dependency location and the component fields it fills in: `pkg:NPM/Foo@1` becomes `pkg:npm/foo@1`, and `pkg:pypi/Django_Foo@1.0` becomes `pkg:pypi/django-foo@1.0`.
+
+A package field never stops the import. A value that cannot be used is left out, logged as a warning, and the finding imports as if it had not been sent:
+
+- A `null`, blank or non-string value, or one containing a NUL character (sent as is or percent-encoded in a `purl`), counts as absent.
+- A `purl` that is not a valid Package URL is ignored. `purl_type` and `namespace`, if sent, are used instead.
+- A `purl_type` that is not a valid Package URL type (letters, digits, `.`, `+` and `-`, not starting with a digit) is ignored, so the type is `generic`. It is read in lowercase.
+- A valid `purl` sent with `purl_type` or `namespace` wins, and the other two are ignored.
+- `purl_type` and `namespace` on a finding that has no `component_name`, `component_version`, `file_path` or `purl` are ignored: there is no component for them to describe.
+- A name or version in the `purl` that is longer than `component_name` or `component_version` allows is not copied into them.
+- A dependency location with a part too long to store is left off the finding, and the rest of the finding imports.
+
+A blank `component_name` or `component_version` counts as not set, so the `purl` fills it in. When `component_name` is set, the finding keeps it, and the `purl`'s version is only copied into an unset `component_version` when `component_name` is the same name the `purl` carries. The dependency location follows the `purl` either way.
+
+Other package details, such as hashes, license and component type, are not read from this format. To import them, use a [CycloneDX](/supported_tools/parsers/file/cyclonedx/) SBOM.
+
#### Attached files
`files` is a list of objects, each with a `title` (string) and `data` (the file's contents, Base64-encoded). The files are attached to the finding.
diff --git a/docs/content/supported_tools/parsers/generic_findings_import.md b/docs/content/supported_tools/parsers/generic_findings_import.md
index 039359002ad..fca64ea65cf 100644
--- a/docs/content/supported_tools/parsers/generic_findings_import.md
+++ b/docs/content/supported_tools/parsers/generic_findings_import.md
@@ -17,7 +17,7 @@ DefectDojo Pro users can also use the [Universal Parser](/import_data/pro/specia
DefectDojo picks the format from the file name: a name ending in `.csv` is read as CSV, and any other file is read as JSON.
- **CSV** covers the basics: title, severity, description, date, CWE, CVE, a URL, CVSS, EPSS, KEV and fix fields. Rows that share a severity, title and description are merged into one finding. See [CSV format](/supported_tools/parsers/file/generic/#csv-format) and the [example CSV](/supported_tools/parsers/file/generic/#example-csv).
-- **JSON** supports everything CSV does, plus components, SAST source and sink data, tags, several endpoints per finding, attached files, and report-level metadata such as the tool name. See [JSON format](/supported_tools/parsers/file/generic/#json-format) and the [example JSON](/supported_tools/parsers/file/generic/#example-json).
+- **JSON** supports everything CSV does, plus components with their Package URLs, SAST source and sink data, tags, several endpoints per finding, attached files, and report-level metadata such as the tool name. See [JSON format](/supported_tools/parsers/file/generic/#json-format) and the [example JSON](/supported_tools/parsers/file/generic/#example-json).
Use JSON when you can. A JSON report can also set its own Test Type, which matters for deduplication (see below).
diff --git a/docs/content/triage_findings/finding_deduplication/PRO__deduplication_tuning.md b/docs/content/triage_findings/finding_deduplication/PRO__deduplication_tuning.md
index e10592c3a97..6c3398d2465 100644
--- a/docs/content/triage_findings/finding_deduplication/PRO__deduplication_tuning.md
+++ b/docs/content/triage_findings/finding_deduplication/PRO__deduplication_tuning.md
@@ -110,6 +110,17 @@ python manage.py consolidate_folded_findings --product --apply
The newest finding of each group survives, and the others' locations move onto it, keeping the test that reported each one. The others become its duplicates.
+Consolidation records each finding it folds: the finding it went onto, the locations that moved, and what marking it a duplicate cleared. Moving the tool back to **As the parser groups them** after consolidating does not split anything when you save it, because what the parser makes of a report is only known at the next import. The split happens on the next reimport instead. When it reports a finding that consolidation folded away, that finding becomes active again, its locations (files, URLs or any other kind) move back from the surviving finding with their status history, it is verified again if it was before, and a note on it names the finding it had been folded into. Only findings with a consolidation record come back: a finding you marked as a duplicate yourself stays a duplicate. A folded-away finding is also left alone when the surviving finding is no longer active, for example because it was marked a false positive or risk accepted.
+
+Consolidations run on an earlier release have no record, so a reimport leaves their folded findings closed. To reopen them, run `unfold_consolidated_findings` for the Assets concerned. It lists each finding it would reopen with its reasons, counts the ones it leaves alone (`--verbose` lists those too), and changes nothing until you pass `--apply` (with `--user`, the account the notes are written as):
+
+```bash
+python manage.py unfold_consolidated_findings --product
+python manage.py unfold_consolidated_findings --product --apply --user
+```
+
+It recognizes these findings by their file and line, so it only handles findings with a code location. A finding on the same line as the finding it duplicates is left alone, because a person marking one finding on that line a duplicate of another leaves the same traces. If a reimport on an earlier release already gave a folded-away finding a new location row of its own, that newer row is removed when the finding is reopened, so its original row, with its history, comes back to it from the surviving finding; the note on the finding names each row removed. Findings without a file are only counted, because nothing on them says which locations they held. Reopen any of these yourself if they should be open. Use `--pool ` instead of `--product` to cover every Asset in a dedupe pool.
+
To use a grouping only for some Assets, put them in a [dedupe pool](/triage_findings/finding_deduplication/pro__dedupe_pools/) and pick that pool under **Applies To**. Assets outside the pool keep the instance default.
## Same Tool Deduplication
diff --git a/dojo/__init__.py b/dojo/__init__.py
index 0155b24cfcb..a51fc98afce 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.4.303"
+__version__ = "3.4.304"
__url__ = "https://github.com/DefectDojo/django-DefectDojo"
__docs__ = "https://documentation.defectdojo.com"
diff --git a/dojo/engagement/ui/views.py b/dojo/engagement/ui/views.py
index 640511d1016..a2e4733c9db 100644
--- a/dojo/engagement/ui/views.py
+++ b/dojo/engagement/ui/views.py
@@ -1352,7 +1352,7 @@ def view_edit_risk_acceptance(request, eid, raid, *, edit_mode=False):
extra_tags="alert-success")
if "delete_note" in request.POST:
- note = get_object_or_404(Notes, pk=request.POST["delete_note_id"])
+ note = get_object_or_404(risk_acceptance.notes, pk=request.POST["delete_note_id"])
if note.author.username == request.user.username:
risk_acceptance.notes.remove(note)
note.delete()
diff --git a/dojo/tools/generic/json_parser.py b/dojo/tools/generic/json_parser.py
index 5a986912c4f..35d7bf5d24d 100644
--- a/dojo/tools/generic/json_parser.py
+++ b/dojo/tools/generic/json_parser.py
@@ -1,10 +1,13 @@
import base64
+import logging
import math
+import re
from functools import cache
import dateutil
from django.core.files.base import ContentFile
from django.db.models import CharField
+from packageurl import PackageURL
from dojo.finding.cwe import cwe_number
from dojo.location.feature import locations_enabled
@@ -13,6 +16,16 @@
from dojo.tools.parser_test import ParserTest
from dojo.tools.utils import mark_unique_ids_ambiguous_when_repeated
+# Package coordinates a finding may carry for its dependency location. ``purl`` is a full
+# Package URL; ``purl_type`` and ``namespace`` complete ``component_name`` and
+# ``component_version`` when the tool reports the parts instead.
+PACKAGE_FIELDS = ("purl", "purl_type", "namespace")
+
+# A Package URL type: ASCII letters, digits, ".", "+" and "-", not starting with a digit.
+PURL_TYPE = re.compile(r"[a-z.+-][a-z0-9.+-]*")
+
+logger = logging.getLogger(__name__)
+
# Accepted fields that map to a numeric column on Finding, and the type they hold.
NUMERIC_FIELDS = {
"cvssv3_score": float,
@@ -41,6 +54,18 @@ def to_number(value, converter):
return number if math.isfinite(number) else None
+def _stripped(value):
+ """Return a string value with surrounding whitespace removed, and any other value unchanged."""
+ return value.strip() if isinstance(value, str) else value
+
+
+def _purl_parts(purl):
+ """Every decoded string a Package URL carries: its components and its qualifier keys and values."""
+ qualifiers = purl.qualifiers if isinstance(purl.qualifiers, dict) else {}
+ parts = (purl.type, purl.namespace, purl.name, purl.version, purl.subpath, *qualifiers.keys(), *qualifiers.values())
+ return [part for part in parts if isinstance(part, str)]
+
+
@cache
def _finding_max_lengths() -> dict[str, int]:
"""
@@ -96,6 +121,9 @@ def _get_test_json(self, data):
if "cwes" in item:
unsaved_cwes = item["cwes"]
del item["cwes"]
+ # remove the package coordinates from the dictionary: they are not Finding
+ # fields, they describe the dependency location built further down.
+ package = {key: item.pop(key) for key in PACKAGE_FIELDS if key in item}
# check for required keys
required = {"title", "severity", "description"}
@@ -170,6 +198,24 @@ def _get_test_json(self, data):
raise ValueError(msg)
self._normalize_numeric_fields(item)
+ purl, purl_type, namespace = self._parse_package(package)
+ if purl:
+ # The purl names the component, so a report that sends only the purl still
+ # gets a component on the finding. Explicit component fields win, a blank one
+ # counts as unset, the version is only taken for the package the purl names
+ # (never next to a different component_name), and a value too wide for its
+ # column is left out rather than failing the import.
+ max_lengths = _finding_max_lengths()
+ name = _stripped(item.get("component_name"))
+ if not name and purl.name and len(purl.name) <= max_lengths["component_name"]:
+ item["component_name"] = name = purl.name
+ if (
+ name == purl.name
+ and not _stripped(item.get("component_version"))
+ and purl.version
+ and len(purl.version) <= max_lengths["component_version"]
+ ):
+ item["component_version"] = purl.version
# A value wider than its column would only fail at INSERT time, deep in the
# importer, as a DataError the API reports as a 500. Reject it here instead: the
# importer turns the ValueError into a 400 naming the field and the limit.
@@ -213,9 +259,16 @@ def _get_test_json(self, data):
component_name = item.get("component_name")
component_version = item.get("component_version")
file_path = item.get("file_path")
- if component_name or component_version or file_path:
+ if not (purl or component_name or component_version or file_path) and (purl_type or namespace):
+ logger.warning(
+ "Generic Findings Import: ignoring purl_type and namespace, the finding names no component",
+ )
+ if purl or component_name or component_version or file_path:
finding.unsaved_locations.append(
LocationData.dependency(
+ purl=purl.to_string() if purl else "",
+ purl_type=purl_type,
+ namespace=namespace,
name=component_name,
version=component_version,
file_path=file_path,
@@ -272,6 +325,55 @@ def _get_test_json(self, data):
mark_unique_ids_ambiguous_when_repeated(test_internal.findings)
return test_internal
+ def _parse_package(self, package):
+ """
+ Read the package coordinates of a finding, returning ``(purl, purl_type, namespace)``.
+
+ Never raises. The package fields only refine the dependency location, so a value that
+ cannot be used is dropped with a warning and the finding imports as it would without
+ it: a null or blank value counts as absent, a purl that does not parse falls back to
+ ``purl_type`` and ``namespace``, and a ``purl_type`` outside the Package URL grammar
+ falls back to the generic type. A valid purl already names its type and namespace, so
+ it takes precedence over the two parts. A NUL character, which PostgreSQL cannot store
+ in a text column, makes a value unusable too, including one percent-encoded in a purl.
+ """
+ values = {}
+ for key, value in package.items():
+ if value is None:
+ continue
+ if not isinstance(value, str):
+ logger.warning("Generic Findings Import: ignoring %s, which is not a string: %.200r", key, value)
+ continue
+ if "\x00" in value:
+ logger.warning("Generic Findings Import: ignoring %s, which contains a NUL character", key)
+ continue
+ if value := value.strip():
+ values[key] = value
+
+ if package_url := values.get("purl"):
+ try:
+ purl = PackageURL.from_string(package_url)
+ # The location stores the normalized string and parses it again, so it has to
+ # survive that second parse too.
+ PackageURL.from_string(purl.to_string())
+ if any("\x00" in part for part in _purl_parts(purl)):
+ msg = "a component decodes to a NUL character"
+ raise ValueError(msg)
+ # Any failure, not only ValueError: an unusable purl must never fail the import.
+ except Exception as error:
+ logger.warning("Generic Findings Import: ignoring purl %.200r, not a valid Package URL: %s", package_url, error)
+ else:
+ ignored = sorted(key for key in ("purl_type", "namespace") if key in values)
+ if ignored:
+ logger.warning("Generic Findings Import: ignoring %s, the purl already sets them", ignored)
+ return purl, "", ""
+
+ purl_type = values.get("purl_type", "").lower()
+ if purl_type and not PURL_TYPE.fullmatch(purl_type):
+ logger.warning("Generic Findings Import: ignoring purl_type %.200r, not a valid Package URL type", purl_type)
+ purl_type = ""
+ return None, purl_type, values.get("namespace", "")
+
def _normalize_numeric_fields(self, item):
"""
Coerce the numeric fields of a finding, dropping the ones that hold no number.
diff --git a/helm/defectdojo/Chart.yaml b/helm/defectdojo/Chart.yaml
index 9b9a1db9792..b560418c774 100644
--- a/helm/defectdojo/Chart.yaml
+++ b/helm/defectdojo/Chart.yaml
@@ -1,8 +1,8 @@
apiVersion: v2
-appVersion: "3.4.303"
+appVersion: "3.4.304"
description: A Helm chart for Kubernetes to install DefectDojo
name: defectdojo
-version: 1.9.63
+version: 1.9.64
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.4.303\n"
+ artifacthub.io/changes: "- kind: changed\n description: Bump DefectDojo to 3.4.304\n"
diff --git a/helm/defectdojo/README.md b/helm/defectdojo/README.md
index 3c56b9bbca4..d6e306186ed 100644
--- a/helm/defectdojo/README.md
+++ b/helm/defectdojo/README.md
@@ -521,7 +521,7 @@ The HELM schema will be generated for you.
# General information about chart values
- 
+ 
A Helm chart for Kubernetes to install DefectDojo
diff --git a/unittests/scans/generic/generic_purl.json b/unittests/scans/generic/generic_purl.json
new file mode 100644
index 00000000000..b1130eae44e
--- /dev/null
+++ b/unittests/scans/generic/generic_purl.json
@@ -0,0 +1,28 @@
+{
+ "type": "Dependency Tool",
+ "findings": [
+ {
+ "title": "Vulnerable component sent as a purl",
+ "severity": "Critical",
+ "description": "Only the purl names the component.",
+ "file_path": "code/pom.xml",
+ "purl": "pkg:maven/commons-collections/commons-collections@3.2.1"
+ },
+ {
+ "title": "Vulnerable component sent as purl parts",
+ "severity": "High",
+ "description": "The component fields are completed by a package type and namespace.",
+ "component_name": "lodash",
+ "component_version": "4.17.20",
+ "purl_type": "npm",
+ "namespace": ""
+ },
+ {
+ "title": "Vulnerable component without package coordinates",
+ "severity": "Low",
+ "description": "Falls back to a generic package type.",
+ "component_name": "openssl",
+ "component_version": "1.1.1"
+ }
+ ]
+}
diff --git a/unittests/test_permissions_audit.py b/unittests/test_permissions_audit.py
index 6486fdafa69..9c60912d2b1 100644
--- a/unittests/test_permissions_audit.py
+++ b/unittests/test_permissions_audit.py
@@ -1013,6 +1013,28 @@ def test_nonexistent_finding_id_returns_404(self):
response = client.post(url, self._remove_finding_data(999999))
self.assertEqual(response.status_code, 404)
+ def test_own_note_outside_risk_acceptance_is_not_deleted(self):
+ note = Notes.objects.create(entry="note on product B", author=self.owner_user_a)
+ self.finding_b.notes.add(note)
+ client = self._login("ra_remove_owner_a")
+ url = reverse("view_risk_acceptance", args=(
+ self.engagement_a.id, self.risk_acceptance_a.id,
+ ))
+ response = client.post(url, {"delete_note": "Delete", "delete_note_id": note.id})
+ self.assertEqual(response.status_code, 404)
+ self.assertTrue(Notes.objects.filter(pk=note.pk).exists())
+
+ def test_own_note_on_risk_acceptance_is_deleted(self):
+ note = Notes.objects.create(entry="note on RA A", author=self.owner_user_a)
+ self.risk_acceptance_a.notes.add(note)
+ client = self._login("ra_remove_owner_a")
+ url = reverse("view_risk_acceptance", args=(
+ self.engagement_a.id, self.risk_acceptance_a.id,
+ ))
+ response = client.post(url, {"delete_note": "Delete", "delete_note_id": note.id})
+ self.assertEqual(response.status_code, 302)
+ self.assertFalse(Notes.objects.filter(pk=note.pk).exists())
+
class TestEngagementPresetsCrossProductIDOR(LegacyAuthMirrorMixin, DojoTestCase):
diff --git a/unittests/tools/test_generic_parser.py b/unittests/tools/test_generic_parser.py
index 811078c6877..eb01b3581c7 100644
--- a/unittests/tools/test_generic_parser.py
+++ b/unittests/tools/test_generic_parser.py
@@ -1,6 +1,8 @@
import datetime
import json
+from django.test import override_settings
+
from dojo.finding.cwe import finding_cwe_labels
from dojo.models import Engagement, Finding, Product, Test
from dojo.tools.generic.parser import GenericParser
@@ -966,3 +968,137 @@ def test_fixture_with_filled_false_and_empty_cells(self):
self.assertEqual(fix_available, finding.fix_available)
self.assertEqual(cvssv3_score, finding.cvssv3_score)
self.assertEqual(cwe, finding.cwe)
+
+
+class TestGenericJSONParserPackage(DojoTestCase):
+
+ """The purl, purl_type and namespace fields that build a finding's dependency location."""
+
+ def _dependency_locations(self, finding):
+ return [location for location in finding.unsaved_locations if location.type == "dependency"]
+
+ @override_settings(V3_FEATURE_LOCATIONS=True)
+ def test_parse_json_purl_builds_the_dependency_location(self):
+ with (get_unit_tests_scans_path("generic") / "generic_purl.json").open(encoding="utf-8") as file:
+ findings = {f.title: f for f in GenericParser().get_findings(file, Test())}
+ self.assertEqual(3, len(findings))
+
+ from_purl = findings["Vulnerable component sent as a purl"]
+ # The purl fills in the component the finding did not name.
+ self.assertEqual("commons-collections", from_purl.component_name)
+ self.assertEqual("3.2.1", from_purl.component_version)
+ (dependency,) = self._dependency_locations(from_purl)
+ self.assertEqual("pkg:maven/commons-collections/commons-collections@3.2.1", dependency.data["purl"])
+ self.assertEqual("code/pom.xml", dependency.data["file_path"])
+
+ from_parts = findings["Vulnerable component sent as purl parts"]
+ (dependency,) = self._dependency_locations(from_parts)
+ self.assertEqual("", dependency.data["purl"])
+ self.assertEqual("npm", dependency.data["purl_type"])
+ self.assertEqual("lodash", dependency.data["name"])
+ self.assertEqual("4.17.20", dependency.data["version"])
+
+ without_package = findings["Vulnerable component without package coordinates"]
+ (dependency,) = self._dependency_locations(without_package)
+ self.assertEqual("", dependency.data["purl"])
+ self.assertEqual("", dependency.data["purl_type"])
+ self.assertEqual("openssl", dependency.data["name"])
+
+ @override_settings(V3_FEATURE_LOCATIONS=True)
+ def test_parse_json_purl_keeps_explicit_component_fields(self):
+ content = json.dumps({"findings": [{
+ "title": "t", "severity": "High", "description": "d",
+ "component_name": "commons-collections:commons-collections", "component_version": "3.2.1-patched",
+ "purl": "pkg:maven/commons-collections/commons-collections@3.2.1",
+ }]})
+ (finding,) = GenericParser().get_findings(TestFile("findings.json", content), Test())
+ self.assertEqual("commons-collections:commons-collections", finding.component_name)
+ self.assertEqual("3.2.1-patched", finding.component_version)
+ (dependency,) = self._dependency_locations(finding)
+ self.assertEqual("pkg:maven/commons-collections/commons-collections@3.2.1", dependency.data["purl"])
+
+ @override_settings(V3_FEATURE_LOCATIONS=False)
+ def test_parse_json_purl_without_locations_still_sets_the_component(self):
+ content = json.dumps({"findings": [{
+ "title": "t", "severity": "High", "description": "d",
+ "purl": "pkg:npm/%40angular/core@16.0.0",
+ }]})
+ (finding,) = GenericParser().get_findings(TestFile("findings.json", content), Test())
+ self.assertEqual("core", finding.component_name)
+ self.assertEqual("16.0.0", finding.component_version)
+ self.assertEqual([], getattr(finding, "unsaved_locations", []))
+
+ def _parse_one(self, **fields):
+ content = json.dumps({"findings": [{"title": "t", "severity": "High", "description": "d", **fields}]})
+ (finding,) = GenericParser().get_findings(TestFile("findings.json", content), Test())
+ return finding
+
+ @override_settings(V3_FEATURE_LOCATIONS=True)
+ def test_parse_json_unusable_package_fields_never_fail_the_import(self):
+ # Each case imports the finding and builds the location the remaining fields describe.
+ for fields, expected in [
+ ({"purl": "commons-collections"}, {"purl": "", "purl_type": "", "namespace": ""}),
+ ({"purl": "pkg:npm/a@1?x"}, {"purl": "", "purl_type": "", "namespace": ""}),
+ ({"purl": None, "purl_type": None, "namespace": None}, {"purl": "", "purl_type": "", "namespace": ""}),
+ ({"purl": 42, "purl_type": ["maven"], "namespace": {"a": 1}}, {"purl": "", "purl_type": "", "namespace": ""}),
+ ({"purl": " ", "purl_type": " npm "}, {"purl": "", "purl_type": "npm", "namespace": ""}),
+ ({"purl_type": "Maven", "namespace": "g"}, {"purl": "", "purl_type": "maven", "namespace": "g"}),
+ ({"purl_type": "not a type!"}, {"purl": "", "purl_type": "", "namespace": ""}),
+ ({"purl_type": "1abc"}, {"purl": "", "purl_type": "", "namespace": ""}),
+ # Parses, but its normalized string does not parse again.
+ ({"purl": "pkg:npm/%2F %2F#@~"}, {"purl": "", "purl_type": "", "namespace": ""}),
+ # A NUL character cannot be stored, sent plainly or percent-encoded in any purl part.
+ ({"purl": "pkg:npm/a%00b@1.0"}, {"purl": "", "purl_type": "", "namespace": ""}),
+ ({"purl": "pkg:npm/%00scope/ab@1"}, {"purl": "", "purl_type": "", "namespace": ""}),
+ ({"purl": "pkg:npm/ab@1?k=%00"}, {"purl": "", "purl_type": "", "namespace": ""}),
+ ({"purl": "pkg:npm/ab@1#a%00b"}, {"purl": "", "purl_type": "", "namespace": ""}),
+ ({"purl_type": "npm", "namespace": "a\u0000b"}, {"purl": "", "purl_type": "npm", "namespace": ""}),
+ # An unusable purl falls back to the parts sent beside it.
+ ({"purl": "not-a-purl", "purl_type": "npm"}, {"purl": "", "purl_type": "npm", "namespace": ""}),
+ # A valid purl takes precedence over parts that disagree with it.
+ (
+ {"purl": "pkg:maven/a/b@1", "purl_type": "npm", "namespace": "x"},
+ {"purl": "pkg:maven/a/b@1", "purl_type": "", "namespace": ""},
+ ),
+ ]:
+ with self.subTest(fields=fields):
+ finding = self._parse_one(component_name="lib", component_version="1.0", **fields)
+ self.assertEqual("lib", finding.component_name)
+ (dependency,) = self._dependency_locations(finding)
+ self.assertEqual(expected, {key: dependency.data[key] for key in expected})
+
+ @override_settings(V3_FEATURE_LOCATIONS=True)
+ def test_parse_json_purl_too_wide_for_the_component_columns(self):
+ # The purl still builds the location; the component fields it cannot fill stay empty.
+ name = "n" * 600
+ finding = self._parse_one(purl=f"pkg:npm/{name}@{'9' * 150}")
+ self.assertIsNone(finding.component_name)
+ self.assertIsNone(finding.component_version)
+ (dependency,) = self._dependency_locations(finding)
+ self.assertTrue(dependency.data["purl"].startswith(f"pkg:npm/{name}@"))
+
+ def test_parse_json_purl_with_nul_does_not_fill_the_component(self):
+ finding = self._parse_one(purl="pkg:npm/a%00b@1.0")
+ self.assertIsNone(finding.component_name)
+ self.assertIsNone(finding.component_version)
+
+ def test_parse_json_purl_fills_the_version_only_for_its_own_package(self):
+ for fields, name, version in [
+ # A different component keeps its own (missing) version.
+ ({"component_name": "foo", "purl": "pkg:npm/bar@2.0"}, "foo", None),
+ # The same component gets the purl's version.
+ ({"component_name": "lodash", "purl": "pkg:npm/lodash@4.17.20"}, "lodash", "4.17.20"),
+ # Blank component fields count as unset, so the purl fills both.
+ ({"component_name": " ", "component_version": " ", "purl": "pkg:npm/lodash@4.17.20"}, "lodash", "4.17.20"),
+ ]:
+ with self.subTest(fields=fields):
+ finding = self._parse_one(**fields)
+ self.assertEqual(name, finding.component_name)
+ self.assertEqual(version, finding.component_version)
+
+ @override_settings(V3_FEATURE_LOCATIONS=True)
+ def test_parse_json_package_parts_without_a_component_are_logged(self):
+ with self.assertLogs("dojo.tools.generic.json_parser", "WARNING") as logs:
+ finding = self._parse_one(purl_type="npm", namespace="@x")
+ self.assertEqual([], self._dependency_locations(finding))
+ self.assertIn("the finding names no component", "\n".join(logs.output))