diff --git a/components/package.json b/components/package.json index f960d3ac200..57ac3662ac5 100644 --- a/components/package.json +++ b/components/package.json @@ -1,6 +1,6 @@ { "name": "defectdojo", - "version": "3.4.304", + "version": "3.5.0-dev", "license": "BSD-3-Clause", "private": true, "dependencies": { diff --git a/docs/assets/images/edit_ignore_delete_records.png b/docs/assets/images/edit_ignore_delete_records.png index e7a26f5e1c1..2eaae73490d 100644 Binary files a/docs/assets/images/edit_ignore_delete_records.png and b/docs/assets/images/edit_ignore_delete_records.png differ diff --git a/docs/assets/images/edit_ignore_delete_records_2.png b/docs/assets/images/edit_ignore_delete_records_2.png index b00ce391eaf..55093738bb2 100644 Binary files a/docs/assets/images/edit_ignore_delete_records_2.png and b/docs/assets/images/edit_ignore_delete_records_2.png differ diff --git a/docs/content/admin/sso/PRO__login_settings.md b/docs/content/admin/sso/PRO__login_settings.md index ce0067916d7..7d58c8df972 100644 --- a/docs/content/admin/sso/PRO__login_settings.md +++ b/docs/content/admin/sso/PRO__login_settings.md @@ -1,25 +1,35 @@ --- title: "Login Settings" -description: "Control the DefectDojo login page: disable password login, auto-redirect to SSO, session length, and JIT user creation" +description: "Control the DefectDojo login page: disable password login, auto-redirect to SSO, session length, idle timeout, and JIT user creation" weight: 20 audience: pro --- Once you have an SSO provider working, **Login Settings** controls how the DefectDojo login page itself behaves — whether username/password login is still offered, whether visitors are sent straight to your identity provider, how long a session lasts, and whether SSO can create accounts on the fly. Open-source DefectDojo does not include these controls — see [Authorized Users](/admin/user_management/os__authorized_users/). -Find it under **Connect > Authorization > Login Settings**. Only a **Superuser** can change it. +Find it under **Connect > Authorization > Login Settings**. Changing it takes the **Tuner Edit** permission, which every Superuser has. ![Login Settings](images/pro_login_settings.png) ## Settings -- **Session Age** — how long a session stays valid before the user must log in again. Choices range from 30 minutes to 30 days; the default is **12 hours**. +- **Session Age**: the longest a login lasts, counted from the moment the user logged in. When it passes, the user is signed out and signs in again, however active they have been. Choices range from 30 minutes to 30 days; the default is **12 hours**. +- **Idle Timeout**: signs a user out after this long without a request to DefectDojo. Choices are **Off** (the default), 15 minutes, 30 minutes, and 1, 2, 4 or 8 hours. With **Off**, only Session Age ends a session. When both are set, whichever comes first ends it. See [Session expiry](#session-expiry). - **Show Username and Password Fields** — when enabled, the username and password fields are shown on the login page. - **Allow Login via Username and Password** — when enabled, classic username/password authentication is allowed and its login button is shown. Turn this **off** once SSO is working to make the instance SSO-only. See [Making the instance SSO-only](#making-the-instance-sso-only) for the safe way to do this. - **Allow Password Reset** — when enabled, users can start the password-reset flow from the login page. This only applies to local passwords; SSO users reset credentials at their identity provider. - **Create User on Successful Login** — when enabled, a user authenticated by SSO who does not yet exist in DefectDojo is created automatically with minimal permissions (just-in-time provisioning). When disabled, only users who already exist in DefectDojo can sign in via SSO. Pair this with a **Default group** (below) so new users are not stranded with no access. - **Auto-Redirect to SSO Login Page** — when enabled, visiting DefectDojo redirects straight to the SSO login instead of showing the DefectDojo login form. **This only works when exactly one SSO connection is configured.** Always keep the [login fallback](#login-fallback) in mind before enabling it. +## Session expiry + +Session Age and Idle Timeout apply to every login, whether by username and password or by SSO. A user whose session ends, for either reason, is taken to the login page on their next click, and lands back on the page they were on once they sign in again. + +- Only requests the user makes count as activity. A page left open in a browser tab still reaches the Idle Timeout, even though it keeps refreshing its notification count in the background. +- Activity is recorded at most once a minute, so an idle session can end up to a minute before the Idle Timeout, never after it. +- A change to either setting reaches every running server within a minute, and applies to sessions that are already signed in. Shortening Session Age signs out anyone whose login is older than the new value. +- API tokens are not sessions: neither setting affects automation that calls the API with a token. + ## Default access for new users **Create User on Successful Login** decides *whether* an SSO user is provisioned; it does not grant them any access. A newly created user with no group membership sees an empty dashboard. Configure a **Default group** and **Default group role** on the System Settings page so every new user gets a sensible baseline — the mechanism is described in full under [Default access for SSO-provisioned users](/admin/sso/pro__saml/#default-access-for-sso-provisioned-users). 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 a3a61781b5e..8e6bb2defea 100644 --- a/docs/content/admin/user_management/PRO__custom_rbac_roles.md +++ b/docs/content/admin/user_management/PRO__custom_rbac_roles.md @@ -198,7 +198,7 @@ A user's Configuration Permissions are the union of what their own Global Roles 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. +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, inactive users included, so a user who is reactivated later finds their access in place; the Global Role they had before (Reader, Maintainer, Owner, or none) is left exactly as it was. Superusers are the exception: they already hold every permission, so no role is created from a superuser's own Configuration Permissions, and removing their superuser status later leaves them only the roles they were given. A Group a superuser belongs to is still converted for its other members. 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. diff --git a/docs/content/admin/user_management/user_permission_chart.md b/docs/content/admin/user_management/user_permission_chart.md index 50685b6c0eb..1c17d5ebfce 100644 --- a/docs/content/admin/user_management/user_permission_chart.md +++ b/docs/content/admin/user_management/user_permission_chart.md @@ -45,8 +45,8 @@ The five roles below are DefectDojo Pro's **built-in roles**. They are locked pr | | Edit Benchmarks | | ☑️ | ☑️ | ☑️ | | | | Delete Benchmarks | | | ☑️ | ☑️ | | | | View Note History | ☑️ | ☑️ | ☑️ | ☑️ | | -| | Add, Edit, Delete Own Notes | ☑️ | ☑️ | ☑️ | ☑️ | ☑️ | -| | Edit Other Notes | | ☑️ | ☑️ | ☑️ | ☑️ | +| | Add, Edit, Delete Own Notes | ☑️ | ☑️ | ☑️ | ☑️ | | +| | Edit Other Notes | | ☑️ | ☑️ | ☑️ | | | | Delete Other Notes | | | ☑️ | ☑️ | | 1. A user who is assigned permissions at the Asset level only cannot view the Organization it is contained in. diff --git a/docs/content/asset_modelling/PRO__archive.md b/docs/content/asset_modelling/PRO__archive.md new file mode 100644 index 00000000000..2f5a73a5190 --- /dev/null +++ b/docs/content/asset_modelling/PRO__archive.md @@ -0,0 +1,101 @@ +--- +title: "Archive" +description: "Archive Organizations, Assets, Engagements, Tests and Findings instead of deleting them, restore them during the retention period, and remove old Findings automatically with Triage Engine rules" +audience: pro +weight: 8 +--- + +**Archive** removes an Organization, Asset, Engagement, Test or Finding from view without deleting it. Archived data stays in the database for a fixed retention period, and you can restore it at any point during that period. When the period ends, DefectDojo deletes the archived data permanently, together with its audit history. + +Archive is turned on from the **Archive** row on the **Settings > Feature Flags** page. Turning the flag off stops new archives but does not bring archived data back, and archived data is still deleted on its scheduled date. + +## What archiving does + +When you archive an object, DefectDojo archives it and everything under it: + +- Archiving an Organization archives its Assets, their Engagements, Tests and Findings. +- Archiving an Asset archives its Engagements, Tests and Findings. +- Archiving an Engagement archives its Tests and Findings. +- Archiving a Test archives its Findings. + +The records attached to those objects are archived with them, for example endpoint and location statuses, vulnerability IDs, Jira links, deduplication signatures, and memberships. + +Archived data does not appear in: + +- list and detail pages, search, dashboards, metrics and reports +- the API (`/api/v2/` returns 404 for an archived object) +- deduplication and reimport: a new scan never matches an archived Finding, and you cannot import into an archived Engagement or Test +- Triage Engine rules: archived Findings never enter a rule run + +The only place archived data appears is the **Archived** page. + +If a Finding you archive is the original of a duplicate cluster, DefectDojo promotes one of the remaining duplicates to be the new original, the same way it does when you delete a Finding. Restoring the archived Finding does not relink the cluster. + +## The retention period + +Archived data is kept for 2 months by default. Self-hosted installs can change this with the `DD_ARCHIVE_RETENTION_MONTHS` environment variable (minimum 1). The period cannot be shortened for data that is already archived. + +Every archive has a purge date. A daily task permanently deletes archives whose purge date has passed. This removes the archived objects, everything under them, and their audit history. Nothing remains that refers to them, and they cannot be recovered. + +Because of this, DefectDojo asks you to confirm the retention period every time you archive. + +## Archived data and your license + +Archived data does not count toward your license. Archived Findings, Endpoints and Assets leave license usage when they are archived and count again if you restore them. Archived rows still take up database storage until they are purged, so a database size limit still includes them. While your license is over its limit and blocking changes, archiving still works, so it is a way back under the limit. Restoring an archive adds its objects back to your usage, so it is blocked until you are under the limit again. + +## Archiving an object + +You need permission to delete an object to archive it. + +1. Open the gear menu on the object, either in a list or on its detail page, and choose **Archive**. +2. The dialog shows how many objects will be archived and the date they will be permanently deleted. +3. Enter a reason. It is recorded with the archive and shown on the **Archived** page and in the audit log. +4. Tick the box to confirm you understand the data is deleted after the retention period, then choose **Archive**. + +The object disappears straight away. Everything under it is archived in the background, so a large Asset or Organization can take a few minutes to finish. Until it does, its archive shows **Archiving** on the **Archived** page, and the Engagements, Tests and Findings not yet reached are still visible in lists, metrics and the API, and can still be changed. They are hidden as the background work reaches them. + +To archive several Findings at once, select them in a Findings list and choose **Archive** from the bulk actions. The selected Findings form one archive and are restored together. + +## Restoring archived data + +The **Archived** page lists every archive you have permission to restore. For each one it shows what was archived, who archived it and why, when, how it was archived (user interface, API or Triage Engine rule) and its purge date. + +Expand an archive to browse the objects it holds. Choose **Unarchive** from its menu to restore it. Restoring brings back the whole tree that was archived together. It runs in the background: the archive shows **Restoring** until everything is back, then leaves the list. An archive that is still archiving or restoring cannot be restored until that finishes. + +An object archived on its own before its parent was archived keeps its own archive. Restoring the parent does not restore it, so restore it separately from the **Archived** page. + +The reverse is refused: an archive cannot be restored while something above it is still archived, because its objects would come back under a parent that stays hidden. Restore the parent's archive first. The API refuses it with `400` and `"code": "parent_archived"`. + +## Removing old Findings with Triage Engine rules + +Triage Engine has two actions for retention, available while Archive is turned on: + +- **Archive Findings** archives the Findings that reach it, with the reason you set. Each rule run makes one archive holding every Finding it archived, and restoring that archive brings them all back. +- **Delete Findings Permanently** deletes each Finding that reaches it. This cannot be undone. + +Both actions require you to tick a confirmation in the node before the rule can be saved. + +A typical retention rule runs on a schedule and archives Findings older than a set age: + +1. **On a Schedule** trigger, for example daily. +2. **If / Filter**: `Date` **is more than N days ago**, with N set to `365`. +3. **Archive Findings**, with a reason such as "Retention: older than 365 days". + +## Excess duplicates are archived, not deleted + +When **Delete Duplicate Findings** is turned on in System Settings, DefectDojo removes the oldest duplicates of a Finding once it has more than **Maximum Duplicates**. While Archive is turned on, those duplicates are archived instead of deleted, so a deduplication run that marked the wrong Findings as duplicates can be undone. With Archive turned off they are deleted, as before. + +The duplicates removed on one day share one archive, listed on the **Archived** page with the source **Deduplication**. It is kept for the same retention period as any other archive, counted from the last duplicate added to it. + +Before you restore one of these archives, turn off **Delete Duplicate Findings** or raise **Maximum Duplicates**. Otherwise the restored duplicates are still over the maximum, and the next run, within a minute, archives them again. The same applies to any archive that holds duplicates of a Finding that is still live: restoring it can put that Finding over its maximum. + +## API + +Each of `/api/v2/product_types/`, `/api/v2/products/`, `/api/v2/engagements/`, `/api/v2/tests/` and `/api/v2/findings/` has two actions for a single object: + +- `GET {id}/archive_preview/` returns what an archive would hide and the purge date. +- `POST {id}/archive/` archives the object. The body is `{"reason": "...", "acknowledge_retention": true}`. Without `acknowledge_retention: true` the request is refused with `400` and `"code": "acknowledge_required"`. It returns `202` with the archive record: the object is hidden already, and the record's `state` is `archiving` until everything under it is archived, then `archived`. + +Archives are listed at `/api/v2/archive/records/`. `GET /api/v2/archive/records/{id}/tree/?model=finding` browses an archive's contents, and `POST /api/v2/archive/records/{id}/unarchive/` starts restoring it and returns `202` with the record in state `restoring`. The record is deleted once everything is back. Restoring an archive that is still archiving or restoring is refused with `400` and `"code": "archive_busy"`. + +A new Asset or Organization cannot reuse the name of an archived one. Restore the archived one or choose another name. diff --git a/docs/content/asset_modelling/engagements_tests/OS__findings.md b/docs/content/asset_modelling/engagements_tests/OS__findings.md index 61f1c4d0721..d4a02abc96e 100644 --- a/docs/content/asset_modelling/engagements_tests/OS__findings.md +++ b/docs/content/asset_modelling/engagements_tests/OS__findings.md @@ -28,6 +28,8 @@ Overall, Findings are designed to work with the Asset Hierarchy to standardize y Findings are accessible via the sidebar. The submenu provides access to Open and Closed Findings, All Findings (regardless of Open or Closed status), [Risk Accepted Findings](/triage_findings/findings_workflows/os__risk_acceptance/), as well as Findings Templates. Individual Findings are also accessible from within the Test that contains them. +Through the API, `/api/v2/findings/?engagement=` lists the Findings of one Engagement, `?product=` those of one Asset and `?prod_type=` those of one Organization. Each takes a comma-separated list of ids and combines with every other filter. The longer `test__engagement`, `test__engagement__product` and `test__engagement__product__prod_type` names do the same. + ![image](images/osfindings_ss1.png) ### Permissions diff --git a/docs/content/asset_modelling/engagements_tests/PRO__assets.md b/docs/content/asset_modelling/engagements_tests/PRO__assets.md index 72ead5323e8..a152070e552 100644 --- a/docs/content/asset_modelling/engagements_tests/PRO__assets.md +++ b/docs/content/asset_modelling/engagements_tests/PRO__assets.md @@ -100,7 +100,8 @@ Asset views contain a variety of tables and charts to interpret an Asset’s sta - **Finding Severity Breakdown** - A graph of the Findings within the Asset, organized by severity. - **Finding Distribution** - - A breakdown of the Findings within the Asset, organized by status (e.g., Active, Mitigated, Static, and Dynamic) + - A ring of the Asset's Findings by status: Active, Mitigated, and Other (Findings that are neither active nor mitigated, such as false positives and accepted risks). The three add up to the total, which the panel header shows, and each status in the legend links to the Asset's Findings with that status. + - Beside the ring, the share of Findings reported by static and by dynamic tools. Each is measured against the total on its own, because a Finding can be both static and dynamic, or neither. - **All Engagements** - A list of Engagements contained within the Asset. diff --git a/docs/content/asset_modelling/engagements_tests/PRO__findings.md b/docs/content/asset_modelling/engagements_tests/PRO__findings.md index b01fdb6b183..d368bcb29f8 100644 --- a/docs/content/asset_modelling/engagements_tests/PRO__findings.md +++ b/docs/content/asset_modelling/engagements_tests/PRO__findings.md @@ -26,6 +26,8 @@ Overall, Findings are designed to work with the Asset Hierarchy to standardize y ## Accessing Findings Findings are accessible from the sidebar under **Act > Explore > Findings**. The submenu provides access to **Active** and **Mitigated** Findings, **All** Findings (regardless of Open or Closed status), Finding **Groups**, **Risk Accepted** Findings and Finding **Templates**; new Findings are created with the **New Finding** button at the top of a Findings list. Individual Findings are also accessible from within the Test that contains them. +Through the API, `/api/v2/findings/?engagement=` lists the Findings of one Engagement, `?product=` those of one Asset and `?prod_type=` those of one Organization. Each takes a comma-separated list of ids and combines with every other filter. The longer `test__engagement`, `test__engagement__product` and `test__engagement__product__prod_type` names do the same. + [Risk Accepted Findings](/triage_findings/findings_workflows/os__risk_acceptance/) are listed under **Act > Explore > Findings > Risk Accepted**; the Risk Acceptance records themselves are managed from **Act > Risk Acceptances** in the sidebar. ![image](images/profindings_ss1.png) diff --git a/docs/content/asset_modelling/locations/PRO__location_map.md b/docs/content/asset_modelling/locations/PRO__location_map.md index 05c0bbe7951..c99d6b38df4 100644 --- a/docs/content/asset_modelling/locations/PRO__location_map.md +++ b/docs/content/asset_modelling/locations/PRO__location_map.md @@ -58,15 +58,15 @@ The page has three panels: actions on the left, the canvas in the middle, and de Click a node to select it. The right panel then shows: - **Type** and **Value** of the node. -- For a single location: **Status Here** and its **Relationship** to the Asset. Users who can edit Locations on the Asset can change the relationship with **Set a Relationship** and **Save**, or remove it with **Clear**. +- For a single location: **Status Here** and its **Relationship** to the Asset. Users who can edit Locations on the Asset can change the relationship: pick one under **Set a Relationship** and click **Save**. Picking **Clear** and saving removes it. A Reader sees the relationship but cannot change it. - For a group: how many **Locations** and **Children** it has. -- **Findings**: active findings by severity, split into **Direct** (on this node) and **Beneath** (on nodes under it), with a **Total**. +- **Findings**: active findings by severity, split into **Direct** (on this node) and **Beneath** (on nodes under it), with a **Total**. A group also shows how many **Locations Beneath** it. With no node selected, the right panel shows a short **Map Overview**. Click the empty canvas or use **Clear the selection** to deselect. When a single location is selected, the left panel offers two actions: -- **View Findings** opens the active findings recorded on that location. +- **View Findings** opens the active findings recorded on that location. To see all of its findings, whatever their status, use the **Total Findings** count in the location list (see [Narrowing the Map with the List](#narrowing-the-map-with-the-list)). - **Copy Value** copies the full location value to the clipboard. A group is drawn from several locations, so these actions apply only to a single location. @@ -88,7 +88,7 @@ Under **Canvas**, **Type Boundaries** turns the dashed regions on or off. The ** ### Adding Locations -**New Location** and **Add Existing** work as they do on the table page, and the map redraws to include what you added. See [Working with URLs](/asset_modelling/locations/pro__working_with_urls/) for the permissions each one needs. +**New Location** and **Add Existing** work as they do on the table page, and the map redraws to include what you added. They appear only for users who can add or edit Locations on the Asset, so a Reader does not see them. See [Working with URLs](/asset_modelling/locations/pro__working_with_urls/) for the permissions each one needs. ## Turning the Map Off diff --git a/docs/content/asset_modelling/locations/PRO__locations_overview.md b/docs/content/asset_modelling/locations/PRO__locations_overview.md index 098fc81eca7..e4f3fd2562f 100644 --- a/docs/content/asset_modelling/locations/PRO__locations_overview.md +++ b/docs/content/asset_modelling/locations/PRO__locations_overview.md @@ -91,7 +91,8 @@ Existing parsers have been updated to emit Location data when the feature flag i ## Where to Go Next -- **Enable the feature** — turn Locations on yourself from the [Feature Flags page](/admin/feature_flags/pro__feature_flags/); no Support request is required. -- **Migrate from Endpoints** — see [Migrating from Endpoints](../pro__migrating_from_endpoints) for what the migration preserves, and how the legacy Endpoint API behaves afterward. -- **Day-to-day URL workflows** — see [Working with URLs](../pro__working_with_urls). -- **SBOMs and dependencies** — see [Working with SBOMs](../pro__working_with_sboms). +- **Enable the feature**: turn Locations on yourself from the [Feature Flags page](/admin/feature_flags/pro__feature_flags/); no Support request is required. +- **Migrate from Endpoints**: see [Migrating from Endpoints](../pro__migrating_from_endpoints) for what the migration preserves, and how the legacy Endpoint API behaves afterward. +- **Day-to-day URL workflows**: see [Working with URLs](../pro__working_with_urls). +- **See an Asset's Locations on a map**: see [Location Map](../pro__location_map). +- **SBOMs and dependencies**: see [Working with SBOMs](../pro__working_with_sboms). diff --git a/docs/content/asset_modelling/locations/PRO__working_with_urls.md b/docs/content/asset_modelling/locations/PRO__working_with_urls.md index fc9909f3e7a..79ad743939c 100644 --- a/docs/content/asset_modelling/locations/PRO__working_with_urls.md +++ b/docs/content/asset_modelling/locations/PRO__working_with_urls.md @@ -22,12 +22,12 @@ When you create or upload a URL, DefectDojo parses it into the structured fields When the Locations feature is enabled, the navigation exposes: -- **Act > Explore > Attack Surface > Locations** — A list of every Location across both URL and Dependency subtypes. Filter by type, status, Asset, Finding, or tag. -- **Act > Explore > Attack Surface > URLs** — A scoped list of URL Locations only. This is the closest analogue to the old Endpoints page. +- **Act > Explore > Attack Surface > Locations**: A list of every Location across both URL and Dependency subtypes. Filter by type, status, Asset, Finding, or tag. +- **Act > Explore > Attack Surface > URLs**: A scoped list of URL Locations only. This is the closest analogue to the old Endpoints page. - **New URL** on the URLs list — A form to create a single URL with structured fields, tags, and optional Asset/Finding associations. -- **Locations on an Asset** — From any Asset, the **Locations** tab shows the URLs and Dependencies attached to that Asset, with status counts and quick actions. -- **The Location page** — Click a Location's value in any list, or choose **View Location** from its row menu, to open the page for that Location. It is the counterpart of the old Endpoint page: the Location's type, value, tags and overall status; when it was first and last seen; each Asset that references it, in a table you can search, sort and filter by Asset, status, relationship, and first or last seen date; its **Custom Fields**; and every Finding found there. -- **Custom Fields on a Location** — The name and value pairs the Endpoint page called metadata appear in the same **Custom Fields** card the Endpoint page uses. A Location is shared by every Asset that recorded the same value, so a field can belong to one Asset. The card shows the Asset under each value, adds the Asset to a tab's name when two Assets use the same field name, and only shows fields for Assets you can view. Add a field with the **+** on the card or **Add Custom Field** in the Location's menu, and choose the Asset it belongs to, or none. Edit or delete a field from its gear menu. Changing a field needs edit permission on the Location, and on its Asset when the field belongs to one. +- **Locations on an Asset**: From any Asset, the **Locations** tab shows the URLs and Dependencies attached to that Asset, with status counts and quick actions. +- **The Location page**: Click a Location's value in any list, or choose **View Location** from its row menu, to open the page for that Location. It is the counterpart of the old Endpoint page: the Location's type, value, tags and overall status; when it was first and last seen; each Asset that references it, in a table you can search, sort and filter by Asset, status, relationship, and first or last seen date; its **Custom Fields**; and **Affected Open Findings**, the active Findings recorded there. That table always filters on Status Active. To see every Finding on a Location, whatever its status, open a list of Locations (an Asset's Locations table, **Choose Locations** on the [Location Map](../pro__location_map), or **All Locations**) and click the location's count in the **Total Findings** column: it opens the Findings list filtered to that Location. **Mitigated Findings** does the same for the Findings recorded there as Mitigated, False Positive, Risk Accepted or Out of Scope. +- **Custom Fields on a Location**: The name and value pairs the Endpoint page called metadata appear in the same **Custom Fields** card the Endpoint page uses. A Location is shared by every Asset that recorded the same value, so a field can belong to one Asset. The card shows the Asset under each value, adds the Asset to a tab's name when two Assets use the same field name, and only shows fields for Assets you can view. Add a field with the **+** on the card or **Add Custom Field** in the Location's menu, and choose the Asset it belongs to, or none. Edit or delete a field from its gear menu. Changing a field needs edit permission on the Location, and on its Asset when the field belongs to one. ![image](images/PRO_Location_Page.png) diff --git a/docs/content/automation/api/api-v2-docs.md b/docs/content/automation/api/api-v2-docs.md index f1e0942f751..0adfa8de780 100644 --- a/docs/content/automation/api/api-v2-docs.md +++ b/docs/content/automation/api/api-v2-docs.md @@ -116,82 +116,76 @@ Plan the change before making it: pick a window, ask token owners to rotate firs ## Sample Code -Here are some simple python examples and their results produced against -the `/users` endpoint: +Here are two simple Python examples against the `/findings` endpoint. Any user with an API token can run them: the response holds only the findings that user has permission to view. + +The first lists findings and prints each one's id and title: {{< highlight python >}} import requests -url = 'http://127.0.0.1:8000/api/v2/users' -headers = {'content-type': 'application/json', - 'Authorization': 'Token c8572a5adf107a693aa6c72584da31f4d1f1dcff'} -r = requests.get(url, headers=headers, verify=True) # set verify to False if ssl cert is self-signed +url = 'https://defectdojo.example.com/api/v2/findings/' +headers = { + 'content-type': 'application/json', + 'Authorization': 'Token ', +} +r = requests.get(url, headers=headers, verify=True) # set verify to False if the SSL certificate is self-signed +r.raise_for_status() -for key, value in r.__dict__.items(): - print(f"'{key}': '{value}'") - print('------------------') +for finding in r.json()['results']: + print(finding['id'], finding['title']) {{< /highlight >}} -This code will return the list of all the users defined in DefectDojo. -The json object result looks like: +The output is one line per finding: -{{< highlight json >}} - [ - { - "first_name": "Tyagi", - "id": 22, - "last_login": "2019-06-18T08:05:51.925743", - "last_name": "Paz", - "username": "dev7958" - }, - { - "first_name": "saurabh", - "id": 31, - "last_login": "2019-06-06T11:44:32.533035", - "last_name": "", - "username": "saurabh.paz" - } - ] +{{< highlight text >}} +101 Example SQL Injection +102 Example Cross-Site Scripting {{< /highlight >}} -Here is another example against the `/users` endpoint, this -time we will filter the results to include only the users whose user -name includes `jay`: +The second filters the list to active findings of High severity, using query parameters. Severity values are case-sensitive: use `Critical`, `High`, `Medium`, `Low` or `Info`. {{< highlight python >}} import requests -url = 'http://127.0.0.1:8000/api/v2/users/?username__contains=jay' -headers = {'content-type': 'application/json', - 'Authorization': 'Token c8572a5adf107a693aa6c72584da31f4d1f1dcff'} -r = requests.get(url, headers=headers, verify=True) # set verify to False if ssl cert is self-signed +url = 'https://defectdojo.example.com/api/v2/findings/' +headers = { + 'content-type': 'application/json', + 'Authorization': 'Token ', +} +params = {'severity': 'High', 'active': 'true'} +r = requests.get(url, headers=headers, params=params, verify=True) # set verify to False if the SSL certificate is self-signed +r.raise_for_status() -for key, value in r.__dict__.items(): - print(f"'{key}': '{value}'") - print('------------------') +print(r.json()) {{< /highlight >}} -The json object result is: +List endpoints are paginated. The response holds the total `count`, the URLs of the `next` and `previous` pages (`null` when there are none), and the current page of findings in `results`. `prefetch` stays empty unless you ask for related objects with the `prefetch` parameter. Each finding carries every field of the Finding model; the example below is trimmed to a few of them: {{< highlight json >}} -[ - { - "first_name": "Jay", - "id": 22, - "last_login": "2015-10-28T08:05:51.925743", - "last_name": "Paz", - "username": "jay7958" - }, - { - "first_name": "", - "id": 31, - "last_login": "2015-10-13T11:44:32.533035", - "last_name": "", - "username": "jay.paz" - } -] +{ + "count": 2, + "next": null, + "previous": null, + "prefetch": {}, + "results": [ + { + "id": 101, + "title": "Example SQL Injection", + "severity": "High", + "active": true + }, + { + "id": 103, + "title": "Example Server-Side Request Forgery", + "severity": "High", + "active": true + } + ] +} {{< /highlight >}} +A page holds 25 results by default. Set `limit` to change the page size and `offset` to start further in, or follow the `next` URL until it is `null` to read every page. + See [Django Rest Framework\'s documentation on interacting with an API](https://www.django-rest-framework.org/) for additional examples and tips. @@ -313,7 +307,7 @@ A reimport can be performed by specifying the names of these entities in the API When `auto_create_context` is `True`, the Organization, Asset and Engagement will be created if they do not already exist. Make sure your user has sufficient [permissions](/admin/user_management/about_perms_and_roles/) to create an Asset/Organization. -When `do_not_reactivate` is `True`, the importing/reimporting will ignore uploaded active findings and not reactivate previously closed findings, while still creating new findings if there are new ones. You will get a note on the finding to explain that it was not reactivated for that reason. +When `do_not_reactivate` is `True`, a reimport keeps a closed finding closed when it appears in the report again, instead of reactivating it, and adds a note to that finding saying it was not reactivated. New findings in the report are still created, and findings that are already open are processed as usual. A first import has no existing findings to reactivate, so the option only changes a reimport. A reimport will automatically select the latest test inside the provided engagement that satisfies the provided `scan_type` and (optionally) provided `test_title`. diff --git a/docs/content/connectors/toolreference/checkmarx_one.md b/docs/content/connectors/toolreference/checkmarx_one.md index f452dd8b3ee..e7bab3090d8 100644 --- a/docs/content/connectors/toolreference/checkmarx_one.md +++ b/docs/content/connectors/toolreference/checkmarx_one.md @@ -36,6 +36,22 @@ Notes on **Track Scanned Branches**: - Turning the toggle off later is safe: per-branch engagements simply stop receiving imports and the default engagement resumes on the next sync. - Connectors reconcile state on the sync schedule. Branch tracking makes each sync complete across branches; it does not make data real-time between syncs. +#### **Selecting scans by Initiator** + +Some scans can only be told apart by who started them, for example scans run by a QA service account on branches your **Branch** filter does not select. The optional **Initiator** field selects those scans by the Checkmarx scan's Initiator (the user or service that started it). + +- **Initiator** is a regular expression, matched anywhere in the Initiator unless you anchor it with `^` and `$`. For example `^qa-scanner-\d{2}$` matches `qa-scanner-01`, and `Sécurité` matches any Initiator containing that word. +- Matching **ignores case and accents** on both sides, so `Sécurité` also matches `SECURITE` and `securite`. +- The pattern uses [RE2 syntax](https://github.com/google/re2/wiki/Syntax). An invalid pattern is rejected when you save the connector. + +How it combines with the branch fields: + +- A scan is selected when its branch matches **Branch** **or** its Initiator matches **Initiator**. For a branch the Branch field selects, the connector imports that branch's latest completed scan, as it does without an Initiator. For any other branch, it imports the latest completed scan whose Initiator matches. A wildcard Branch that matches nothing no longer skips the sync, because the Initiator-matched branches still import. +- Setting **Initiator** always syncs per branch, as if **Track Scanned Branches** were on: each selected branch gets its own engagement, so Initiator-matched scans and Branch-matched scans never share an engagement or close each other's findings. +- With **Branch** blank, only Initiator-matched scans are imported. You can use this for a separate connector that covers only those scans, alongside your existing connector. +- Only the most recent 1,000 completed scans of each project are searched for a matching Initiator. A branch whose last matching scan is older than that stops receiving updates, the same way a branch that stops being scanned does. +- Clearing **Initiator** later does not close anything: the engagements of branches it alone selected stop receiving imports and keep their last-known findings, which you can review and close in bulk. If **Track Scanned Branches** is off, the default engagement resumes on the next sync, as when that toggle is turned off. + #### **Result States** By default, the connector imports findings in every Checkmarx result state except **Not Exploitable**. To import only findings in some states, fill in the optional **Result States** field. diff --git a/docs/content/connectors/toolreference/security_hub.md b/docs/content/connectors/toolreference/security_hub.md index 9aa1dbf6697..de28ee05030 100644 --- a/docs/content/connectors/toolreference/security_hub.md +++ b/docs/content/connectors/toolreference/security_hub.md @@ -54,7 +54,7 @@ DefectDojo can pull Findings from more than one region using Security Hub's **cr #### Asset Grouping (optional) -By default the connector creates one Record, and so one asset, per AWS account and region, named `Account ()`. Every finding in that account and region lands on that one asset, whatever resource it is about. The **Asset Grouping** field, under **Import Filters** on the connector form, can split findings more finely: +By default the connector creates one Record, and so one asset, per AWS account and region, named `Account ()`. Every finding in that account and region lands on that one asset, whatever resource it is about. The **Asset Grouping** field, under **Import Configuration** on the connector form, can split findings more finely: | Asset Grouping | Records | Findings are imported on | |---|---|---| @@ -90,6 +90,27 @@ Turn on **Parent Asset per AWS Account** to add one asset per AWS account above Existing account and region assets keep their names and identities, so existing mappings, findings and history are unchanged; they gain the parent on the next discovery. The account asset is placed in the same organization as the account's other assets (see *Organization Placement*). Turning the toggle off stops creating account assets; relationships already drawn stay until you remove them. +#### ECR Images in Use Only (optional) + +Amazon Inspector reports the vulnerable packages of the images that it monitors in ECR. It also reports images that nothing runs. Turn on **ECR Images in Use Only** to skip the findings of ECR images that do not run. The toggle is under **Import Configuration** on the connector form. + +If the toggle is on, the connector checks each finding on an ECR container image. It imports the finding only if Amazon Inspector reports the image in an ECS task or an EKS pod. The connector asks Inspector at each Sync. + +* The connector imports findings on every other resource as before. This includes the ECR repository's own findings. +* Only Amazon ECS tasks and Amazon EKS pods count, also on AWS Fargate. An image that runs anywhere else counts as not running. This includes AWS Lambda, AWS App Runner, Docker on EC2 and self-managed Kubernetes. +* Amazon Inspector updates the in-use data once a day. For a new image, the data can take up to 36 hours to show. After an image stops, its findings can stay open for up to two days. See [Mapping container images to running containers](https://docs.aws.amazon.com/inspector/latest/user/scanning-ecr.html#ecr-mapping-container-images) in the Amazon Inspector documentation. +* Amazon Inspector ECR scanning must be on in every region that you sync. Amazon Inspector supplies in-use data only for ECR repositories that use continuous scanning. If a repository uses scan on push, the next Sync closes its image findings, even if the image runs. +* Amazon Inspector does not support last-in-use tracking for multi-architecture images. Before you rely on the toggle for such an image, check that Amazon Inspector shows it as in use. +* At the next Sync, DefectDojo closes the imported findings of images that do not run. If an image runs again, its findings reopen at a later Sync. +* A scheduled job can run for a short time only. Its image findings can then open and close between Syncs. +* Under the **Resource** grouping, some ECR repositories do not become a Record. This applies to a repository with no running image and no finding of its own. +* Security Hub does not show which images run. The connector reads this from Amazon Inspector with `inspector2:ListCoverage`. It reads at least once per account and region for each Record that it syncs. For an ECR repository Record, it reads only that repository. Under a finer grouping, discovery also reads once per account and region. +* For member accounts, the access key must belong to the Amazon Inspector delegated administrator account in each region. +* If the toggle is on, saving the connection checks the `inspector2:ListCoverage` permission. If the permission goes missing later, the Sync of each Record with image findings fails. Under a finer grouping, discovery also fails. The findings stay as they were. +* If Inspector covers no images at all for an account and region, the Sync of its Records with image findings fails, with an error that says why. This happens when ECR scanning is off in that region. It also happens when the access key is not the delegated administrator for a member account. The findings of a failed Record stay as they were. The failure covers all findings of the Record, not only image findings. Under the default grouping, the EC2, IAM and AWS Config findings of that account and region also stop syncing until Inspector can see the account, or until you turn the toggle off. Under the **Resource Type** grouping, only the ECR repository Record fails. Under the **Resource** grouping, the connector asks Inspector about each ECR repository on its own. If Inspector covers no image for a repository, only the Record of that repository fails. Discovery leaves out the image findings of an account and region that Inspector cannot see. +* The connector never imports every image as a fallback. +* The connector still reads every finding from Security Hub. That part of a Sync takes as long as before. Each Record with image findings adds at least one Amazon Inspector request. Under the **Resource** grouping, that means about one request per ECR repository Record. Only the number of imported findings goes down. + #### Organization Placement (optional) New assets from a Security Hub connection are placed in the organization named **Security Hub Connector**. The **Organization Placement** field can instead place each AWS account's assets in an organization of its own: @@ -191,6 +212,21 @@ With the **Resource** grouping, each ECR repository asset gets the repository's The permission is optional. Without it, or for a repository in an account these credentials cannot read, the connector makes one refused request per account, logs the first, and leaves those repositories with only the tags their findings carry. When a lookup fails for another reason, the repository asset keeps the tags it already has until the next discovery; if Amazon ECR cannot be reached at all, or five lookups in a row fail, the remaining repositories are left the same way for that discovery. The connector makes one request per discovered repository on each discovery, paced at ten per second, so a discovery across 2,000 repositories takes about three minutes longer. The other groupings make no ECR request. +#### Optional permissions: Amazon Inspector + +**ECR Images in Use Only** needs `inspector2:ListCoverage`: + +``` +{ + "Sid": "AWSSecurityHubConnectorInspector", + "Effect": "Allow", + "Action": [ + "inspector2:ListCoverage" + ], + "Resource": "*" +} +``` + #### Compliance Tags Each Finding is tagged with the compliance requirements Security Hub relates its control to, for diff --git a/docs/content/connectors/toolreference/servicedesk_plus.md b/docs/content/connectors/toolreference/servicedesk_plus.md index 479c6cfec1a..1c1f9958c0e 100644 --- a/docs/content/connectors/toolreference/servicedesk_plus.md +++ b/docs/content/connectors/toolreference/servicedesk_plus.md @@ -62,8 +62,30 @@ This maps to the request **Status** field by name. The defaults use the built-i - **False Positive Mapping**: `Closed` - **Risk Accepted Mapping**: `On Hold` +### Custom Fields (optional) + +Use the mapping's **Custom Fields** step to send fields beyond the subject, description, priority, status and group. This is how you set a **Requester** when your ServiceDesk Plus account makes it mandatory. Each custom-field mapping has a **Source** (an attribute of the Finding, Test, Engagement or Asset being pushed, or a **Static value**), a **Value**, and a **Vendor Field**, which is the ServiceDesk Plus field name. + +DefectDojo sends each value in the shape the ServiceDesk Plus API expects for that field: + +| Vendor Field | Sent as | Example | +|---|---|---| +| `requester`, `technician`, `category`, `subcategory`, `item`, `site`, `impact`, `urgency`, `level`, `mode`, `request_type`, `service_category`, `template` | a reference by name | `requester` with the static value `Security Bot` | +| `.` | a reference by that attribute | `requester.email_id` with the static value `security-bot@example.com` | +| a name starting with `udf_` | a user-defined field | `udf_sline_301` | +| anything else | plain text | `email_ids_to_notify` | + +A user-defined field's name (such as `udf_sline_301`) is the key ServiceDesk Plus shows for it under `udf_fields` when you read a request through its API. + +**Apply On** decides when a mapping is sent: **Creation** (the default) or **Update**. ServiceDesk Plus does not use the transition options; status changes travel with the update. The fields DefectDojo already sends always take precedence: a custom mapping cannot replace the subject, the description or the request ID, or a priority, status or group set by the other mapping steps. + +### Ticket Template (optional) + +Attach a **Ticket Template** in the mapping's **Ticket Template** step to replace the built-in request subject and description, for Findings, Finding Groups or both. The template's summary becomes the request subject, shortened to 250 characters, and its description becomes the request description. A part the template leaves blank uses the built-in content: the Finding title or Finding Group name for the subject. If a template fails to render, the request is sent with the built-in subject or description instead, and the failure is listed under Errors. + A few ServiceDesk Plus-specific behaviors to be aware of: - Updates sync the full request content - unlike most trackers, ServiceDesk Plus allows the subject and description to be edited after creation. - Requests are closed rather than deleted when a Finding is removed; requests already Closed or Resolved are left untouched. - If your account makes fields mandatory on closure (for example a resolution), a close pushed from DefectDojo may be rejected by those rules and will appear in the Integration errors table. +- If ServiceDesk Plus rejects a request because a mandatory field is missing, the Integration errors table names the field, for example `Please fill the mandatory fields (fields requester)`. Add that field under **Custom Fields**. diff --git a/docs/content/federal_compliance/eu_cra_setup.md b/docs/content/federal_compliance/eu_cra_setup.md index bdae639d008..88be7d63093 100644 --- a/docs/content/federal_compliance/eu_cra_setup.md +++ b/docs/content/federal_compliance/eu_cra_setup.md @@ -74,7 +74,7 @@ No restart is needed. Leave every other flag as it is; this guide does not use t | Expected | Where | | --- | --- | | **CRA Article 14 Reports** under **Act** | The left menu | -| A **Regulatory** card near the top of every Asset page | Any Asset | +| A **Regulatory** card on every Asset page | Any Asset | | An **EU Evidence** tab | Asset > **Asset Overview** > **Compliance** tab > **Compliance Profile** | ### Step 2 — Enable per-release SBOMs @@ -179,8 +179,9 @@ Repeat Steps 6 to 9 for each product. For many products, use the API (see the en ### Step 6 — Record the product's regulatory facts In the left menu, go to **Explore > Assets** and select the product's Asset. The **Regulatory** -card is near the top of the Asset page, below the Engagements, Tests, Findings and Locations tiles. -Fill it in and select **Submit**: +card is on the Asset page below the Details row, after the Open Finding Severity meter, the Asset +Overview tabs and the Organization Memberships and Asset Identity cards. Fill it in and select +**Submit**: | Field | Example | What it records | | --- | --- | --- | diff --git a/docs/content/get_started/about/defectdojo_versions.md b/docs/content/get_started/about/defectdojo_versions.md index fe1d94a2cbe..56b2d0cfb9c 100644 --- a/docs/content/get_started/about/defectdojo_versions.md +++ b/docs/content/get_started/about/defectdojo_versions.md @@ -97,7 +97,7 @@ Each table below covers one part of a vulnerability management program. **The va | Configuration advice | ❌ Not included | ✅ [Sensei Advisor](/sensei/sensei_advisor/) suggests changes to your deduplication and prioritization settings and shows how each would affect your own findings. Nothing changes until someone applies it, and applied changes can be undone (Sensei license) | Tuning is based on what your own data shows, not guesswork, and it is safe to try. | | AI inventory | ❌ Not included | ✅ **AI Inventory** lists the AI coding assistants, MCP servers, models and AI packages found in your code repositories. **AI Governance Policies** create a finding whenever an unapproved one turns up | You can see, and govern, the AI your developers are actually using. | -Sensei is never trained on your data, and DefectDojo's AI provider does not train on it either. On DefectDojo Cloud, Sensei is an AI agent built by DefectDojo. It runs inside your dedicated DefectDojo instance and reaches its models within DefectDojo's Google Cloud environment, using credentials unique to your instance, and your data is never mixed with another customer's. On self-hosted Pro, Sensei is the agent harness and you bring the model: connect the provider you already use (Anthropic, OpenAI, Amazon Bedrock or Google Vertex AI) under [AI Model Settings](/sensei/ai_model_settings/), and model traffic goes directly from your instance to your own provider account. +Sensei is never trained on your data, and DefectDojo's AI provider does not train on it either. On DefectDojo Cloud, Sensei is an AI agent built by DefectDojo. It runs inside your dedicated DefectDojo instance and reaches its models within DefectDojo's Google Cloud environment, using credentials unique to your instance, and your data is never mixed with another customer's. On self-hosted Pro, Sensei is the agent harness and you bring the model: connect the provider you already use (Anthropic, OpenAI, Amazon Bedrock or Google Vertex AI) under [AI Model Settings](/sensei/ai_model_settings/), and model traffic goes directly from your instance to your own provider account. Cloud customers who want the same can [bring their own model](/sensei/ai_model_settings/#bring-your-own-model-on-defectdojo-cloud) as an enterprise option: contact [hello@defectdojo.com](mailto:hello@defectdojo.com). ### Compliance and product security diff --git a/docs/content/get_started/about/faq.md b/docs/content/get_started/about/faq.md index 9ca28dad06b..aa83a3b7efc 100644 --- a/docs/content/get_started/about/faq.md +++ b/docs/content/get_started/about/faq.md @@ -38,7 +38,7 @@ In Pro, check these in order: 1. **Feature Flags.** Many capabilities are switched on by an administrator under [Feature Flags](/admin/feature_flags/pro__feature_flags/). 2. **License.** Sensei and PSIRT need an add-on license. 3. **Permissions.** The sidebar only shows the pages your role can open. -4. **Deployment.** A few settings exist only on self-hosted instances (for example AI Model Settings and Version Manager) or only on DefectDojo Cloud (for example Firewall Rules). +4. **Deployment.** A few settings exist only on self-hosted instances (for example Version Manager, and AI Model Settings unless your Cloud instance brings its own model) or only on DefectDojo Cloud (for example Firewall Rules). In Open-Source, check that the page describes Open-Source: the edition switch at the top of the navigation shows the pages for the edition you choose. @@ -275,6 +275,8 @@ Sensei is never trained on your data. DefectDojo uses your data only to complete Model traffic goes directly from your instance to your own provider account and never passes through DefectDojo. Your provider's data terms apply to those requests. +**On DefectDojo Cloud, you can also bring your own model.** This is an enterprise option: DefectDojo turns on [AI Model Settings](/sensei/ai_model_settings/#bring-your-own-model-on-defectdojo-cloud) for your instance, and Sensei then runs only on the provider account you connect there. To request it, contact [hello@defectdojo.com](mailto:hello@defectdojo.com). + If you connect an AI assistant through the [MCP server](/metrics_reports/ai/mcp_server_pro/), the data it reads goes to that assistant, under that assistant's terms. ### Does Sensei change my code without approval? diff --git a/docs/content/get_started/pro/onprem/hardware_sizing.md b/docs/content/get_started/pro/onprem/hardware_sizing.md index 730892672f2..348cd40d13c 100644 --- a/docs/content/get_started/pro/onprem/hardware_sizing.md +++ b/docs/content/get_started/pro/onprem/hardware_sizing.md @@ -102,6 +102,18 @@ These four decide how many database connections the application holds open, so t Change either one with `dojo-compose-cli environment add` and restart, as above. A harakiri restart frees the process but does not remove the cause. If one shows up in the log, send the stack traces that come with it to support, since they show where the process was stuck. +**Slow database statements.** A harakiri restart stops the web process, but not the database query it was running: PostgreSQL keeps working on that query until it finishes. From 3.5.0, PostgreSQL itself stops any single statement of a web request that runs past 30 seconds under that service's harakiri (870 seconds on `dojo`, 1740 seconds on `dojo-import-scan`), and the request answers with a 503 that says it took too long. Where harakiri is unset or `0`, the limit is 1740 seconds. Imports and exports may get a higher limit than other requests, but never more than 30 seconds under their own service's harakiri, because harakiri ends the request at that point anyway: with the defaults they get the same limit as everything else on that service. Background tasks, migrations and management commands are never limited. + +The limit is sent to PostgreSQL as a connection startup option. If DefectDojo reaches PostgreSQL through PgBouncer or another connection pooler, the pooler may refuse those connections or drop the option; set the limits below to `0` there. + +| Setting | Service | Default | Notes | +| --- | --- | --- | --- | +| `DD_DATABASE_WEB_STATEMENT_TIMEOUT` | `dojo` (web) | 30 seconds under `DD_UWSGI_HARAKIRI`, or 1740 when that is unset or `0` | Seconds; `0` turns the limit off. | +| `DD_DATABASE_IMPORT_STATEMENT_TIMEOUT` | `dojo-import-scan` (imports) | 30 seconds under `DD_UWSGI_IMPORT_HARAKIRI`, or 1740 when that is unset or `0` | Docker Compose only. Seconds; `0` turns the limit off. | +| `DD_DATABASE_WEB_LONG_STATEMENT_TIMEOUT` | `dojo` (web) | 30 seconds under the service's harakiri, or 1740 when that is unset or `0` | The limit for imports and exports. A value above the default is capped at it; `0` means the default. | + +On Kubernetes, the limit follows each uWSGI pool's harakiri from the chart values: 870 seconds on the web pool and 1740 seconds on the import pool, or 1740 seconds on the web pool when `django.uwsgiImport.enabled` is `false`. To change it, set `DD_DATABASE_WEB_STATEMENT_TIMEOUT` in `config.extra` or `django.extraEnv`. The chart has no separate setting for the import pool, so a value set there applies to the web and import pools alike. If a request answers with this 503, the log names the request: send it to support, since a statement that runs this long is a performance problem to fix. + **Import memory and shutdown.** uWSGI also replaces a process whose memory grows too large. `DD_UWSGI_RELOAD_ON_RSS` (MB) replaces it after a request leaves it above the limit: it stops taking requests, and the ones still running on its other threads get `DD_UWSGI_WORKER_RELOAD_MERCY` seconds before uWSGI kills it, failing them with a 502 (the log says `NO MERCY`). `DD_UWSGI_EVIL_RELOAD_ON_RSS` (MB) is a safety limit: a process that passes it is killed within about a second, and every request it is serving fails with a 502. The log says `EVIL RELOAD`, and Docker shows no restart. Imports need far more memory than pages, so from 3.4.300 the import service can be set on its own: | Setting | Service | Default | Notes | diff --git a/docs/content/get_started/pro/onprem/restoring.md b/docs/content/get_started/pro/onprem/restoring.md index 1d3157f9b86..3bcb461c634 100644 --- a/docs/content/get_started/pro/onprem/restoring.md +++ b/docs/content/get_started/pro/onprem/restoring.md @@ -16,7 +16,7 @@ A restore puts back the same four things a backup captures: the configuration an **There are two databases.** Alongside the main database (`dojodb` by default), the orchestrator keeps its state in a second one with `-ddorch` added to the name, for example `dojodb-ddorch`. Back up and restore both. If the second one is missing, the orchestrator creates an empty one at startup and the state of background work from before the backup is lost. -**Restore the keys with the database.** Encrypted credentials only decrypt with the keys that encrypted them. Connector and integration credentials use `DD_SECRET_KEY`, and Tool Configuration credentials use `DD_CREDENTIAL_AES_256_KEY`. A mismatch fails quietly rather than with an error, which is why it gets its own entry under [Common problems](#common-problems). +**Restore the keys with the database.** Encrypted credentials only decrypt with the keys that encrypted them. Connector and integration credentials, and the secrets in the SSO, OIDC, LDAP and email settings, use `DD_SECRET_KEY`. Tool Configuration credentials use `DD_CREDENTIAL_AES_256_KEY`. A mismatch fails quietly rather than with an error, which is why it gets its own entry under [Common problems](#common-problems). ## Docker Compose @@ -228,7 +228,9 @@ Work through the [restore checklist](/get_started/pro/onprem/backing_up/#confirm ## Common problems -**Connector or integration credentials are blank, with no error.** `DD_SECRET_KEY` is not the one they were encrypted with. They decrypt to nothing rather than failing, so the connector looks unconfigured. Tool Configuration credentials are unaffected. The stored values are intact: put the original key back and restart, and they read correctly again. +**Connector or integration credentials are blank, or SSO, LDAP or email stops working, with no error.** `DD_SECRET_KEY` is not the one they were encrypted with. They decrypt to nothing rather than failing, so the connector looks unconfigured and the sign-in and email settings fall back to their defaults. DefectDojo logs a warning that it could not decrypt a stored value. Tool Configuration credentials are unaffected. The stored values are intact: put the original key back and restart, and they read correctly again. + +**Changing `DD_SECRET_KEY` on purpose.** Set the new key in `DD_SECRET_KEY`, and keep the old one in `DD_SECRET_KEY_FALLBACKS`. That variable takes a comma-separated list. Values encrypted under a key in that list still read correctly, and new values use the new key. Keep the old key in the list until nothing still depends on it. **Tool Configuration credentials do not work.** `DD_CREDENTIAL_AES_256_KEY` does not match. As above, the data is intact and the original key fixes it. diff --git a/docs/content/import_data/import_scan_files/github_actions.de.md b/docs/content/import_data/import_scan_files/github_actions.de.md index 3c0bf44fab5..e5257fea14e 100644 --- a/docs/content/import_data/import_scan_files/github_actions.de.md +++ b/docs/content/import_data/import_scan_files/github_actions.de.md @@ -5,7 +5,7 @@ description: Importieren Sie Scan-Ergebnisse aus einem GitHub Actions-Workflow m weight: 3 --- -Die offizielle [DefectDojo-Action](https://github.com/DefectDojo/defectdojo-action) importiert einen Scan-Bericht aus einem GitHub Actions-Workflow in DefectDojo. Sie funktioniert mit DefectDojo Pro und der Open-Source-Edition und liest jedes von DefectDojo unterstützte Berichtsformat, einschließlich SARIF. +Die offizielle [DefectDojo-Action](https://github.com/marketplace/actions/defectdojo) importiert einen Scan-Bericht aus einem GitHub Actions-Workflow in DefectDojo. Sie funktioniert mit DefectDojo Pro und der Open-Source-Edition und liest jedes von DefectDojo unterstützte Berichtsformat, einschließlich SARIF. Jeder Lauf führt einen Reimport in denselben Test durch. Befunde, die der Scanner nicht mehr meldet, werden daher geschlossen, und jeder Befund behält seinen Verlauf. Die Action erfasst Build, Branch, Commit und Repository im Test und kann den Workflow-Schritt fehlschlagen lassen, wenn Befunde mit einem gewählten Schweregrad aktiv bleiben. diff --git a/docs/content/import_data/import_scan_files/github_actions.es.md b/docs/content/import_data/import_scan_files/github_actions.es.md index 26dd9087c0f..9a9e987374c 100644 --- a/docs/content/import_data/import_scan_files/github_actions.es.md +++ b/docs/content/import_data/import_scan_files/github_actions.es.md @@ -5,7 +5,7 @@ description: Importe los resultados de un escaneo desde un flujo de trabajo de G weight: 3 --- -La [acción de DefectDojo](https://github.com/DefectDojo/defectdojo-action) oficial importa un informe de escaneo en DefectDojo desde un flujo de trabajo de GitHub Actions. Funciona con DefectDojo Pro y con la edición Open Source, y lee todos los formatos de informe que DefectDojo admite, incluido SARIF. +La [acción de DefectDojo](https://github.com/marketplace/actions/defectdojo) oficial importa un informe de escaneo en DefectDojo desde un flujo de trabajo de GitHub Actions. Funciona con DefectDojo Pro y con la edición Open Source, y lee todos los formatos de informe que DefectDojo admite, incluido SARIF. Cada ejecución reimporta en el mismo Test, de modo que los Hallazgos que el escáner ya no informa se cierran y cada Hallazgo conserva su historial. La acción registra la compilación, la rama, el commit y el repositorio en el Test, y puede hacer fallar el paso del flujo de trabajo cuando siguen activos Hallazgos con una severidad determinada. diff --git a/docs/content/import_data/import_scan_files/github_actions.fr.md b/docs/content/import_data/import_scan_files/github_actions.fr.md index 354b50aa640..2967d064763 100644 --- a/docs/content/import_data/import_scan_files/github_actions.fr.md +++ b/docs/content/import_data/import_scan_files/github_actions.fr.md @@ -5,7 +5,7 @@ description: Importez des résultats de scan depuis un workflow GitHub Actions a weight: 3 --- -L'[action DefectDojo](https://github.com/DefectDojo/defectdojo-action) officielle importe un rapport de scan dans DefectDojo depuis un workflow GitHub Actions. Elle fonctionne avec DefectDojo Pro et avec l'édition Open Source, et lit tous les formats de rapport pris en charge par DefectDojo, y compris SARIF. +L'[action DefectDojo](https://github.com/marketplace/actions/defectdojo) officielle importe un rapport de scan dans DefectDojo depuis un workflow GitHub Actions. Elle fonctionne avec DefectDojo Pro et avec l'édition Open Source, et lit tous les formats de rapport pris en charge par DefectDojo, y compris SARIF. Chaque exécution effectue un Reimport dans le même Test : les Constatations que le scanner ne signale plus sont donc clôturées, et chaque Constatation conserve son historique. L'action enregistre le build, la branche, le commit et le dépôt sur le Test, et peut faire échouer l'étape du workflow lorsque des Constatations d'une sévérité choisie restent actives. diff --git a/docs/content/import_data/import_scan_files/github_actions.it.md b/docs/content/import_data/import_scan_files/github_actions.it.md index 05cc7e7a90d..e5a5225f77d 100644 --- a/docs/content/import_data/import_scan_files/github_actions.it.md +++ b/docs/content/import_data/import_scan_files/github_actions.it.md @@ -5,7 +5,7 @@ description: Importare i risultati di una scansione da un workflow di GitHub Act weight: 3 --- -L'[azione DefectDojo](https://github.com/DefectDojo/defectdojo-action) ufficiale importa un report di scansione in DefectDojo da un workflow di GitHub Actions. Funziona con DefectDojo Pro e con l'edizione Open Source, e legge ogni formato di report supportato da DefectDojo, incluso SARIF. +L'[azione DefectDojo](https://github.com/marketplace/actions/defectdojo) ufficiale importa un report di scansione in DefectDojo da un workflow di GitHub Actions. Funziona con DefectDojo Pro e con l'edizione Open Source, e legge ogni formato di report supportato da DefectDojo, incluso SARIF. Ogni esecuzione effettua un Reimport nello stesso Test: i Riscontri che lo scanner non segnala più vengono chiusi e ciascun Riscontro conserva la propria cronologia. L'azione registra nel Test la build, il branch, il commit e il repository, e può far fallire il passaggio del workflow quando restano attivi Riscontri con una severità scelta. diff --git a/docs/content/import_data/import_scan_files/github_actions.ja.md b/docs/content/import_data/import_scan_files/github_actions.ja.md index c7ef52d4d05..4dc5c1a52c3 100644 --- a/docs/content/import_data/import_scan_files/github_actions.ja.md +++ b/docs/content/import_data/import_scan_files/github_actions.ja.md @@ -4,7 +4,7 @@ description: 公式の DefectDojo アクションを使用して、GitHub Action weight: 3 --- -公式の [DefectDojo アクション](https://github.com/DefectDojo/defectdojo-action)は、GitHub Actions のワークフローからスキャンレポートを DefectDojo にインポートします。DefectDojo Pro とオープンソース版の両方で動作し、SARIF を含め、DefectDojo がサポートするすべてのレポート形式を読み取ります。 +公式の [DefectDojo アクション](https://github.com/marketplace/actions/defectdojo)は、GitHub Actions のワークフローからスキャンレポートを DefectDojo にインポートします。DefectDojo Pro とオープンソース版の両方で動作し、SARIF を含め、DefectDojo がサポートするすべてのレポート形式を読み取ります。 実行のたびに同じテストへ再インポートされるため、スキャナーが報告しなくなった検出事項はクローズされ、各検出事項は履歴を保持します。このアクションは、ビルド、ブランチ、コミット、リポジトリをテストに記録し、選択した重大度の検出事項がアクティブなまま残っている場合には、ワークフローのステップを失敗させることもできます。 diff --git a/docs/content/import_data/import_scan_files/github_actions.md b/docs/content/import_data/import_scan_files/github_actions.md index e6ebb5a1b05..a3c73986ba2 100644 --- a/docs/content/import_data/import_scan_files/github_actions.md +++ b/docs/content/import_data/import_scan_files/github_actions.md @@ -4,7 +4,7 @@ description: "Import scan results from a GitHub Actions workflow with the offici weight: 3 --- -The official [DefectDojo action](https://github.com/DefectDojo/defectdojo-action) imports a scan report into DefectDojo from a GitHub Actions workflow. It works with DefectDojo Pro and the Open Source edition, and reads every report format DefectDojo supports, including SARIF. +The official [DefectDojo action](https://github.com/marketplace/actions/defectdojo) imports a scan report into DefectDojo from a GitHub Actions workflow. It works with DefectDojo Pro and the Open Source edition, and reads every report format DefectDojo supports, including SARIF. Each run reimports into the same Test, so Findings the scanner no longer reports are closed and each Finding keeps its history. The action records the build, branch, commit and repository on the Test, and can fail the workflow step when Findings at a chosen severity remain active. diff --git a/docs/content/import_data/import_scan_files/github_actions.pt-br.md b/docs/content/import_data/import_scan_files/github_actions.pt-br.md index a48c85ec34d..d4777a82d4e 100644 --- a/docs/content/import_data/import_scan_files/github_actions.pt-br.md +++ b/docs/content/import_data/import_scan_files/github_actions.pt-br.md @@ -5,7 +5,7 @@ description: Importe resultados de scan de um workflow do GitHub Actions com a a weight: 3 --- -A [action oficial do DefectDojo](https://github.com/DefectDojo/defectdojo-action) importa um relatório de scan para o DefectDojo a partir de um workflow do GitHub Actions. Ela funciona com o DefectDojo Pro e com a edição Open Source, e lê todos os formatos de relatório compatíveis com o DefectDojo, incluindo SARIF. +A [action oficial do DefectDojo](https://github.com/marketplace/actions/defectdojo) importa um relatório de scan para o DefectDojo a partir de um workflow do GitHub Actions. Ela funciona com o DefectDojo Pro e com a edição Open Source, e lê todos os formatos de relatório compatíveis com o DefectDojo, incluindo SARIF. Cada execução faz um reimport no mesmo Teste, de modo que os Achados que o scanner deixou de reportar são fechados e cada Achado mantém o seu histórico. A action registra o build, o branch, o commit e o repositório no Teste, e pode fazer a etapa do workflow falhar quando ainda houver Achados ativos na severidade escolhida. diff --git a/docs/content/import_data/import_scan_files/github_actions.zh-hans.md b/docs/content/import_data/import_scan_files/github_actions.zh-hans.md index 107438cc71c..3c154603553 100644 --- a/docs/content/import_data/import_scan_files/github_actions.zh-hans.md +++ b/docs/content/import_data/import_scan_files/github_actions.zh-hans.md @@ -4,7 +4,7 @@ description: 使用官方 DefectDojo Action 从 GitHub Actions 工作流导入 weight: 3 --- -官方 [DefectDojo Action](https://github.com/DefectDojo/defectdojo-action) 可在 GitHub Actions 工作流中将扫描报告导入 DefectDojo。它同时适用于 DefectDojo Pro 和开源版,并可读取 DefectDojo 支持的所有报告格式,包括 SARIF。 +官方 [DefectDojo Action](https://github.com/marketplace/actions/defectdojo) 可在 GitHub Actions 工作流中将扫描报告导入 DefectDojo。它同时适用于 DefectDojo Pro 和开源版,并可读取 DefectDojo 支持的所有报告格式,包括 SARIF。 每次运行都会重新导入到同一个测试中,因此扫描器不再报告的发现项会被关闭,且每个发现项都会保留其历史记录。该 Action 会在测试上记录构建、分支、提交和仓库信息,并且当所选严重程度的发现项仍处于活动状态时,可以让工作流步骤失败。 diff --git a/docs/content/navigation/PRO__landing_page.md b/docs/content/navigation/PRO__landing_page.md new file mode 100644 index 00000000000..5a210cbcb78 --- /dev/null +++ b/docs/content/navigation/PRO__landing_page.md @@ -0,0 +1,32 @@ +--- +title: "Default Landing Page" +description: "Choose the page DefectDojo Pro opens after you sign in" +audience: pro +weight: 7 +--- + +Your **Default Landing Page** is the page DefectDojo Pro opens after you sign in. Set it on your **User Profile** page: select your username at the bottom of the sidebar, then **User Profile**, and find the field under **Signing In**. + +The landing page is saved to your account, not to the browser, so it follows you to every browser and device you sign in from. + +## Choosing a page + +When no page is chosen, signing in opens **Home**, as it always has. + +The list holds every page in the sidebar that your account can open, named by where it sits in the menu (for example **Explore > Findings > All**). Type in the list to filter it. Clear the field to go back to Home. Select **Submit** to save it with the rest of your profile. A change takes effect the next time you sign in, in any browser. + +## When it applies + +The landing page applies when signing in has nowhere else to go: + +- After signing in from the sign-in page, including single sign-on, multi-factor verification and a required password change. +- After a single sign-on started from your identity provider (an app tile in its portal, for example). An administrator who sets `DD_LOGIN_REDIRECT_URL` to another page sends these sign-ins there instead. +- When your session ended while you were on Home, or you open a bookmark of it. + +When you were sent to the sign-in page from a specific page (a link to a finding, for example), signing in takes you back to that page instead. + +If your landing page later becomes unavailable to you (a role is removed, or the feature behind it is turned off), signing in opens Home instead, without an error. + +## Turning it off + +The field is controlled by the **Default Landing Page** feature flag (see [Feature Flags](/admin/feature_flags/pro__feature_flags/)), which is on by default. While it is off, the field is hidden and signing in opens Home. Saved landing pages are kept, and apply again when the flag is turned back on. diff --git a/docs/content/navigation/PRO__page_layouts.md b/docs/content/navigation/PRO__page_layouts.md index b510355da10..2eebdd88f1a 100644 --- a/docs/content/navigation/PRO__page_layouts.md +++ b/docs/content/navigation/PRO__page_layouts.md @@ -137,4 +137,4 @@ Which widgets a page offers depends on the page, and **Add Widget** shows what i Some widgets appear only when the feature behind them is available to you. For example, the **Automation History** widget on the Asset and Finding pages shows what the rules engine did to that record and why, so it is offered only when Triage Engine is turned on and you hold Rule View. -The record cards at the top of the Asset page (**Organization Memberships**, **Asset Identity**, **Exposure** and **Regulatory**) and the **Member Assets** table at the top of the Organization page are widgets as well. The shipped layouts place them first, and layouts saved before they were widgets have them added at the top, above their own widgets, so nothing that was on the page goes missing. Like any widget you can move, resize or remove them. Each one is shown only while the feature behind it is turned on. +The record cards on the Asset page (**Organization Memberships**, **Asset Identity**, **Exposure** and **Regulatory**) and the **Member Assets** table at the top of the Organization page are widgets as well. The shipped Asset layout starts with the Open Finding Severity meter and places the cards after the Details row: Organization Memberships and Asset Identity side by side, then Exposure and Regulatory at full width. The shipped Organization layout places Member Assets first. A layout saved before the cards were widgets had them added above its own widgets, so nothing that was on the page went missing; one that was not rearranged since has them moved below its widgets, in the same arrangement as the shipped layout. Like any widget you can move, resize or remove them. Each one is shown only while the feature behind it is turned on. diff --git a/docs/content/navigation/PRO__sidebar.md b/docs/content/navigation/PRO__sidebar.md index 8d5e44d522c..1c4b907a021 100644 --- a/docs/content/navigation/PRO__sidebar.md +++ b/docs/content/navigation/PRO__sidebar.md @@ -98,7 +98,7 @@ The AI capabilities sit together in their own section rather than being spread t **AI Proposals** lists the verdicts the Triage Engine's AI steps have left for a person to confirm. It appears only when your license includes Sensei. -**MCP** is shown to superusers, and to everyone once the MCP server is enabled. **AI Model Settings** appears only on on-premise instances, because DefectDojo manages the model credentials on Cloud. +**MCP** is shown to superusers, and to everyone once the MCP server is enabled. **AI Model Settings** appears on on-premise instances. On Cloud, DefectDojo manages the model credentials, so it appears only on an instance that [brings its own model](/sensei/ai_model_settings/#bring-your-own-model-on-defectdojo-cloud). ## Act diff --git a/docs/content/releases/os_upgrading/3.5.md b/docs/content/releases/os_upgrading/3.5.md index a65adf278f0..5b298097dda 100644 --- a/docs/content/releases/os_upgrading/3.5.md +++ b/docs/content/releases/os_upgrading/3.5.md @@ -2,6 +2,10 @@ title: 'Upgrading to DefectDojo Version 3.5.x' toc_hide: true weight: -20261005 -description: No special instructions. +description: Findings API engagement, product and prod_type filters now apply. --- -There are no special instructions for upgrading to 3.5.x. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/3.5.0) for the contents of the release. +There are no special instructions for upgrading to 3.5.x beyond the API change below. Check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/3.5.0) for the contents of the release. + +## Findings API: `engagement`, `product` and `prod_type` filters now apply + +Before 3.5.x, `/api/v2/findings/` ignored the `engagement`, `product` and `prod_type` query parameters and returned every Finding the caller could see. They are now applied, as short names for `test__engagement`, `test__engagement__product` and `test__engagement__product__prod_type`. A client that already sends them gets only the Findings of that Engagement, Asset or Organization, so expect a smaller `count` and different pages. A non-numeric value is now rejected with a 400. Clients that use the `test__engagement*` parameters see no change. diff --git a/docs/content/releases/pro/changelog.md b/docs/content/releases/pro/changelog.md index c0ef69cbde8..6efd70c883c 100644 --- a/docs/content/releases/pro/changelog.md +++ b/docs/content/releases/pro/changelog.md @@ -20,6 +20,38 @@ For Open Source release notes, please see the [Releases page on GitHub](https:// ## October 2026: v3.4 +### October 30, 2026: v3.4.304 + +New features: +* **(Connectors)** Ingestion Health has its own page, **Settings > Operations > Ingestion Health**, listing every source of Findings: each Upstream Connector, each scheduled Triage Engine rule, and each Asset and scan type imported through the API in the last 30 days. Each row shows the last run and its result, the baseline, when the next run is expected and any open anomalies. Three anomaly kinds join the zero-Findings check: **Volume Drop**, **Stale** and **Auth Failed**. Each source can have its own thresholds or be snoozed, the **Ingestion Health Warning** notification goes out once per anomaly, and the **Ingestion Health Status** dashboard widget counts the sources that need attention. Requires the **Ingestion Health** feature flag, off by default. See [Ingestion Health](/connectors/upstream/ingestion_health/#the-ingestion-health-page). + +Enhancements: +* **(Connectors)** A Connector shows when its next scheduled Discover or Sync will run. The Operations view labels its Type, Status and Trigger tags in the reader's language, and a Connector save that fails because the connector service cannot be reached says so in plain words. See [Managing Operations](/connectors/upstream/manage_operations/#when-the-next-run-is-scheduled). +* **(Assets)** The Open Finding Severity tile on the Asset page is one bar split by severity, with a legend across its full width and the total in the tile header, and its severity links filter correctly in every language. The Reachability Coverage panel leads with the Criticals confirmed reachable and the Criticals demoted, and shows coverage as one bar. See [Assets](/asset_modelling/engagements_tests/pro__assets/#asset-view) and [Reachability](/triage_findings/finding_scoring/reachability/#what-you-see). +* **(Dashboards)** Each dashboard tile keeps its actions in one **Widget Actions** menu, so its controls no longer shrink or overlap beside the title and freshness stamp. **Add to Report** moved into the same menu. See [Customizable Dashboards](/metrics_reports/dashboards/custom-dashboards/#step-4-arrange-then-save). +* **(Settings)** The Feature Flags page groups features by area, with an index beside the cards, an **X of Y Enabled** count, filters by state, and one-line descriptions that expand in place. See [Feature Flags](/admin/feature_flags/pro__feature_flags/#opening-the-feature-flags-page). +* **(Parsers)** A [Generic Findings Import](/supported_tools/parsers/file/generic/#package-details) JSON finding accepts a `purl` for its dependency, or `purl_type` and `namespace` alongside `component_name`, so the dependency location carries the right package type. +* **(Performance)** Reimporting large reports costs less per Finding, filtering the Findings list by mitigated date and requesting `?prefetch=endpoints` are faster, and updating or deleting a single Finding through the API no longer loads its related records one row at a time. + +Bug fixes: +* **(Permissions)** The built-in Writer role can add files to Findings, Tests, Engagements and Assets again. A Permissions dialog opened before its page finishes loading lists only that record's members. See [Set Permissions in Pro](/admin/user_management/pro_permissions_overhaul/). +* **(Deduplication)** After consolidating Findings, moving a tool back to **As the parser groups them** splits them on the next reimport, which reactivates each folded-away Finding with its locations. The first grouping change on an instance backfills existing Findings before it takes effect, so a reimport in between no longer closes Findings and opens copies. Reimporting an older Test whose Finding was folded into another keeps that Test's location active. On the Dedupe Pools page, a **Preview Re-run** is kept when **Add to Pool** finishes, a pool created during a refresh is listed, and a delete waits for the pool's members to load. See [Deduplication Tuning](/triage_findings/finding_deduplication/pro__deduplication_tuning/#changing-a-tools-grouping) and [Dedupe Pools](/triage_findings/finding_deduplication/pro__dedupe_pools/#adding-assets-and-previewing-first). +* **(Importers)** **Reimport Scan** works on a Test whose scan type differs from its test type name, such as a GitLab SAST Report. With Locations enabled, a dependency too long to store is left off its Finding with a warning in the log, instead of failing the whole import. See [Tests](/asset_modelling/engagements_tests/pro__tests/#reimporting-scan-results-ui). +* **(Findings)** Updating a Finding through the API returns its recalculated priority, and an update with `recalculate_hash_code` stores the recalculated hash and deduplicates on it. When a close is refused, **Close Finding** shows one message with the server's reason, and a refused note is shown under the **Note** field. +* **(Tables)** The **On**, **Before**, **After** and **During** filters on date-only fields match the day the viewer picked, including for viewers in a timezone ahead of the server's. See [Customizing Tables](/navigation/pro__table_customization/#filtering-columns). +* **(Licensing)** On an instance over its license limit, OAuth and OIDC sign-in works again, and only changes that add usage are blocked: new Findings, imports and reimports, new Assets, new users and new Connector operations. Revoking access, editing or deactivating users, and rotating tokens keep working. +* **(SSO)** The SAML settings help says that the NameID picks the account a login lands on, not a mapped Username or Email. See [SAML Configuration](/admin/sso/pro__saml/#which-account-a-login-lands-on). +* **(Navigation)** Every in-app walkthrough can be finished, starts on a record the user can act on, and describes what is on screen. A highlighted sidebar row stays in view when the sidebar footer grows. See [In-App Walkthroughs](/navigation/pro__walkthroughs/#task-walkthroughs). +* **(UI)** A dashboard tile's default title follows the reader's language, the toggles on Priority Insights and the Asset hierarchy panel are translated, and translated column headers fit in the Regulatory Scope widget. Screen readers announce whether a popup menu is open. A switch whose change is refused, such as a policy's **Enabled** switch on **Settings > AI Governance Policies**, flips back and shows one message. A long unbroken error wraps inside its message instead of widening the page, and the PSIRT **Applies To** panel keeps keyboard focus after each change. See [Language and Localization](/navigation/language_and_localization/#what-is-translated-and-what-is-not). + +Security fixes: +* **(Authentication)** A SAML logout message that cannot be read is refused with a 400 response. See [SAML Configuration](/admin/sso/pro__saml/). +* **(Authorization)** The Risk Acceptance page acts only on notes that belong to that Risk Acceptance. See [Risk Acceptances](/triage_findings/findings_workflows/pro__risk_acceptance/). +* **(Hardening)** Triage Engine Slack messages show Finding titles, Asset names and Card Facts as plain text. The Helm bootstrap scripts no longer print the admin password; they name the Secret that holds it and how to read it. The application images apply security updates to their bundled components. See [Send a Slack Message](/automation/triage_engine/node_reference/#send-a-slack-message) and [DefectDojo Pro Installation Guide (Kubernetes / Helm)](/get_started/pro/onprem/kubernetes/installing_on_kubernetes/). + +Behavior changes: +* **(Assets)** Unmanaged Devices is now called Asset Discovery: the sidebar entry under **Attack Surface**, the page and its walkthrough, and the feature flag's title on the Feature Flags page. Menu search still finds the page by its old name. See [Asset Discovery](/asset_modelling/pro__unmanaged_devices/). + ### October 29, 2026: v3.4.303 New features: diff --git a/docs/content/sensei/ai_model_settings.md b/docs/content/sensei/ai_model_settings.md index 1db81623b36..ff9cd3c839e 100644 --- a/docs/content/sensei/ai_model_settings.md +++ b/docs/content/sensei/ai_model_settings.md @@ -20,9 +20,36 @@ hardening suggestions and runtime checks. Every provider below works for all of You reach it from the sidebar under **Sensei + AI > AI Model Settings**. You need a global **Maintainer** or **Owner** role to change it. -> **On-premise only.** This page exists only on **on-premise** ("local") deployments, where you -> bring your own model. On DefectDojo **Cloud** the LLM is managed for you and this page is not -> shown. +> **Where this page appears.** Every **on-premise** ("local") deployment has this page, because +> there you always bring your own model. On DefectDojo **Cloud**, DefectDojo manages the model by +> default and the page is not shown. Cloud customers can also bring their own model: see +> [Bring your own model on DefectDojo Cloud](#bring-your-own-model-on-defectdojo-cloud). + +## Bring your own model on DefectDojo Cloud + +A DefectDojo Cloud instance can run Sensei, and the in-app AI features listed above, on your own +model provider account instead of the model DefectDojo provides. This is an enterprise option +that DefectDojo turns on for your instance. To request it, contact +[hello@defectdojo.com](mailto:hello@defectdojo.com). + +Once it is on, **AI Model Settings** appears in the sidebar and works as described on this page, +with these differences: + +- **Your instance uses only your model.** It never falls back to the model DefectDojo provides. + Until you save a working configuration, Sensei scans and fixes, and the other AI features, are + refused with a message that points you to this page. +- **Credentials are required.** A Cloud instance has no ambient cloud identity of yours to fall + back on, so every provider needs its own secret: an API key for Claude or OpenAI, an **AWS + Access Key ID** and **Secret Access Key** for Amazon Bedrock, or a **Service Account Key + (JSON)** for Google Vertex AI. The "leave the keys blank" setups described below for Bedrock and + Vertex apply only on-premise. +- **The API Base URL must be a public address.** A base URL that points at a private network + address, loopback or a cloud metadata service is refused when you save or test it, and again + every time the model is called. A custom Amazon Bedrock endpoint is not available on Cloud: + leave Bedrock's **API Base URL** blank. + +Model traffic goes from your DefectDojo Cloud instance to your own provider account, and your +provider's data terms apply to those requests. ## Providers diff --git a/dojo/__init__.py b/dojo/__init__.py index a51fc98afce..2681fcdfc4a 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.304" +__version__ = "3.5.0-dev" __url__ = "https://github.com/DefectDojo/django-DefectDojo" __docs__ = "https://documentation.defectdojo.com" diff --git a/dojo/db_migrations/0304_archive_columns.py b/dojo/db_migrations/0304_archive_columns.py new file mode 100644 index 00000000000..c7f8d8eaef5 --- /dev/null +++ b/dojo/db_migrations/0304_archive_columns.py @@ -0,0 +1,3339 @@ +# Generated by Django 5.2.17 on 2026-10-22 19:48 + +import pgtrigger.compiler +import pgtrigger.migrations +from django.db import migrations, models + +# Generated with `manage.py sqlmigrate` from the operations below, then frozen. +FORWARD_SQL = r""" +-- +-- Remove trigger insert_insert from model endpoint +-- +DROP TRIGGER IF EXISTS pgtrigger_insert_insert_c7973 ON "dojo_endpoint"; +-- +-- Remove trigger update_update from model endpoint +-- +DROP TRIGGER IF EXISTS pgtrigger_update_update_2b19a ON "dojo_endpoint"; +-- +-- Remove trigger delete_delete from model endpoint +-- +DROP TRIGGER IF EXISTS pgtrigger_delete_delete_dd1f9 ON "dojo_endpoint"; +-- +-- Remove trigger insert_insert from model engagement +-- +DROP TRIGGER IF EXISTS pgtrigger_insert_insert_125f1 ON "dojo_engagement"; +-- +-- Remove trigger update_update from model engagement +-- +DROP TRIGGER IF EXISTS pgtrigger_update_update_65136 ON "dojo_engagement"; +-- +-- Remove trigger delete_delete from model engagement +-- +DROP TRIGGER IF EXISTS pgtrigger_delete_delete_9f4df ON "dojo_engagement"; +-- +-- Remove trigger insert_insert from model finding +-- +DROP TRIGGER IF EXISTS pgtrigger_insert_insert_2fbbb ON "dojo_finding"; +-- +-- Remove trigger update_update from model finding +-- +DROP TRIGGER IF EXISTS pgtrigger_update_update_92175 ON "dojo_finding"; +-- +-- Remove trigger delete_delete from model finding +-- +DROP TRIGGER IF EXISTS pgtrigger_delete_delete_72933 ON "dojo_finding"; +-- +-- Remove trigger insert_insert from model finding_group +-- +DROP TRIGGER IF EXISTS pgtrigger_insert_insert_94bd2 ON "dojo_finding_group"; +-- +-- Remove trigger update_update from model finding_group +-- +DROP TRIGGER IF EXISTS pgtrigger_update_update_37b5f ON "dojo_finding_group"; +-- +-- Remove trigger delete_delete from model finding_group +-- +DROP TRIGGER IF EXISTS pgtrigger_delete_delete_24b41 ON "dojo_finding_group"; +-- +-- Remove trigger insert_insert from model product +-- +DROP TRIGGER IF EXISTS pgtrigger_insert_insert_d5d32 ON "dojo_product"; +-- +-- Remove trigger update_update from model product +-- +DROP TRIGGER IF EXISTS pgtrigger_update_update_e7040 ON "dojo_product"; +-- +-- Remove trigger delete_delete from model product +-- +DROP TRIGGER IF EXISTS pgtrigger_delete_delete_064dd ON "dojo_product"; +-- +-- Remove trigger insert_insert from model product_type +-- +DROP TRIGGER IF EXISTS pgtrigger_insert_insert_2d109 ON "dojo_product_type"; +-- +-- Remove trigger update_update from model product_type +-- +DROP TRIGGER IF EXISTS pgtrigger_update_update_a0136 ON "dojo_product_type"; +-- +-- Remove trigger delete_delete from model product_type +-- +DROP TRIGGER IF EXISTS pgtrigger_delete_delete_66b18 ON "dojo_product_type"; +-- +-- Remove trigger insert_insert from model test +-- +DROP TRIGGER IF EXISTS pgtrigger_insert_insert_ecfc1 ON "dojo_test"; +-- +-- Remove trigger update_update from model test +-- +DROP TRIGGER IF EXISTS pgtrigger_update_update_c40f8 ON "dojo_test"; +-- +-- Remove trigger delete_delete from model test +-- +DROP TRIGGER IF EXISTS pgtrigger_delete_delete_66d18 ON "dojo_test"; +-- +-- Add field archive_batch to app_analysis +-- +ALTER TABLE "dojo_app_analysis" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to app_analysis +-- +ALTER TABLE "dojo_app_analysis" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to dojometa +-- +ALTER TABLE "dojo_dojometa" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to dojometa +-- +ALTER TABLE "dojo_dojometa" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to endpoint +-- +ALTER TABLE "dojo_endpoint" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to endpoint +-- +ALTER TABLE "dojo_endpoint" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to endpoint_status +-- +ALTER TABLE "dojo_endpoint_status" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to endpoint_status +-- +ALTER TABLE "dojo_endpoint_status" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to endpointevent +-- +ALTER TABLE "dojo_endpointevent" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to endpointevent +-- +ALTER TABLE "dojo_endpointevent" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to engagement +-- +ALTER TABLE "dojo_engagement" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to engagement +-- +ALTER TABLE "dojo_engagement" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to engagement_presets +-- +ALTER TABLE "dojo_engagement_presets" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to engagement_presets +-- +ALTER TABLE "dojo_engagement_presets" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to engagementevent +-- +ALTER TABLE "dojo_engagementevent" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to engagementevent +-- +ALTER TABLE "dojo_engagementevent" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to finding +-- +ALTER TABLE "dojo_finding" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to finding +-- +ALTER TABLE "dojo_finding" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to finding_group +-- +ALTER TABLE "dojo_finding_group" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to finding_group +-- +ALTER TABLE "dojo_finding_group" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to finding_groupevent +-- +ALTER TABLE "dojo_finding_groupevent" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to finding_groupevent +-- +ALTER TABLE "dojo_finding_groupevent" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to findingevent +-- +ALTER TABLE "dojo_findingevent" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to findingevent +-- +ALTER TABLE "dojo_findingevent" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to findingvulnerabilityreference +-- +ALTER TABLE "dojo_findingvulnerabilityreference" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to findingvulnerabilityreference +-- +ALTER TABLE "dojo_findingvulnerabilityreference" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to jira_issue +-- +ALTER TABLE "dojo_jira_issue" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to jira_issue +-- +ALTER TABLE "dojo_jira_issue" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to jira_project +-- +ALTER TABLE "dojo_jira_project" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to jira_project +-- +ALTER TABLE "dojo_jira_project" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to languages +-- +ALTER TABLE "dojo_languages" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to languages +-- +ALTER TABLE "dojo_languages" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to locationfindingreference +-- +ALTER TABLE "dojo_locationfindingreference" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to locationfindingreference +-- +ALTER TABLE "dojo_locationfindingreference" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to locationproductreference +-- +ALTER TABLE "dojo_locationproductreference" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to locationproductreference +-- +ALTER TABLE "dojo_locationproductreference" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to objects_product +-- +ALTER TABLE "dojo_objects_product" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to objects_product +-- +ALTER TABLE "dojo_objects_product" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to product +-- +ALTER TABLE "dojo_product" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to product +-- +ALTER TABLE "dojo_product" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to product_api_scan_configuration +-- +ALTER TABLE "dojo_product_api_scan_configuration" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to product_api_scan_configuration +-- +ALTER TABLE "dojo_product_api_scan_configuration" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to product_type +-- +ALTER TABLE "dojo_product_type" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to product_type +-- +ALTER TABLE "dojo_product_type" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to product_typeevent +-- +ALTER TABLE "dojo_product_typeevent" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to product_typeevent +-- +ALTER TABLE "dojo_product_typeevent" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to productevent +-- +ALTER TABLE "dojo_productevent" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to productevent +-- +ALTER TABLE "dojo_productevent" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to test +-- +ALTER TABLE "dojo_test" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to test +-- +ALTER TABLE "dojo_test" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to test_import +-- +ALTER TABLE "dojo_test_import" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to test_import +-- +ALTER TABLE "dojo_test_import" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to test_import_finding_action +-- +ALTER TABLE "dojo_test_import_finding_action" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to test_import_finding_action +-- +ALTER TABLE "dojo_test_import_finding_action" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to testevent +-- +ALTER TABLE "dojo_testevent" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to testevent +-- +ALTER TABLE "dojo_testevent" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to tool_product_settings +-- +ALTER TABLE "dojo_tool_product_settings" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to tool_product_settings +-- +ALTER TABLE "dojo_tool_product_settings" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Add field archive_batch to vulnerability_id +-- +ALTER TABLE "dojo_vulnerability_id" ADD COLUMN "archive_batch" uuid NULL; +-- +-- Add field archived_at to vulnerability_id +-- +ALTER TABLE "dojo_vulnerability_id" ADD COLUMN "archived_at" timestamp with time zone NULL; +-- +-- Create trigger insert_insert on model endpoint +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_insert_insert_c7973() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_endpointevent" ("archive_batch", "archived_at", "fragment", "host", "id", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "port", "product_id", "protocol", "query", "userinfo") VALUES (NEW."archive_batch", NEW."archived_at", NEW."fragment", NEW."host", NEW."id", NEW."path", _pgh_attach_context(), NOW(), 'insert', NEW."id", NEW."port", NEW."product_id", NEW."protocol", NEW."query", NEW."userinfo"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_insert_insert_c7973 ON "dojo_endpoint"; + CREATE TRIGGER pgtrigger_insert_insert_c7973 + AFTER INSERT ON "dojo_endpoint" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_insert_insert_c7973(); + + COMMENT ON TRIGGER pgtrigger_insert_insert_c7973 ON "dojo_endpoint" IS '4aa86c99a53e2afc8a2e7bbc9dce7e87cfe93c99'; + +-- +-- Create trigger update_update on model endpoint +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_update_update_2b19a() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_endpointevent" ("archive_batch", "archived_at", "fragment", "host", "id", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "port", "product_id", "protocol", "query", "userinfo") VALUES (NEW."archive_batch", NEW."archived_at", NEW."fragment", NEW."host", NEW."id", NEW."path", _pgh_attach_context(), NOW(), 'update', NEW."id", NEW."port", NEW."product_id", NEW."protocol", NEW."query", NEW."userinfo"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_update_update_2b19a ON "dojo_endpoint"; + CREATE TRIGGER pgtrigger_update_update_2b19a + AFTER UPDATE ON "dojo_endpoint" + + + FOR EACH ROW WHEN (OLD.* IS DISTINCT FROM NEW.*) + EXECUTE PROCEDURE pgtrigger_update_update_2b19a(); + + COMMENT ON TRIGGER pgtrigger_update_update_2b19a ON "dojo_endpoint" IS '187a4d67775847794459c2d4249fe13e333b2118'; + +-- +-- Create trigger delete_delete on model endpoint +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_delete_delete_dd1f9() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_endpointevent" ("archive_batch", "archived_at", "fragment", "host", "id", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "port", "product_id", "protocol", "query", "userinfo") VALUES (OLD."archive_batch", OLD."archived_at", OLD."fragment", OLD."host", OLD."id", OLD."path", _pgh_attach_context(), NOW(), 'delete', OLD."id", OLD."port", OLD."product_id", OLD."protocol", OLD."query", OLD."userinfo"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_delete_delete_dd1f9 ON "dojo_endpoint"; + CREATE TRIGGER pgtrigger_delete_delete_dd1f9 + AFTER DELETE ON "dojo_endpoint" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_delete_delete_dd1f9(); + + COMMENT ON TRIGGER pgtrigger_delete_delete_dd1f9 ON "dojo_endpoint" IS 'c30fc33fac55999df702b7c9c249f64513bf7497'; + +-- +-- Create trigger insert_insert on model engagement +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_insert_insert_125f1() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_engagementevent" ("active", "api_test", "archive_batch", "archived_at", "branch_tag", "build_id", "check_list", "cicd_build_server_id", "cicd_orchestration_engine_id", "cicd_scm_server_id", "commit_hash", "created", "deduplication_on_engagement", "description", "done_testing", "engagement_type", "first_contacted", "id", "lead_id", "name", "pen_test", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preset_id", "product_id", "progress", "reason", "report_type_id", "requester_id", "source_code_management_uri", "status", "target_end", "target_start", "test_strategy", "threat_model", "tmodel_path", "tracker", "updated", "version") VALUES (NEW."active", NEW."api_test", NEW."archive_batch", NEW."archived_at", NEW."branch_tag", NEW."build_id", NEW."check_list", NEW."cicd_build_server_id", NEW."cicd_orchestration_engine_id", NEW."cicd_scm_server_id", NEW."commit_hash", NEW."created", NEW."deduplication_on_engagement", NEW."description", NEW."done_testing", NEW."engagement_type", NEW."first_contacted", NEW."id", NEW."lead_id", NEW."name", NEW."pen_test", _pgh_attach_context(), NOW(), 'insert', NEW."id", NEW."preset_id", NEW."product_id", NEW."progress", NEW."reason", NEW."report_type_id", NEW."requester_id", NEW."source_code_management_uri", NEW."status", NEW."target_end", NEW."target_start", NEW."test_strategy", NEW."threat_model", NEW."tmodel_path", NEW."tracker", NEW."updated", NEW."version"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_insert_insert_125f1 ON "dojo_engagement"; + CREATE TRIGGER pgtrigger_insert_insert_125f1 + AFTER INSERT ON "dojo_engagement" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_insert_insert_125f1(); + + COMMENT ON TRIGGER pgtrigger_insert_insert_125f1 ON "dojo_engagement" IS '5b48232cfe7dc67a91eebfd023a4ccb6eb920c39'; + +-- +-- Create trigger update_update on model engagement +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_update_update_65136() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_engagementevent" ("active", "api_test", "archive_batch", "archived_at", "branch_tag", "build_id", "check_list", "cicd_build_server_id", "cicd_orchestration_engine_id", "cicd_scm_server_id", "commit_hash", "created", "deduplication_on_engagement", "description", "done_testing", "engagement_type", "first_contacted", "id", "lead_id", "name", "pen_test", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preset_id", "product_id", "progress", "reason", "report_type_id", "requester_id", "source_code_management_uri", "status", "target_end", "target_start", "test_strategy", "threat_model", "tmodel_path", "tracker", "updated", "version") VALUES (NEW."active", NEW."api_test", NEW."archive_batch", NEW."archived_at", NEW."branch_tag", NEW."build_id", NEW."check_list", NEW."cicd_build_server_id", NEW."cicd_orchestration_engine_id", NEW."cicd_scm_server_id", NEW."commit_hash", NEW."created", NEW."deduplication_on_engagement", NEW."description", NEW."done_testing", NEW."engagement_type", NEW."first_contacted", NEW."id", NEW."lead_id", NEW."name", NEW."pen_test", _pgh_attach_context(), NOW(), 'update', NEW."id", NEW."preset_id", NEW."product_id", NEW."progress", NEW."reason", NEW."report_type_id", NEW."requester_id", NEW."source_code_management_uri", NEW."status", NEW."target_end", NEW."target_start", NEW."test_strategy", NEW."threat_model", NEW."tmodel_path", NEW."tracker", NEW."updated", NEW."version"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_update_update_65136 ON "dojo_engagement"; + CREATE TRIGGER pgtrigger_update_update_65136 + AFTER UPDATE ON "dojo_engagement" + + + FOR EACH ROW WHEN (OLD."active" IS DISTINCT FROM (NEW."active") OR OLD."api_test" IS DISTINCT FROM (NEW."api_test") OR OLD."archive_batch" IS DISTINCT FROM (NEW."archive_batch") OR OLD."archived_at" IS DISTINCT FROM (NEW."archived_at") OR OLD."branch_tag" IS DISTINCT FROM (NEW."branch_tag") OR OLD."build_id" IS DISTINCT FROM (NEW."build_id") OR OLD."check_list" IS DISTINCT FROM (NEW."check_list") OR OLD."cicd_build_server_id" IS DISTINCT FROM (NEW."cicd_build_server_id") OR OLD."cicd_orchestration_engine_id" IS DISTINCT FROM (NEW."cicd_orchestration_engine_id") OR OLD."cicd_scm_server_id" IS DISTINCT FROM (NEW."cicd_scm_server_id") OR OLD."commit_hash" IS DISTINCT FROM (NEW."commit_hash") OR OLD."deduplication_on_engagement" IS DISTINCT FROM (NEW."deduplication_on_engagement") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."done_testing" IS DISTINCT FROM (NEW."done_testing") OR OLD."engagement_type" IS DISTINCT FROM (NEW."engagement_type") OR OLD."first_contacted" IS DISTINCT FROM (NEW."first_contacted") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."lead_id" IS DISTINCT FROM (NEW."lead_id") OR OLD."name" IS DISTINCT FROM (NEW."name") OR OLD."pen_test" IS DISTINCT FROM (NEW."pen_test") OR OLD."preset_id" IS DISTINCT FROM (NEW."preset_id") OR OLD."product_id" IS DISTINCT FROM (NEW."product_id") OR OLD."progress" IS DISTINCT FROM (NEW."progress") OR OLD."reason" IS DISTINCT FROM (NEW."reason") OR OLD."report_type_id" IS DISTINCT FROM (NEW."report_type_id") OR OLD."requester_id" IS DISTINCT FROM (NEW."requester_id") OR OLD."source_code_management_uri" IS DISTINCT FROM (NEW."source_code_management_uri") OR OLD."status" IS DISTINCT FROM (NEW."status") OR OLD."target_end" IS DISTINCT FROM (NEW."target_end") OR OLD."target_start" IS DISTINCT FROM (NEW."target_start") OR OLD."test_strategy" IS DISTINCT FROM (NEW."test_strategy") OR OLD."threat_model" IS DISTINCT FROM (NEW."threat_model") OR OLD."tmodel_path" IS DISTINCT FROM (NEW."tmodel_path") OR OLD."tracker" IS DISTINCT FROM (NEW."tracker") OR OLD."version" IS DISTINCT FROM (NEW."version")) + EXECUTE PROCEDURE pgtrigger_update_update_65136(); + + COMMENT ON TRIGGER pgtrigger_update_update_65136 ON "dojo_engagement" IS '8990b78ab9370e3cd1ec2b85dd89d52834d5aaf6'; + +-- +-- Create trigger delete_delete on model engagement +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_delete_delete_9f4df() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_engagementevent" ("active", "api_test", "archive_batch", "archived_at", "branch_tag", "build_id", "check_list", "cicd_build_server_id", "cicd_orchestration_engine_id", "cicd_scm_server_id", "commit_hash", "created", "deduplication_on_engagement", "description", "done_testing", "engagement_type", "first_contacted", "id", "lead_id", "name", "pen_test", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preset_id", "product_id", "progress", "reason", "report_type_id", "requester_id", "source_code_management_uri", "status", "target_end", "target_start", "test_strategy", "threat_model", "tmodel_path", "tracker", "updated", "version") VALUES (OLD."active", OLD."api_test", OLD."archive_batch", OLD."archived_at", OLD."branch_tag", OLD."build_id", OLD."check_list", OLD."cicd_build_server_id", OLD."cicd_orchestration_engine_id", OLD."cicd_scm_server_id", OLD."commit_hash", OLD."created", OLD."deduplication_on_engagement", OLD."description", OLD."done_testing", OLD."engagement_type", OLD."first_contacted", OLD."id", OLD."lead_id", OLD."name", OLD."pen_test", _pgh_attach_context(), NOW(), 'delete', OLD."id", OLD."preset_id", OLD."product_id", OLD."progress", OLD."reason", OLD."report_type_id", OLD."requester_id", OLD."source_code_management_uri", OLD."status", OLD."target_end", OLD."target_start", OLD."test_strategy", OLD."threat_model", OLD."tmodel_path", OLD."tracker", OLD."updated", OLD."version"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_delete_delete_9f4df ON "dojo_engagement"; + CREATE TRIGGER pgtrigger_delete_delete_9f4df + AFTER DELETE ON "dojo_engagement" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_delete_delete_9f4df(); + + COMMENT ON TRIGGER pgtrigger_delete_delete_9f4df ON "dojo_engagement" IS '0de881cdff9409808f24eb0258bdd61ae9ddc60e'; + +-- +-- Create trigger insert_insert on model finding +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_insert_insert_2fbbb() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_findingevent" ("active", "archive_batch", "archived_at", "component_name", "component_version", "created", "cve", "cvssv3", "cvssv3_score", "cvssv4", "cvssv4_score", "cwe", "date", "defect_review_requested_by_id", "description", "duplicate", "duplicate_finding_id", "dynamic_finding", "effort_for_fixing", "epss_percentile", "epss_score", "false_p", "file_path", "fix_available", "fix_version", "hash_code", "id", "impact", "is_mitigated", "kev_date", "known_exploited", "last_reviewed", "last_reviewed_by_id", "last_status_update", "line", "mitigated", "mitigated_by_id", "mitigation", "nb_occurences", "numerical_severity", "out_of_scope", "param", "payload", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "planned_remediation_date", "planned_remediation_version", "publish_date", "ransomware_used", "refs", "reporter_id", "review_requested_by_id", "risk_accepted", "sast_sink_object", "sast_source_file_path", "sast_source_line", "sast_source_object", "scanner_confidence", "service", "severity", "severity_justification", "sla_expiration_date", "sla_start_date", "sonarqube_issue_id", "static_finding", "steps_to_reproduce", "test_id", "thread_id", "title", "under_defect_review", "under_review", "unique_id_from_tool", "updated", "url", "verified", "vuln_id_from_tool") VALUES (NEW."active", NEW."archive_batch", NEW."archived_at", NEW."component_name", NEW."component_version", NEW."created", NEW."cve", NEW."cvssv3", NEW."cvssv3_score", NEW."cvssv4", NEW."cvssv4_score", NEW."cwe", NEW."date", NEW."defect_review_requested_by_id", NEW."description", NEW."duplicate", NEW."duplicate_finding_id", NEW."dynamic_finding", NEW."effort_for_fixing", NEW."epss_percentile", NEW."epss_score", NEW."false_p", NEW."file_path", NEW."fix_available", NEW."fix_version", NEW."hash_code", NEW."id", NEW."impact", NEW."is_mitigated", NEW."kev_date", NEW."known_exploited", NEW."last_reviewed", NEW."last_reviewed_by_id", NEW."last_status_update", NEW."line", NEW."mitigated", NEW."mitigated_by_id", NEW."mitigation", NEW."nb_occurences", NEW."numerical_severity", NEW."out_of_scope", NEW."param", NEW."payload", _pgh_attach_context(), NOW(), 'insert', NEW."id", NEW."planned_remediation_date", NEW."planned_remediation_version", NEW."publish_date", NEW."ransomware_used", NEW."refs", NEW."reporter_id", NEW."review_requested_by_id", NEW."risk_accepted", NEW."sast_sink_object", NEW."sast_source_file_path", NEW."sast_source_line", NEW."sast_source_object", NEW."scanner_confidence", NEW."service", NEW."severity", NEW."severity_justification", NEW."sla_expiration_date", NEW."sla_start_date", NEW."sonarqube_issue_id", NEW."static_finding", NEW."steps_to_reproduce", NEW."test_id", NEW."thread_id", NEW."title", NEW."under_defect_review", NEW."under_review", NEW."unique_id_from_tool", NEW."updated", NEW."url", NEW."verified", NEW."vuln_id_from_tool"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_insert_insert_2fbbb ON "dojo_finding"; + CREATE TRIGGER pgtrigger_insert_insert_2fbbb + AFTER INSERT ON "dojo_finding" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_insert_insert_2fbbb(); + + COMMENT ON TRIGGER pgtrigger_insert_insert_2fbbb ON "dojo_finding" IS 'bfca827e3bf9d38085346f34984b62bfc9efa036'; + +-- +-- Create trigger update_update on model finding +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_update_update_92175() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_findingevent" ("active", "archive_batch", "archived_at", "component_name", "component_version", "created", "cve", "cvssv3", "cvssv3_score", "cvssv4", "cvssv4_score", "cwe", "date", "defect_review_requested_by_id", "description", "duplicate", "duplicate_finding_id", "dynamic_finding", "effort_for_fixing", "epss_percentile", "epss_score", "false_p", "file_path", "fix_available", "fix_version", "hash_code", "id", "impact", "is_mitigated", "kev_date", "known_exploited", "last_reviewed", "last_reviewed_by_id", "last_status_update", "line", "mitigated", "mitigated_by_id", "mitigation", "nb_occurences", "numerical_severity", "out_of_scope", "param", "payload", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "planned_remediation_date", "planned_remediation_version", "publish_date", "ransomware_used", "refs", "reporter_id", "review_requested_by_id", "risk_accepted", "sast_sink_object", "sast_source_file_path", "sast_source_line", "sast_source_object", "scanner_confidence", "service", "severity", "severity_justification", "sla_expiration_date", "sla_start_date", "sonarqube_issue_id", "static_finding", "steps_to_reproduce", "test_id", "thread_id", "title", "under_defect_review", "under_review", "unique_id_from_tool", "updated", "url", "verified", "vuln_id_from_tool") VALUES (NEW."active", NEW."archive_batch", NEW."archived_at", NEW."component_name", NEW."component_version", NEW."created", NEW."cve", NEW."cvssv3", NEW."cvssv3_score", NEW."cvssv4", NEW."cvssv4_score", NEW."cwe", NEW."date", NEW."defect_review_requested_by_id", NEW."description", NEW."duplicate", NEW."duplicate_finding_id", NEW."dynamic_finding", NEW."effort_for_fixing", NEW."epss_percentile", NEW."epss_score", NEW."false_p", NEW."file_path", NEW."fix_available", NEW."fix_version", NEW."hash_code", NEW."id", NEW."impact", NEW."is_mitigated", NEW."kev_date", NEW."known_exploited", NEW."last_reviewed", NEW."last_reviewed_by_id", NEW."last_status_update", NEW."line", NEW."mitigated", NEW."mitigated_by_id", NEW."mitigation", NEW."nb_occurences", NEW."numerical_severity", NEW."out_of_scope", NEW."param", NEW."payload", _pgh_attach_context(), NOW(), 'update', NEW."id", NEW."planned_remediation_date", NEW."planned_remediation_version", NEW."publish_date", NEW."ransomware_used", NEW."refs", NEW."reporter_id", NEW."review_requested_by_id", NEW."risk_accepted", NEW."sast_sink_object", NEW."sast_source_file_path", NEW."sast_source_line", NEW."sast_source_object", NEW."scanner_confidence", NEW."service", NEW."severity", NEW."severity_justification", NEW."sla_expiration_date", NEW."sla_start_date", NEW."sonarqube_issue_id", NEW."static_finding", NEW."steps_to_reproduce", NEW."test_id", NEW."thread_id", NEW."title", NEW."under_defect_review", NEW."under_review", NEW."unique_id_from_tool", NEW."updated", NEW."url", NEW."verified", NEW."vuln_id_from_tool"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_update_update_92175 ON "dojo_finding"; + CREATE TRIGGER pgtrigger_update_update_92175 + AFTER UPDATE ON "dojo_finding" + + + FOR EACH ROW WHEN (OLD."active" IS DISTINCT FROM (NEW."active") OR OLD."archive_batch" IS DISTINCT FROM (NEW."archive_batch") OR OLD."archived_at" IS DISTINCT FROM (NEW."archived_at") OR OLD."component_name" IS DISTINCT FROM (NEW."component_name") OR OLD."component_version" IS DISTINCT FROM (NEW."component_version") OR OLD."cve" IS DISTINCT FROM (NEW."cve") OR OLD."cvssv3" IS DISTINCT FROM (NEW."cvssv3") OR OLD."cvssv3_score" IS DISTINCT FROM (NEW."cvssv3_score") OR OLD."cvssv4" IS DISTINCT FROM (NEW."cvssv4") OR OLD."cvssv4_score" IS DISTINCT FROM (NEW."cvssv4_score") OR OLD."cwe" IS DISTINCT FROM (NEW."cwe") OR OLD."date" IS DISTINCT FROM (NEW."date") OR OLD."defect_review_requested_by_id" IS DISTINCT FROM (NEW."defect_review_requested_by_id") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."duplicate" IS DISTINCT FROM (NEW."duplicate") OR OLD."duplicate_finding_id" IS DISTINCT FROM (NEW."duplicate_finding_id") OR OLD."dynamic_finding" IS DISTINCT FROM (NEW."dynamic_finding") OR OLD."effort_for_fixing" IS DISTINCT FROM (NEW."effort_for_fixing") OR OLD."epss_percentile" IS DISTINCT FROM (NEW."epss_percentile") OR OLD."epss_score" IS DISTINCT FROM (NEW."epss_score") OR OLD."false_p" IS DISTINCT FROM (NEW."false_p") OR OLD."file_path" IS DISTINCT FROM (NEW."file_path") OR OLD."fix_available" IS DISTINCT FROM (NEW."fix_available") OR OLD."fix_version" IS DISTINCT FROM (NEW."fix_version") OR OLD."hash_code" IS DISTINCT FROM (NEW."hash_code") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."impact" IS DISTINCT FROM (NEW."impact") OR OLD."is_mitigated" IS DISTINCT FROM (NEW."is_mitigated") OR OLD."kev_date" IS DISTINCT FROM (NEW."kev_date") OR OLD."known_exploited" IS DISTINCT FROM (NEW."known_exploited") OR OLD."last_reviewed" IS DISTINCT FROM (NEW."last_reviewed") OR OLD."last_reviewed_by_id" IS DISTINCT FROM (NEW."last_reviewed_by_id") OR OLD."line" IS DISTINCT FROM (NEW."line") OR OLD."mitigated" IS DISTINCT FROM (NEW."mitigated") OR OLD."mitigated_by_id" IS DISTINCT FROM (NEW."mitigated_by_id") OR OLD."mitigation" IS DISTINCT FROM (NEW."mitigation") OR OLD."nb_occurences" IS DISTINCT FROM (NEW."nb_occurences") OR OLD."numerical_severity" IS DISTINCT FROM (NEW."numerical_severity") OR OLD."out_of_scope" IS DISTINCT FROM (NEW."out_of_scope") OR OLD."param" IS DISTINCT FROM (NEW."param") OR OLD."payload" IS DISTINCT FROM (NEW."payload") OR OLD."planned_remediation_date" IS DISTINCT FROM (NEW."planned_remediation_date") OR OLD."planned_remediation_version" IS DISTINCT FROM (NEW."planned_remediation_version") OR OLD."publish_date" IS DISTINCT FROM (NEW."publish_date") OR OLD."ransomware_used" IS DISTINCT FROM (NEW."ransomware_used") OR OLD."refs" IS DISTINCT FROM (NEW."refs") OR OLD."reporter_id" IS DISTINCT FROM (NEW."reporter_id") OR OLD."review_requested_by_id" IS DISTINCT FROM (NEW."review_requested_by_id") OR OLD."risk_accepted" IS DISTINCT FROM (NEW."risk_accepted") OR OLD."sast_sink_object" IS DISTINCT FROM (NEW."sast_sink_object") OR OLD."sast_source_file_path" IS DISTINCT FROM (NEW."sast_source_file_path") OR OLD."sast_source_line" IS DISTINCT FROM (NEW."sast_source_line") OR OLD."sast_source_object" IS DISTINCT FROM (NEW."sast_source_object") OR OLD."scanner_confidence" IS DISTINCT FROM (NEW."scanner_confidence") OR OLD."service" IS DISTINCT FROM (NEW."service") OR OLD."severity" IS DISTINCT FROM (NEW."severity") OR OLD."severity_justification" IS DISTINCT FROM (NEW."severity_justification") OR OLD."sla_expiration_date" IS DISTINCT FROM (NEW."sla_expiration_date") OR OLD."sla_start_date" IS DISTINCT FROM (NEW."sla_start_date") OR OLD."sonarqube_issue_id" IS DISTINCT FROM (NEW."sonarqube_issue_id") OR OLD."static_finding" IS DISTINCT FROM (NEW."static_finding") OR OLD."steps_to_reproduce" IS DISTINCT FROM (NEW."steps_to_reproduce") OR OLD."test_id" IS DISTINCT FROM (NEW."test_id") OR OLD."thread_id" IS DISTINCT FROM (NEW."thread_id") OR OLD."title" IS DISTINCT FROM (NEW."title") OR OLD."under_defect_review" IS DISTINCT FROM (NEW."under_defect_review") OR OLD."under_review" IS DISTINCT FROM (NEW."under_review") OR OLD."unique_id_from_tool" IS DISTINCT FROM (NEW."unique_id_from_tool") OR OLD."url" IS DISTINCT FROM (NEW."url") OR OLD."verified" IS DISTINCT FROM (NEW."verified") OR OLD."vuln_id_from_tool" IS DISTINCT FROM (NEW."vuln_id_from_tool")) + EXECUTE PROCEDURE pgtrigger_update_update_92175(); + + COMMENT ON TRIGGER pgtrigger_update_update_92175 ON "dojo_finding" IS '7b1c5da1fc67ea4d10b0d6e9ec42fbf428ad6d47'; + +-- +-- Create trigger delete_delete on model finding +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_delete_delete_72933() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_findingevent" ("active", "archive_batch", "archived_at", "component_name", "component_version", "created", "cve", "cvssv3", "cvssv3_score", "cvssv4", "cvssv4_score", "cwe", "date", "defect_review_requested_by_id", "description", "duplicate", "duplicate_finding_id", "dynamic_finding", "effort_for_fixing", "epss_percentile", "epss_score", "false_p", "file_path", "fix_available", "fix_version", "hash_code", "id", "impact", "is_mitigated", "kev_date", "known_exploited", "last_reviewed", "last_reviewed_by_id", "last_status_update", "line", "mitigated", "mitigated_by_id", "mitigation", "nb_occurences", "numerical_severity", "out_of_scope", "param", "payload", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "planned_remediation_date", "planned_remediation_version", "publish_date", "ransomware_used", "refs", "reporter_id", "review_requested_by_id", "risk_accepted", "sast_sink_object", "sast_source_file_path", "sast_source_line", "sast_source_object", "scanner_confidence", "service", "severity", "severity_justification", "sla_expiration_date", "sla_start_date", "sonarqube_issue_id", "static_finding", "steps_to_reproduce", "test_id", "thread_id", "title", "under_defect_review", "under_review", "unique_id_from_tool", "updated", "url", "verified", "vuln_id_from_tool") VALUES (OLD."active", OLD."archive_batch", OLD."archived_at", OLD."component_name", OLD."component_version", OLD."created", OLD."cve", OLD."cvssv3", OLD."cvssv3_score", OLD."cvssv4", OLD."cvssv4_score", OLD."cwe", OLD."date", OLD."defect_review_requested_by_id", OLD."description", OLD."duplicate", OLD."duplicate_finding_id", OLD."dynamic_finding", OLD."effort_for_fixing", OLD."epss_percentile", OLD."epss_score", OLD."false_p", OLD."file_path", OLD."fix_available", OLD."fix_version", OLD."hash_code", OLD."id", OLD."impact", OLD."is_mitigated", OLD."kev_date", OLD."known_exploited", OLD."last_reviewed", OLD."last_reviewed_by_id", OLD."last_status_update", OLD."line", OLD."mitigated", OLD."mitigated_by_id", OLD."mitigation", OLD."nb_occurences", OLD."numerical_severity", OLD."out_of_scope", OLD."param", OLD."payload", _pgh_attach_context(), NOW(), 'delete', OLD."id", OLD."planned_remediation_date", OLD."planned_remediation_version", OLD."publish_date", OLD."ransomware_used", OLD."refs", OLD."reporter_id", OLD."review_requested_by_id", OLD."risk_accepted", OLD."sast_sink_object", OLD."sast_source_file_path", OLD."sast_source_line", OLD."sast_source_object", OLD."scanner_confidence", OLD."service", OLD."severity", OLD."severity_justification", OLD."sla_expiration_date", OLD."sla_start_date", OLD."sonarqube_issue_id", OLD."static_finding", OLD."steps_to_reproduce", OLD."test_id", OLD."thread_id", OLD."title", OLD."under_defect_review", OLD."under_review", OLD."unique_id_from_tool", OLD."updated", OLD."url", OLD."verified", OLD."vuln_id_from_tool"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_delete_delete_72933 ON "dojo_finding"; + CREATE TRIGGER pgtrigger_delete_delete_72933 + AFTER DELETE ON "dojo_finding" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_delete_delete_72933(); + + COMMENT ON TRIGGER pgtrigger_delete_delete_72933 ON "dojo_finding" IS '0b0ae774839868caab7908236804d2b14393a699'; + +-- +-- Create trigger insert_insert on model finding_group +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_insert_insert_94bd2() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_finding_groupevent" ("archive_batch", "archived_at", "created", "creator_id", "id", "modified", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "test_id") VALUES (NEW."archive_batch", NEW."archived_at", NEW."created", NEW."creator_id", NEW."id", NEW."modified", NEW."name", _pgh_attach_context(), NOW(), 'insert', NEW."id", NEW."test_id"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_insert_insert_94bd2 ON "dojo_finding_group"; + CREATE TRIGGER pgtrigger_insert_insert_94bd2 + AFTER INSERT ON "dojo_finding_group" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_insert_insert_94bd2(); + + COMMENT ON TRIGGER pgtrigger_insert_insert_94bd2 ON "dojo_finding_group" IS 'bcad4bd5b2d4c90eede0665d18169e85f5f14257'; + +-- +-- Create trigger update_update on model finding_group +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_update_update_37b5f() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_finding_groupevent" ("archive_batch", "archived_at", "created", "creator_id", "id", "modified", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "test_id") VALUES (NEW."archive_batch", NEW."archived_at", NEW."created", NEW."creator_id", NEW."id", NEW."modified", NEW."name", _pgh_attach_context(), NOW(), 'update', NEW."id", NEW."test_id"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_update_update_37b5f ON "dojo_finding_group"; + CREATE TRIGGER pgtrigger_update_update_37b5f + AFTER UPDATE ON "dojo_finding_group" + + + FOR EACH ROW WHEN (OLD."archive_batch" IS DISTINCT FROM (NEW."archive_batch") OR OLD."archived_at" IS DISTINCT FROM (NEW."archived_at") OR OLD."creator_id" IS DISTINCT FROM (NEW."creator_id") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."name" IS DISTINCT FROM (NEW."name") OR OLD."test_id" IS DISTINCT FROM (NEW."test_id")) + EXECUTE PROCEDURE pgtrigger_update_update_37b5f(); + + COMMENT ON TRIGGER pgtrigger_update_update_37b5f ON "dojo_finding_group" IS 'bc709885f7ae1a3e00bb01e453bfd461ea3ac298'; + +-- +-- Create trigger delete_delete on model finding_group +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_delete_delete_24b41() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_finding_groupevent" ("archive_batch", "archived_at", "created", "creator_id", "id", "modified", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "test_id") VALUES (OLD."archive_batch", OLD."archived_at", OLD."created", OLD."creator_id", OLD."id", OLD."modified", OLD."name", _pgh_attach_context(), NOW(), 'delete', OLD."id", OLD."test_id"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_delete_delete_24b41 ON "dojo_finding_group"; + CREATE TRIGGER pgtrigger_delete_delete_24b41 + AFTER DELETE ON "dojo_finding_group" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_delete_delete_24b41(); + + COMMENT ON TRIGGER pgtrigger_delete_delete_24b41 ON "dojo_finding_group" IS 'e573c51b932f142bef508cc995f8f4e6657c1d62'; + +-- +-- Create trigger insert_insert on model product +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_insert_insert_d5d32() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_productevent" ("archive_batch", "archived_at", "async_updating", "business_criticality", "created", "description", "disable_sla_breach_notifications", "enable_full_risk_acceptance", "enable_product_tag_inheritance", "enable_simple_risk_acceptance", "external_audience", "id", "internet_accessible", "lifecycle_id", "name", "origin_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "platform_id", "prod_numeric_grade", "prod_type_id", "product_manager_id", "revenue", "sla_configuration_id", "team_manager_id", "technical_contact_id", "tid", "updated", "user_records") VALUES (NEW."archive_batch", NEW."archived_at", NEW."async_updating", NEW."business_criticality", NEW."created", NEW."description", NEW."disable_sla_breach_notifications", NEW."enable_full_risk_acceptance", NEW."enable_product_tag_inheritance", NEW."enable_simple_risk_acceptance", NEW."external_audience", NEW."id", NEW."internet_accessible", NEW."lifecycle_id", NEW."name", NEW."origin_id", _pgh_attach_context(), NOW(), 'insert', NEW."id", NEW."platform_id", NEW."prod_numeric_grade", NEW."prod_type_id", NEW."product_manager_id", NEW."revenue", NEW."sla_configuration_id", NEW."team_manager_id", NEW."technical_contact_id", NEW."tid", NEW."updated", NEW."user_records"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_insert_insert_d5d32 ON "dojo_product"; + CREATE TRIGGER pgtrigger_insert_insert_d5d32 + AFTER INSERT ON "dojo_product" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_insert_insert_d5d32(); + + COMMENT ON TRIGGER pgtrigger_insert_insert_d5d32 ON "dojo_product" IS 'b5851c1970f7b7c91e09d80dc365cbe2579c1220'; + +-- +-- Create trigger update_update on model product +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_update_update_e7040() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_productevent" ("archive_batch", "archived_at", "async_updating", "business_criticality", "created", "description", "disable_sla_breach_notifications", "enable_full_risk_acceptance", "enable_product_tag_inheritance", "enable_simple_risk_acceptance", "external_audience", "id", "internet_accessible", "lifecycle_id", "name", "origin_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "platform_id", "prod_numeric_grade", "prod_type_id", "product_manager_id", "revenue", "sla_configuration_id", "team_manager_id", "technical_contact_id", "tid", "updated", "user_records") VALUES (NEW."archive_batch", NEW."archived_at", NEW."async_updating", NEW."business_criticality", NEW."created", NEW."description", NEW."disable_sla_breach_notifications", NEW."enable_full_risk_acceptance", NEW."enable_product_tag_inheritance", NEW."enable_simple_risk_acceptance", NEW."external_audience", NEW."id", NEW."internet_accessible", NEW."lifecycle_id", NEW."name", NEW."origin_id", _pgh_attach_context(), NOW(), 'update', NEW."id", NEW."platform_id", NEW."prod_numeric_grade", NEW."prod_type_id", NEW."product_manager_id", NEW."revenue", NEW."sla_configuration_id", NEW."team_manager_id", NEW."technical_contact_id", NEW."tid", NEW."updated", NEW."user_records"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_update_update_e7040 ON "dojo_product"; + CREATE TRIGGER pgtrigger_update_update_e7040 + AFTER UPDATE ON "dojo_product" + + + FOR EACH ROW WHEN (OLD."archive_batch" IS DISTINCT FROM (NEW."archive_batch") OR OLD."archived_at" IS DISTINCT FROM (NEW."archived_at") OR OLD."async_updating" IS DISTINCT FROM (NEW."async_updating") OR OLD."business_criticality" IS DISTINCT FROM (NEW."business_criticality") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."disable_sla_breach_notifications" IS DISTINCT FROM (NEW."disable_sla_breach_notifications") OR OLD."enable_full_risk_acceptance" IS DISTINCT FROM (NEW."enable_full_risk_acceptance") OR OLD."enable_product_tag_inheritance" IS DISTINCT FROM (NEW."enable_product_tag_inheritance") OR OLD."enable_simple_risk_acceptance" IS DISTINCT FROM (NEW."enable_simple_risk_acceptance") OR OLD."external_audience" IS DISTINCT FROM (NEW."external_audience") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."internet_accessible" IS DISTINCT FROM (NEW."internet_accessible") OR OLD."lifecycle_id" IS DISTINCT FROM (NEW."lifecycle_id") OR OLD."name" IS DISTINCT FROM (NEW."name") OR OLD."origin_id" IS DISTINCT FROM (NEW."origin_id") OR OLD."platform_id" IS DISTINCT FROM (NEW."platform_id") OR OLD."prod_numeric_grade" IS DISTINCT FROM (NEW."prod_numeric_grade") OR OLD."prod_type_id" IS DISTINCT FROM (NEW."prod_type_id") OR OLD."product_manager_id" IS DISTINCT FROM (NEW."product_manager_id") OR OLD."revenue" IS DISTINCT FROM (NEW."revenue") OR OLD."sla_configuration_id" IS DISTINCT FROM (NEW."sla_configuration_id") OR OLD."team_manager_id" IS DISTINCT FROM (NEW."team_manager_id") OR OLD."technical_contact_id" IS DISTINCT FROM (NEW."technical_contact_id") OR OLD."tid" IS DISTINCT FROM (NEW."tid") OR OLD."user_records" IS DISTINCT FROM (NEW."user_records")) + EXECUTE PROCEDURE pgtrigger_update_update_e7040(); + + COMMENT ON TRIGGER pgtrigger_update_update_e7040 ON "dojo_product" IS 'b5793f361239e1329aea5ee48f4b68ba6d9c5f0f'; + +-- +-- Create trigger delete_delete on model product +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_delete_delete_064dd() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_productevent" ("archive_batch", "archived_at", "async_updating", "business_criticality", "created", "description", "disable_sla_breach_notifications", "enable_full_risk_acceptance", "enable_product_tag_inheritance", "enable_simple_risk_acceptance", "external_audience", "id", "internet_accessible", "lifecycle_id", "name", "origin_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "platform_id", "prod_numeric_grade", "prod_type_id", "product_manager_id", "revenue", "sla_configuration_id", "team_manager_id", "technical_contact_id", "tid", "updated", "user_records") VALUES (OLD."archive_batch", OLD."archived_at", OLD."async_updating", OLD."business_criticality", OLD."created", OLD."description", OLD."disable_sla_breach_notifications", OLD."enable_full_risk_acceptance", OLD."enable_product_tag_inheritance", OLD."enable_simple_risk_acceptance", OLD."external_audience", OLD."id", OLD."internet_accessible", OLD."lifecycle_id", OLD."name", OLD."origin_id", _pgh_attach_context(), NOW(), 'delete', OLD."id", OLD."platform_id", OLD."prod_numeric_grade", OLD."prod_type_id", OLD."product_manager_id", OLD."revenue", OLD."sla_configuration_id", OLD."team_manager_id", OLD."technical_contact_id", OLD."tid", OLD."updated", OLD."user_records"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_delete_delete_064dd ON "dojo_product"; + CREATE TRIGGER pgtrigger_delete_delete_064dd + AFTER DELETE ON "dojo_product" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_delete_delete_064dd(); + + COMMENT ON TRIGGER pgtrigger_delete_delete_064dd ON "dojo_product" IS 'b680a761758c22da5777b8b70f718169dfaf58b5'; + +-- +-- Create trigger insert_insert on model product_type +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_insert_insert_2d109() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_product_typeevent" ("archive_batch", "archived_at", "created", "critical_product", "description", "id", "key_product", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "updated") VALUES (NEW."archive_batch", NEW."archived_at", NEW."created", NEW."critical_product", NEW."description", NEW."id", NEW."key_product", NEW."name", _pgh_attach_context(), NOW(), 'insert', NEW."id", NEW."updated"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_insert_insert_2d109 ON "dojo_product_type"; + CREATE TRIGGER pgtrigger_insert_insert_2d109 + AFTER INSERT ON "dojo_product_type" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_insert_insert_2d109(); + + COMMENT ON TRIGGER pgtrigger_insert_insert_2d109 ON "dojo_product_type" IS 'c22c7686452daedc2b9216ac67df591501e319f2'; + +-- +-- Create trigger update_update on model product_type +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_update_update_a0136() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_product_typeevent" ("archive_batch", "archived_at", "created", "critical_product", "description", "id", "key_product", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "updated") VALUES (NEW."archive_batch", NEW."archived_at", NEW."created", NEW."critical_product", NEW."description", NEW."id", NEW."key_product", NEW."name", _pgh_attach_context(), NOW(), 'update', NEW."id", NEW."updated"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_update_update_a0136 ON "dojo_product_type"; + CREATE TRIGGER pgtrigger_update_update_a0136 + AFTER UPDATE ON "dojo_product_type" + + + FOR EACH ROW WHEN (OLD."archive_batch" IS DISTINCT FROM (NEW."archive_batch") OR OLD."archived_at" IS DISTINCT FROM (NEW."archived_at") OR OLD."critical_product" IS DISTINCT FROM (NEW."critical_product") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."key_product" IS DISTINCT FROM (NEW."key_product") OR OLD."name" IS DISTINCT FROM (NEW."name")) + EXECUTE PROCEDURE pgtrigger_update_update_a0136(); + + COMMENT ON TRIGGER pgtrigger_update_update_a0136 ON "dojo_product_type" IS 'f0b2de3b71e7b546d0efe2cb7950638c095d85fd'; + +-- +-- Create trigger delete_delete on model product_type +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_delete_delete_66b18() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_product_typeevent" ("archive_batch", "archived_at", "created", "critical_product", "description", "id", "key_product", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "updated") VALUES (OLD."archive_batch", OLD."archived_at", OLD."created", OLD."critical_product", OLD."description", OLD."id", OLD."key_product", OLD."name", _pgh_attach_context(), NOW(), 'delete', OLD."id", OLD."updated"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_delete_delete_66b18 ON "dojo_product_type"; + CREATE TRIGGER pgtrigger_delete_delete_66b18 + AFTER DELETE ON "dojo_product_type" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_delete_delete_66b18(); + + COMMENT ON TRIGGER pgtrigger_delete_delete_66b18 ON "dojo_product_type" IS '7d8aada72ff77dd0e91e25d2324c57b8d8500509'; + +-- +-- Create trigger insert_insert on model test +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_insert_insert_ecfc1() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_testevent" ("api_scan_configuration_id", "archive_batch", "archived_at", "branch_tag", "build_id", "commit_hash", "created", "description", "engagement_id", "environment_id", "id", "lead_id", "percent_complete", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "scan_type", "target_end", "target_start", "test_type_id", "title", "updated", "version") VALUES (NEW."api_scan_configuration_id", NEW."archive_batch", NEW."archived_at", NEW."branch_tag", NEW."build_id", NEW."commit_hash", NEW."created", NEW."description", NEW."engagement_id", NEW."environment_id", NEW."id", NEW."lead_id", NEW."percent_complete", _pgh_attach_context(), NOW(), 'insert', NEW."id", NEW."scan_type", NEW."target_end", NEW."target_start", NEW."test_type_id", NEW."title", NEW."updated", NEW."version"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_insert_insert_ecfc1 ON "dojo_test"; + CREATE TRIGGER pgtrigger_insert_insert_ecfc1 + AFTER INSERT ON "dojo_test" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_insert_insert_ecfc1(); + + COMMENT ON TRIGGER pgtrigger_insert_insert_ecfc1 ON "dojo_test" IS '477f6a297658d54b13ef83b4f4b19b0e76f2bb27'; + +-- +-- Create trigger update_update on model test +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_update_update_c40f8() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_testevent" ("api_scan_configuration_id", "archive_batch", "archived_at", "branch_tag", "build_id", "commit_hash", "created", "description", "engagement_id", "environment_id", "id", "lead_id", "percent_complete", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "scan_type", "target_end", "target_start", "test_type_id", "title", "updated", "version") VALUES (NEW."api_scan_configuration_id", NEW."archive_batch", NEW."archived_at", NEW."branch_tag", NEW."build_id", NEW."commit_hash", NEW."created", NEW."description", NEW."engagement_id", NEW."environment_id", NEW."id", NEW."lead_id", NEW."percent_complete", _pgh_attach_context(), NOW(), 'update', NEW."id", NEW."scan_type", NEW."target_end", NEW."target_start", NEW."test_type_id", NEW."title", NEW."updated", NEW."version"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_update_update_c40f8 ON "dojo_test"; + CREATE TRIGGER pgtrigger_update_update_c40f8 + AFTER UPDATE ON "dojo_test" + + + FOR EACH ROW WHEN (OLD."api_scan_configuration_id" IS DISTINCT FROM (NEW."api_scan_configuration_id") OR OLD."archive_batch" IS DISTINCT FROM (NEW."archive_batch") OR OLD."archived_at" IS DISTINCT FROM (NEW."archived_at") OR OLD."branch_tag" IS DISTINCT FROM (NEW."branch_tag") OR OLD."build_id" IS DISTINCT FROM (NEW."build_id") OR OLD."commit_hash" IS DISTINCT FROM (NEW."commit_hash") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."engagement_id" IS DISTINCT FROM (NEW."engagement_id") OR OLD."environment_id" IS DISTINCT FROM (NEW."environment_id") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."lead_id" IS DISTINCT FROM (NEW."lead_id") OR OLD."percent_complete" IS DISTINCT FROM (NEW."percent_complete") OR OLD."scan_type" IS DISTINCT FROM (NEW."scan_type") OR OLD."target_end" IS DISTINCT FROM (NEW."target_end") OR OLD."target_start" IS DISTINCT FROM (NEW."target_start") OR OLD."test_type_id" IS DISTINCT FROM (NEW."test_type_id") OR OLD."title" IS DISTINCT FROM (NEW."title") OR OLD."version" IS DISTINCT FROM (NEW."version")) + EXECUTE PROCEDURE pgtrigger_update_update_c40f8(); + + COMMENT ON TRIGGER pgtrigger_update_update_c40f8 ON "dojo_test" IS '50e41edef10c98f5e20b9c43a324c49f5b897f88'; + +-- +-- Create trigger delete_delete on model test +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_delete_delete_66d18() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_testevent" ("api_scan_configuration_id", "archive_batch", "archived_at", "branch_tag", "build_id", "commit_hash", "created", "description", "engagement_id", "environment_id", "id", "lead_id", "percent_complete", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "scan_type", "target_end", "target_start", "test_type_id", "title", "updated", "version") VALUES (OLD."api_scan_configuration_id", OLD."archive_batch", OLD."archived_at", OLD."branch_tag", OLD."build_id", OLD."commit_hash", OLD."created", OLD."description", OLD."engagement_id", OLD."environment_id", OLD."id", OLD."lead_id", OLD."percent_complete", _pgh_attach_context(), NOW(), 'delete', OLD."id", OLD."scan_type", OLD."target_end", OLD."target_start", OLD."test_type_id", OLD."title", OLD."updated", OLD."version"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_delete_delete_66d18 ON "dojo_test"; + CREATE TRIGGER pgtrigger_delete_delete_66d18 + AFTER DELETE ON "dojo_test" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_delete_delete_66d18(); + + COMMENT ON TRIGGER pgtrigger_delete_delete_66d18 ON "dojo_test" IS '0b9eda2793d6208cb855751dc6c39f8d6819cd2d'; +""" + +BACKWARD_SQL = r""" +-- +-- Create trigger delete_delete on model test +-- +DROP TRIGGER IF EXISTS pgtrigger_delete_delete_66d18 ON "dojo_test"; +-- +-- Create trigger update_update on model test +-- +DROP TRIGGER IF EXISTS pgtrigger_update_update_c40f8 ON "dojo_test"; +-- +-- Create trigger insert_insert on model test +-- +DROP TRIGGER IF EXISTS pgtrigger_insert_insert_ecfc1 ON "dojo_test"; +-- +-- Create trigger delete_delete on model product_type +-- +DROP TRIGGER IF EXISTS pgtrigger_delete_delete_66b18 ON "dojo_product_type"; +-- +-- Create trigger update_update on model product_type +-- +DROP TRIGGER IF EXISTS pgtrigger_update_update_a0136 ON "dojo_product_type"; +-- +-- Create trigger insert_insert on model product_type +-- +DROP TRIGGER IF EXISTS pgtrigger_insert_insert_2d109 ON "dojo_product_type"; +-- +-- Create trigger delete_delete on model product +-- +DROP TRIGGER IF EXISTS pgtrigger_delete_delete_064dd ON "dojo_product"; +-- +-- Create trigger update_update on model product +-- +DROP TRIGGER IF EXISTS pgtrigger_update_update_e7040 ON "dojo_product"; +-- +-- Create trigger insert_insert on model product +-- +DROP TRIGGER IF EXISTS pgtrigger_insert_insert_d5d32 ON "dojo_product"; +-- +-- Create trigger delete_delete on model finding_group +-- +DROP TRIGGER IF EXISTS pgtrigger_delete_delete_24b41 ON "dojo_finding_group"; +-- +-- Create trigger update_update on model finding_group +-- +DROP TRIGGER IF EXISTS pgtrigger_update_update_37b5f ON "dojo_finding_group"; +-- +-- Create trigger insert_insert on model finding_group +-- +DROP TRIGGER IF EXISTS pgtrigger_insert_insert_94bd2 ON "dojo_finding_group"; +-- +-- Create trigger delete_delete on model finding +-- +DROP TRIGGER IF EXISTS pgtrigger_delete_delete_72933 ON "dojo_finding"; +-- +-- Create trigger update_update on model finding +-- +DROP TRIGGER IF EXISTS pgtrigger_update_update_92175 ON "dojo_finding"; +-- +-- Create trigger insert_insert on model finding +-- +DROP TRIGGER IF EXISTS pgtrigger_insert_insert_2fbbb ON "dojo_finding"; +-- +-- Create trigger delete_delete on model engagement +-- +DROP TRIGGER IF EXISTS pgtrigger_delete_delete_9f4df ON "dojo_engagement"; +-- +-- Create trigger update_update on model engagement +-- +DROP TRIGGER IF EXISTS pgtrigger_update_update_65136 ON "dojo_engagement"; +-- +-- Create trigger insert_insert on model engagement +-- +DROP TRIGGER IF EXISTS pgtrigger_insert_insert_125f1 ON "dojo_engagement"; +-- +-- Create trigger delete_delete on model endpoint +-- +DROP TRIGGER IF EXISTS pgtrigger_delete_delete_dd1f9 ON "dojo_endpoint"; +-- +-- Create trigger update_update on model endpoint +-- +DROP TRIGGER IF EXISTS pgtrigger_update_update_2b19a ON "dojo_endpoint"; +-- +-- Create trigger insert_insert on model endpoint +-- +DROP TRIGGER IF EXISTS pgtrigger_insert_insert_c7973 ON "dojo_endpoint"; +-- +-- Add field archived_at to vulnerability_id +-- +ALTER TABLE "dojo_vulnerability_id" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to vulnerability_id +-- +ALTER TABLE "dojo_vulnerability_id" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to tool_product_settings +-- +ALTER TABLE "dojo_tool_product_settings" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to tool_product_settings +-- +ALTER TABLE "dojo_tool_product_settings" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to testevent +-- +ALTER TABLE "dojo_testevent" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to testevent +-- +ALTER TABLE "dojo_testevent" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to test_import_finding_action +-- +ALTER TABLE "dojo_test_import_finding_action" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to test_import_finding_action +-- +ALTER TABLE "dojo_test_import_finding_action" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to test_import +-- +ALTER TABLE "dojo_test_import" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to test_import +-- +ALTER TABLE "dojo_test_import" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to test +-- +ALTER TABLE "dojo_test" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to test +-- +ALTER TABLE "dojo_test" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to productevent +-- +ALTER TABLE "dojo_productevent" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to productevent +-- +ALTER TABLE "dojo_productevent" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to product_typeevent +-- +ALTER TABLE "dojo_product_typeevent" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to product_typeevent +-- +ALTER TABLE "dojo_product_typeevent" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to product_type +-- +ALTER TABLE "dojo_product_type" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to product_type +-- +ALTER TABLE "dojo_product_type" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to product_api_scan_configuration +-- +ALTER TABLE "dojo_product_api_scan_configuration" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to product_api_scan_configuration +-- +ALTER TABLE "dojo_product_api_scan_configuration" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to product +-- +ALTER TABLE "dojo_product" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to product +-- +ALTER TABLE "dojo_product" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to objects_product +-- +ALTER TABLE "dojo_objects_product" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to objects_product +-- +ALTER TABLE "dojo_objects_product" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to locationproductreference +-- +ALTER TABLE "dojo_locationproductreference" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to locationproductreference +-- +ALTER TABLE "dojo_locationproductreference" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to locationfindingreference +-- +ALTER TABLE "dojo_locationfindingreference" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to locationfindingreference +-- +ALTER TABLE "dojo_locationfindingreference" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to languages +-- +ALTER TABLE "dojo_languages" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to languages +-- +ALTER TABLE "dojo_languages" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to jira_project +-- +ALTER TABLE "dojo_jira_project" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to jira_project +-- +ALTER TABLE "dojo_jira_project" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to jira_issue +-- +ALTER TABLE "dojo_jira_issue" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to jira_issue +-- +ALTER TABLE "dojo_jira_issue" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to findingvulnerabilityreference +-- +ALTER TABLE "dojo_findingvulnerabilityreference" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to findingvulnerabilityreference +-- +ALTER TABLE "dojo_findingvulnerabilityreference" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to findingevent +-- +ALTER TABLE "dojo_findingevent" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to findingevent +-- +ALTER TABLE "dojo_findingevent" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to finding_groupevent +-- +ALTER TABLE "dojo_finding_groupevent" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to finding_groupevent +-- +ALTER TABLE "dojo_finding_groupevent" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to finding_group +-- +ALTER TABLE "dojo_finding_group" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to finding_group +-- +ALTER TABLE "dojo_finding_group" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to finding +-- +ALTER TABLE "dojo_finding" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to finding +-- +ALTER TABLE "dojo_finding" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to engagementevent +-- +ALTER TABLE "dojo_engagementevent" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to engagementevent +-- +ALTER TABLE "dojo_engagementevent" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to engagement_presets +-- +ALTER TABLE "dojo_engagement_presets" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to engagement_presets +-- +ALTER TABLE "dojo_engagement_presets" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to engagement +-- +ALTER TABLE "dojo_engagement" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to engagement +-- +ALTER TABLE "dojo_engagement" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to endpointevent +-- +ALTER TABLE "dojo_endpointevent" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to endpointevent +-- +ALTER TABLE "dojo_endpointevent" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to endpoint_status +-- +ALTER TABLE "dojo_endpoint_status" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to endpoint_status +-- +ALTER TABLE "dojo_endpoint_status" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to endpoint +-- +ALTER TABLE "dojo_endpoint" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to endpoint +-- +ALTER TABLE "dojo_endpoint" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to dojometa +-- +ALTER TABLE "dojo_dojometa" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to dojometa +-- +ALTER TABLE "dojo_dojometa" DROP COLUMN "archive_batch" CASCADE; +-- +-- Add field archived_at to app_analysis +-- +ALTER TABLE "dojo_app_analysis" DROP COLUMN "archived_at" CASCADE; +-- +-- Add field archive_batch to app_analysis +-- +ALTER TABLE "dojo_app_analysis" DROP COLUMN "archive_batch" CASCADE; +-- +-- Remove trigger delete_delete from model test +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_delete_delete_66d18() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_testevent" ("api_scan_configuration_id", "branch_tag", "build_id", "commit_hash", "created", "description", "engagement_id", "environment_id", "id", "lead_id", "percent_complete", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "scan_type", "target_end", "target_start", "test_type_id", "title", "updated", "version") VALUES (OLD."api_scan_configuration_id", OLD."branch_tag", OLD."build_id", OLD."commit_hash", OLD."created", OLD."description", OLD."engagement_id", OLD."environment_id", OLD."id", OLD."lead_id", OLD."percent_complete", _pgh_attach_context(), NOW(), 'delete', OLD."id", OLD."scan_type", OLD."target_end", OLD."target_start", OLD."test_type_id", OLD."title", OLD."updated", OLD."version"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_delete_delete_66d18 ON "dojo_test"; + CREATE TRIGGER pgtrigger_delete_delete_66d18 + AFTER DELETE ON "dojo_test" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_delete_delete_66d18(); + + COMMENT ON TRIGGER pgtrigger_delete_delete_66d18 ON "dojo_test" IS '51bce27193221308adc41e62f1faff5122bbbceb'; + +-- +-- Remove trigger update_update from model test +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_update_update_c40f8() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_testevent" ("api_scan_configuration_id", "branch_tag", "build_id", "commit_hash", "created", "description", "engagement_id", "environment_id", "id", "lead_id", "percent_complete", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "scan_type", "target_end", "target_start", "test_type_id", "title", "updated", "version") VALUES (NEW."api_scan_configuration_id", NEW."branch_tag", NEW."build_id", NEW."commit_hash", NEW."created", NEW."description", NEW."engagement_id", NEW."environment_id", NEW."id", NEW."lead_id", NEW."percent_complete", _pgh_attach_context(), NOW(), 'update', NEW."id", NEW."scan_type", NEW."target_end", NEW."target_start", NEW."test_type_id", NEW."title", NEW."updated", NEW."version"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_update_update_c40f8 ON "dojo_test"; + CREATE TRIGGER pgtrigger_update_update_c40f8 + AFTER UPDATE ON "dojo_test" + + + FOR EACH ROW WHEN (OLD."api_scan_configuration_id" IS DISTINCT FROM (NEW."api_scan_configuration_id") OR OLD."branch_tag" IS DISTINCT FROM (NEW."branch_tag") OR OLD."build_id" IS DISTINCT FROM (NEW."build_id") OR OLD."commit_hash" IS DISTINCT FROM (NEW."commit_hash") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."engagement_id" IS DISTINCT FROM (NEW."engagement_id") OR OLD."environment_id" IS DISTINCT FROM (NEW."environment_id") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."lead_id" IS DISTINCT FROM (NEW."lead_id") OR OLD."percent_complete" IS DISTINCT FROM (NEW."percent_complete") OR OLD."scan_type" IS DISTINCT FROM (NEW."scan_type") OR OLD."target_end" IS DISTINCT FROM (NEW."target_end") OR OLD."target_start" IS DISTINCT FROM (NEW."target_start") OR OLD."test_type_id" IS DISTINCT FROM (NEW."test_type_id") OR OLD."title" IS DISTINCT FROM (NEW."title") OR OLD."version" IS DISTINCT FROM (NEW."version")) + EXECUTE PROCEDURE pgtrigger_update_update_c40f8(); + + COMMENT ON TRIGGER pgtrigger_update_update_c40f8 ON "dojo_test" IS '777c92a16d48f7e590e50cb8fb6c0d77c9afa1b6'; + +-- +-- Remove trigger insert_insert from model test +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_insert_insert_ecfc1() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_testevent" ("api_scan_configuration_id", "branch_tag", "build_id", "commit_hash", "created", "description", "engagement_id", "environment_id", "id", "lead_id", "percent_complete", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "scan_type", "target_end", "target_start", "test_type_id", "title", "updated", "version") VALUES (NEW."api_scan_configuration_id", NEW."branch_tag", NEW."build_id", NEW."commit_hash", NEW."created", NEW."description", NEW."engagement_id", NEW."environment_id", NEW."id", NEW."lead_id", NEW."percent_complete", _pgh_attach_context(), NOW(), 'insert', NEW."id", NEW."scan_type", NEW."target_end", NEW."target_start", NEW."test_type_id", NEW."title", NEW."updated", NEW."version"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_insert_insert_ecfc1 ON "dojo_test"; + CREATE TRIGGER pgtrigger_insert_insert_ecfc1 + AFTER INSERT ON "dojo_test" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_insert_insert_ecfc1(); + + COMMENT ON TRIGGER pgtrigger_insert_insert_ecfc1 ON "dojo_test" IS '0b6ec21ca35b61b1abcc0b2f8629cb4d1cc92930'; + +-- +-- Remove trigger delete_delete from model product_type +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_delete_delete_66b18() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_product_typeevent" ("created", "critical_product", "description", "id", "key_product", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "updated") VALUES (OLD."created", OLD."critical_product", OLD."description", OLD."id", OLD."key_product", OLD."name", _pgh_attach_context(), NOW(), 'delete', OLD."id", OLD."updated"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_delete_delete_66b18 ON "dojo_product_type"; + CREATE TRIGGER pgtrigger_delete_delete_66b18 + AFTER DELETE ON "dojo_product_type" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_delete_delete_66b18(); + + COMMENT ON TRIGGER pgtrigger_delete_delete_66b18 ON "dojo_product_type" IS '1b3bb470de18c3270ba2a1d4453d276b34f650dc'; + +-- +-- Remove trigger update_update from model product_type +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_update_update_a0136() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_product_typeevent" ("created", "critical_product", "description", "id", "key_product", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "updated") VALUES (NEW."created", NEW."critical_product", NEW."description", NEW."id", NEW."key_product", NEW."name", _pgh_attach_context(), NOW(), 'update', NEW."id", NEW."updated"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_update_update_a0136 ON "dojo_product_type"; + CREATE TRIGGER pgtrigger_update_update_a0136 + AFTER UPDATE ON "dojo_product_type" + + + FOR EACH ROW WHEN (OLD."critical_product" IS DISTINCT FROM (NEW."critical_product") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."key_product" IS DISTINCT FROM (NEW."key_product") OR OLD."name" IS DISTINCT FROM (NEW."name")) + EXECUTE PROCEDURE pgtrigger_update_update_a0136(); + + COMMENT ON TRIGGER pgtrigger_update_update_a0136 ON "dojo_product_type" IS '97c18814aec2e06d73b89e3eb65e0cfbf832dda5'; + +-- +-- Remove trigger insert_insert from model product_type +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_insert_insert_2d109() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_product_typeevent" ("created", "critical_product", "description", "id", "key_product", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "updated") VALUES (NEW."created", NEW."critical_product", NEW."description", NEW."id", NEW."key_product", NEW."name", _pgh_attach_context(), NOW(), 'insert', NEW."id", NEW."updated"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_insert_insert_2d109 ON "dojo_product_type"; + CREATE TRIGGER pgtrigger_insert_insert_2d109 + AFTER INSERT ON "dojo_product_type" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_insert_insert_2d109(); + + COMMENT ON TRIGGER pgtrigger_insert_insert_2d109 ON "dojo_product_type" IS '3f3d8e8a68a63bc86ff3557f8fc5f54d950e9d6d'; + +-- +-- Remove trigger delete_delete from model product +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_delete_delete_064dd() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_productevent" ("async_updating", "business_criticality", "created", "description", "disable_sla_breach_notifications", "enable_full_risk_acceptance", "enable_product_tag_inheritance", "enable_simple_risk_acceptance", "external_audience", "id", "internet_accessible", "lifecycle_id", "name", "origin_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "platform_id", "prod_numeric_grade", "prod_type_id", "product_manager_id", "revenue", "sla_configuration_id", "team_manager_id", "technical_contact_id", "tid", "updated", "user_records") VALUES (OLD."async_updating", OLD."business_criticality", OLD."created", OLD."description", OLD."disable_sla_breach_notifications", OLD."enable_full_risk_acceptance", OLD."enable_product_tag_inheritance", OLD."enable_simple_risk_acceptance", OLD."external_audience", OLD."id", OLD."internet_accessible", OLD."lifecycle_id", OLD."name", OLD."origin_id", _pgh_attach_context(), NOW(), 'delete', OLD."id", OLD."platform_id", OLD."prod_numeric_grade", OLD."prod_type_id", OLD."product_manager_id", OLD."revenue", OLD."sla_configuration_id", OLD."team_manager_id", OLD."technical_contact_id", OLD."tid", OLD."updated", OLD."user_records"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_delete_delete_064dd ON "dojo_product"; + CREATE TRIGGER pgtrigger_delete_delete_064dd + AFTER DELETE ON "dojo_product" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_delete_delete_064dd(); + + COMMENT ON TRIGGER pgtrigger_delete_delete_064dd ON "dojo_product" IS 'fb5075dda22c29fcfbf16c799b636b5dbf993777'; + +-- +-- Remove trigger update_update from model product +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_update_update_e7040() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_productevent" ("async_updating", "business_criticality", "created", "description", "disable_sla_breach_notifications", "enable_full_risk_acceptance", "enable_product_tag_inheritance", "enable_simple_risk_acceptance", "external_audience", "id", "internet_accessible", "lifecycle_id", "name", "origin_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "platform_id", "prod_numeric_grade", "prod_type_id", "product_manager_id", "revenue", "sla_configuration_id", "team_manager_id", "technical_contact_id", "tid", "updated", "user_records") VALUES (NEW."async_updating", NEW."business_criticality", NEW."created", NEW."description", NEW."disable_sla_breach_notifications", NEW."enable_full_risk_acceptance", NEW."enable_product_tag_inheritance", NEW."enable_simple_risk_acceptance", NEW."external_audience", NEW."id", NEW."internet_accessible", NEW."lifecycle_id", NEW."name", NEW."origin_id", _pgh_attach_context(), NOW(), 'update', NEW."id", NEW."platform_id", NEW."prod_numeric_grade", NEW."prod_type_id", NEW."product_manager_id", NEW."revenue", NEW."sla_configuration_id", NEW."team_manager_id", NEW."technical_contact_id", NEW."tid", NEW."updated", NEW."user_records"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_update_update_e7040 ON "dojo_product"; + CREATE TRIGGER pgtrigger_update_update_e7040 + AFTER UPDATE ON "dojo_product" + + + FOR EACH ROW WHEN (OLD."async_updating" IS DISTINCT FROM (NEW."async_updating") OR OLD."business_criticality" IS DISTINCT FROM (NEW."business_criticality") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."disable_sla_breach_notifications" IS DISTINCT FROM (NEW."disable_sla_breach_notifications") OR OLD."enable_full_risk_acceptance" IS DISTINCT FROM (NEW."enable_full_risk_acceptance") OR OLD."enable_product_tag_inheritance" IS DISTINCT FROM (NEW."enable_product_tag_inheritance") OR OLD."enable_simple_risk_acceptance" IS DISTINCT FROM (NEW."enable_simple_risk_acceptance") OR OLD."external_audience" IS DISTINCT FROM (NEW."external_audience") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."internet_accessible" IS DISTINCT FROM (NEW."internet_accessible") OR OLD."lifecycle_id" IS DISTINCT FROM (NEW."lifecycle_id") OR OLD."name" IS DISTINCT FROM (NEW."name") OR OLD."origin_id" IS DISTINCT FROM (NEW."origin_id") OR OLD."platform_id" IS DISTINCT FROM (NEW."platform_id") OR OLD."prod_numeric_grade" IS DISTINCT FROM (NEW."prod_numeric_grade") OR OLD."prod_type_id" IS DISTINCT FROM (NEW."prod_type_id") OR OLD."product_manager_id" IS DISTINCT FROM (NEW."product_manager_id") OR OLD."revenue" IS DISTINCT FROM (NEW."revenue") OR OLD."sla_configuration_id" IS DISTINCT FROM (NEW."sla_configuration_id") OR OLD."team_manager_id" IS DISTINCT FROM (NEW."team_manager_id") OR OLD."technical_contact_id" IS DISTINCT FROM (NEW."technical_contact_id") OR OLD."tid" IS DISTINCT FROM (NEW."tid") OR OLD."user_records" IS DISTINCT FROM (NEW."user_records")) + EXECUTE PROCEDURE pgtrigger_update_update_e7040(); + + COMMENT ON TRIGGER pgtrigger_update_update_e7040 ON "dojo_product" IS '49923bd314b57154a13024be447d70ff62dda123'; + +-- +-- Remove trigger insert_insert from model product +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_insert_insert_d5d32() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_productevent" ("async_updating", "business_criticality", "created", "description", "disable_sla_breach_notifications", "enable_full_risk_acceptance", "enable_product_tag_inheritance", "enable_simple_risk_acceptance", "external_audience", "id", "internet_accessible", "lifecycle_id", "name", "origin_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "platform_id", "prod_numeric_grade", "prod_type_id", "product_manager_id", "revenue", "sla_configuration_id", "team_manager_id", "technical_contact_id", "tid", "updated", "user_records") VALUES (NEW."async_updating", NEW."business_criticality", NEW."created", NEW."description", NEW."disable_sla_breach_notifications", NEW."enable_full_risk_acceptance", NEW."enable_product_tag_inheritance", NEW."enable_simple_risk_acceptance", NEW."external_audience", NEW."id", NEW."internet_accessible", NEW."lifecycle_id", NEW."name", NEW."origin_id", _pgh_attach_context(), NOW(), 'insert', NEW."id", NEW."platform_id", NEW."prod_numeric_grade", NEW."prod_type_id", NEW."product_manager_id", NEW."revenue", NEW."sla_configuration_id", NEW."team_manager_id", NEW."technical_contact_id", NEW."tid", NEW."updated", NEW."user_records"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_insert_insert_d5d32 ON "dojo_product"; + CREATE TRIGGER pgtrigger_insert_insert_d5d32 + AFTER INSERT ON "dojo_product" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_insert_insert_d5d32(); + + COMMENT ON TRIGGER pgtrigger_insert_insert_d5d32 ON "dojo_product" IS '56d7d4e2ec7a3c855444408a7cd6be18e2e9ab02'; + +-- +-- Remove trigger delete_delete from model finding_group +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_delete_delete_24b41() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_finding_groupevent" ("created", "creator_id", "id", "modified", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "test_id") VALUES (OLD."created", OLD."creator_id", OLD."id", OLD."modified", OLD."name", _pgh_attach_context(), NOW(), 'delete', OLD."id", OLD."test_id"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_delete_delete_24b41 ON "dojo_finding_group"; + CREATE TRIGGER pgtrigger_delete_delete_24b41 + AFTER DELETE ON "dojo_finding_group" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_delete_delete_24b41(); + + COMMENT ON TRIGGER pgtrigger_delete_delete_24b41 ON "dojo_finding_group" IS '4d7241855d22804d01d455fb4228e750c5b11497'; + +-- +-- Remove trigger update_update from model finding_group +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_update_update_37b5f() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_finding_groupevent" ("created", "creator_id", "id", "modified", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "test_id") VALUES (NEW."created", NEW."creator_id", NEW."id", NEW."modified", NEW."name", _pgh_attach_context(), NOW(), 'update', NEW."id", NEW."test_id"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_update_update_37b5f ON "dojo_finding_group"; + CREATE TRIGGER pgtrigger_update_update_37b5f + AFTER UPDATE ON "dojo_finding_group" + + + FOR EACH ROW WHEN (OLD."creator_id" IS DISTINCT FROM (NEW."creator_id") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."name" IS DISTINCT FROM (NEW."name") OR OLD."test_id" IS DISTINCT FROM (NEW."test_id")) + EXECUTE PROCEDURE pgtrigger_update_update_37b5f(); + + COMMENT ON TRIGGER pgtrigger_update_update_37b5f ON "dojo_finding_group" IS 'd5d94d19b54079f6c284d9448666eb408b20f245'; + +-- +-- Remove trigger insert_insert from model finding_group +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_insert_insert_94bd2() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_finding_groupevent" ("created", "creator_id", "id", "modified", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "test_id") VALUES (NEW."created", NEW."creator_id", NEW."id", NEW."modified", NEW."name", _pgh_attach_context(), NOW(), 'insert', NEW."id", NEW."test_id"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_insert_insert_94bd2 ON "dojo_finding_group"; + CREATE TRIGGER pgtrigger_insert_insert_94bd2 + AFTER INSERT ON "dojo_finding_group" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_insert_insert_94bd2(); + + COMMENT ON TRIGGER pgtrigger_insert_insert_94bd2 ON "dojo_finding_group" IS 'ccce37d431a0be588c20aa39b570893c63be9b4b'; + +-- +-- Remove trigger delete_delete from model finding +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_delete_delete_72933() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_findingevent" ("active", "component_name", "component_version", "created", "cve", "cvssv3", "cvssv3_score", "cvssv4", "cvssv4_score", "cwe", "date", "defect_review_requested_by_id", "description", "duplicate", "duplicate_finding_id", "dynamic_finding", "effort_for_fixing", "epss_percentile", "epss_score", "false_p", "file_path", "fix_available", "fix_version", "hash_code", "id", "impact", "is_mitigated", "kev_date", "known_exploited", "last_reviewed", "last_reviewed_by_id", "last_status_update", "line", "mitigated", "mitigated_by_id", "mitigation", "nb_occurences", "numerical_severity", "out_of_scope", "param", "payload", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "planned_remediation_date", "planned_remediation_version", "publish_date", "ransomware_used", "refs", "reporter_id", "review_requested_by_id", "risk_accepted", "sast_sink_object", "sast_source_file_path", "sast_source_line", "sast_source_object", "scanner_confidence", "service", "severity", "severity_justification", "sla_expiration_date", "sla_start_date", "sonarqube_issue_id", "static_finding", "steps_to_reproduce", "test_id", "thread_id", "title", "under_defect_review", "under_review", "unique_id_from_tool", "updated", "url", "verified", "vuln_id_from_tool") VALUES (OLD."active", OLD."component_name", OLD."component_version", OLD."created", OLD."cve", OLD."cvssv3", OLD."cvssv3_score", OLD."cvssv4", OLD."cvssv4_score", OLD."cwe", OLD."date", OLD."defect_review_requested_by_id", OLD."description", OLD."duplicate", OLD."duplicate_finding_id", OLD."dynamic_finding", OLD."effort_for_fixing", OLD."epss_percentile", OLD."epss_score", OLD."false_p", OLD."file_path", OLD."fix_available", OLD."fix_version", OLD."hash_code", OLD."id", OLD."impact", OLD."is_mitigated", OLD."kev_date", OLD."known_exploited", OLD."last_reviewed", OLD."last_reviewed_by_id", OLD."last_status_update", OLD."line", OLD."mitigated", OLD."mitigated_by_id", OLD."mitigation", OLD."nb_occurences", OLD."numerical_severity", OLD."out_of_scope", OLD."param", OLD."payload", _pgh_attach_context(), NOW(), 'delete', OLD."id", OLD."planned_remediation_date", OLD."planned_remediation_version", OLD."publish_date", OLD."ransomware_used", OLD."refs", OLD."reporter_id", OLD."review_requested_by_id", OLD."risk_accepted", OLD."sast_sink_object", OLD."sast_source_file_path", OLD."sast_source_line", OLD."sast_source_object", OLD."scanner_confidence", OLD."service", OLD."severity", OLD."severity_justification", OLD."sla_expiration_date", OLD."sla_start_date", OLD."sonarqube_issue_id", OLD."static_finding", OLD."steps_to_reproduce", OLD."test_id", OLD."thread_id", OLD."title", OLD."under_defect_review", OLD."under_review", OLD."unique_id_from_tool", OLD."updated", OLD."url", OLD."verified", OLD."vuln_id_from_tool"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_delete_delete_72933 ON "dojo_finding"; + CREATE TRIGGER pgtrigger_delete_delete_72933 + AFTER DELETE ON "dojo_finding" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_delete_delete_72933(); + + COMMENT ON TRIGGER pgtrigger_delete_delete_72933 ON "dojo_finding" IS 'dd1240d046d5292ab4e6ce9cd193d921c9c2d309'; + +-- +-- Remove trigger update_update from model finding +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_update_update_92175() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_findingevent" ("active", "component_name", "component_version", "created", "cve", "cvssv3", "cvssv3_score", "cvssv4", "cvssv4_score", "cwe", "date", "defect_review_requested_by_id", "description", "duplicate", "duplicate_finding_id", "dynamic_finding", "effort_for_fixing", "epss_percentile", "epss_score", "false_p", "file_path", "fix_available", "fix_version", "hash_code", "id", "impact", "is_mitigated", "kev_date", "known_exploited", "last_reviewed", "last_reviewed_by_id", "last_status_update", "line", "mitigated", "mitigated_by_id", "mitigation", "nb_occurences", "numerical_severity", "out_of_scope", "param", "payload", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "planned_remediation_date", "planned_remediation_version", "publish_date", "ransomware_used", "refs", "reporter_id", "review_requested_by_id", "risk_accepted", "sast_sink_object", "sast_source_file_path", "sast_source_line", "sast_source_object", "scanner_confidence", "service", "severity", "severity_justification", "sla_expiration_date", "sla_start_date", "sonarqube_issue_id", "static_finding", "steps_to_reproduce", "test_id", "thread_id", "title", "under_defect_review", "under_review", "unique_id_from_tool", "updated", "url", "verified", "vuln_id_from_tool") VALUES (NEW."active", NEW."component_name", NEW."component_version", NEW."created", NEW."cve", NEW."cvssv3", NEW."cvssv3_score", NEW."cvssv4", NEW."cvssv4_score", NEW."cwe", NEW."date", NEW."defect_review_requested_by_id", NEW."description", NEW."duplicate", NEW."duplicate_finding_id", NEW."dynamic_finding", NEW."effort_for_fixing", NEW."epss_percentile", NEW."epss_score", NEW."false_p", NEW."file_path", NEW."fix_available", NEW."fix_version", NEW."hash_code", NEW."id", NEW."impact", NEW."is_mitigated", NEW."kev_date", NEW."known_exploited", NEW."last_reviewed", NEW."last_reviewed_by_id", NEW."last_status_update", NEW."line", NEW."mitigated", NEW."mitigated_by_id", NEW."mitigation", NEW."nb_occurences", NEW."numerical_severity", NEW."out_of_scope", NEW."param", NEW."payload", _pgh_attach_context(), NOW(), 'update', NEW."id", NEW."planned_remediation_date", NEW."planned_remediation_version", NEW."publish_date", NEW."ransomware_used", NEW."refs", NEW."reporter_id", NEW."review_requested_by_id", NEW."risk_accepted", NEW."sast_sink_object", NEW."sast_source_file_path", NEW."sast_source_line", NEW."sast_source_object", NEW."scanner_confidence", NEW."service", NEW."severity", NEW."severity_justification", NEW."sla_expiration_date", NEW."sla_start_date", NEW."sonarqube_issue_id", NEW."static_finding", NEW."steps_to_reproduce", NEW."test_id", NEW."thread_id", NEW."title", NEW."under_defect_review", NEW."under_review", NEW."unique_id_from_tool", NEW."updated", NEW."url", NEW."verified", NEW."vuln_id_from_tool"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_update_update_92175 ON "dojo_finding"; + CREATE TRIGGER pgtrigger_update_update_92175 + AFTER UPDATE ON "dojo_finding" + + + FOR EACH ROW WHEN (OLD."active" IS DISTINCT FROM (NEW."active") OR OLD."component_name" IS DISTINCT FROM (NEW."component_name") OR OLD."component_version" IS DISTINCT FROM (NEW."component_version") OR OLD."cve" IS DISTINCT FROM (NEW."cve") OR OLD."cvssv3" IS DISTINCT FROM (NEW."cvssv3") OR OLD."cvssv3_score" IS DISTINCT FROM (NEW."cvssv3_score") OR OLD."cvssv4" IS DISTINCT FROM (NEW."cvssv4") OR OLD."cvssv4_score" IS DISTINCT FROM (NEW."cvssv4_score") OR OLD."cwe" IS DISTINCT FROM (NEW."cwe") OR OLD."date" IS DISTINCT FROM (NEW."date") OR OLD."defect_review_requested_by_id" IS DISTINCT FROM (NEW."defect_review_requested_by_id") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."duplicate" IS DISTINCT FROM (NEW."duplicate") OR OLD."duplicate_finding_id" IS DISTINCT FROM (NEW."duplicate_finding_id") OR OLD."dynamic_finding" IS DISTINCT FROM (NEW."dynamic_finding") OR OLD."effort_for_fixing" IS DISTINCT FROM (NEW."effort_for_fixing") OR OLD."epss_percentile" IS DISTINCT FROM (NEW."epss_percentile") OR OLD."epss_score" IS DISTINCT FROM (NEW."epss_score") OR OLD."false_p" IS DISTINCT FROM (NEW."false_p") OR OLD."file_path" IS DISTINCT FROM (NEW."file_path") OR OLD."fix_available" IS DISTINCT FROM (NEW."fix_available") OR OLD."fix_version" IS DISTINCT FROM (NEW."fix_version") OR OLD."hash_code" IS DISTINCT FROM (NEW."hash_code") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."impact" IS DISTINCT FROM (NEW."impact") OR OLD."is_mitigated" IS DISTINCT FROM (NEW."is_mitigated") OR OLD."kev_date" IS DISTINCT FROM (NEW."kev_date") OR OLD."known_exploited" IS DISTINCT FROM (NEW."known_exploited") OR OLD."last_reviewed" IS DISTINCT FROM (NEW."last_reviewed") OR OLD."last_reviewed_by_id" IS DISTINCT FROM (NEW."last_reviewed_by_id") OR OLD."line" IS DISTINCT FROM (NEW."line") OR OLD."mitigated" IS DISTINCT FROM (NEW."mitigated") OR OLD."mitigated_by_id" IS DISTINCT FROM (NEW."mitigated_by_id") OR OLD."mitigation" IS DISTINCT FROM (NEW."mitigation") OR OLD."nb_occurences" IS DISTINCT FROM (NEW."nb_occurences") OR OLD."numerical_severity" IS DISTINCT FROM (NEW."numerical_severity") OR OLD."out_of_scope" IS DISTINCT FROM (NEW."out_of_scope") OR OLD."param" IS DISTINCT FROM (NEW."param") OR OLD."payload" IS DISTINCT FROM (NEW."payload") OR OLD."planned_remediation_date" IS DISTINCT FROM (NEW."planned_remediation_date") OR OLD."planned_remediation_version" IS DISTINCT FROM (NEW."planned_remediation_version") OR OLD."publish_date" IS DISTINCT FROM (NEW."publish_date") OR OLD."ransomware_used" IS DISTINCT FROM (NEW."ransomware_used") OR OLD."refs" IS DISTINCT FROM (NEW."refs") OR OLD."reporter_id" IS DISTINCT FROM (NEW."reporter_id") OR OLD."review_requested_by_id" IS DISTINCT FROM (NEW."review_requested_by_id") OR OLD."risk_accepted" IS DISTINCT FROM (NEW."risk_accepted") OR OLD."sast_sink_object" IS DISTINCT FROM (NEW."sast_sink_object") OR OLD."sast_source_file_path" IS DISTINCT FROM (NEW."sast_source_file_path") OR OLD."sast_source_line" IS DISTINCT FROM (NEW."sast_source_line") OR OLD."sast_source_object" IS DISTINCT FROM (NEW."sast_source_object") OR OLD."scanner_confidence" IS DISTINCT FROM (NEW."scanner_confidence") OR OLD."service" IS DISTINCT FROM (NEW."service") OR OLD."severity" IS DISTINCT FROM (NEW."severity") OR OLD."severity_justification" IS DISTINCT FROM (NEW."severity_justification") OR OLD."sla_expiration_date" IS DISTINCT FROM (NEW."sla_expiration_date") OR OLD."sla_start_date" IS DISTINCT FROM (NEW."sla_start_date") OR OLD."sonarqube_issue_id" IS DISTINCT FROM (NEW."sonarqube_issue_id") OR OLD."static_finding" IS DISTINCT FROM (NEW."static_finding") OR OLD."steps_to_reproduce" IS DISTINCT FROM (NEW."steps_to_reproduce") OR OLD."test_id" IS DISTINCT FROM (NEW."test_id") OR OLD."thread_id" IS DISTINCT FROM (NEW."thread_id") OR OLD."title" IS DISTINCT FROM (NEW."title") OR OLD."under_defect_review" IS DISTINCT FROM (NEW."under_defect_review") OR OLD."under_review" IS DISTINCT FROM (NEW."under_review") OR OLD."unique_id_from_tool" IS DISTINCT FROM (NEW."unique_id_from_tool") OR OLD."url" IS DISTINCT FROM (NEW."url") OR OLD."verified" IS DISTINCT FROM (NEW."verified") OR OLD."vuln_id_from_tool" IS DISTINCT FROM (NEW."vuln_id_from_tool")) + EXECUTE PROCEDURE pgtrigger_update_update_92175(); + + COMMENT ON TRIGGER pgtrigger_update_update_92175 ON "dojo_finding" IS '3a8608f3269631a51e32dffe79021258c6c4af33'; + +-- +-- Remove trigger insert_insert from model finding +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_insert_insert_2fbbb() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_findingevent" ("active", "component_name", "component_version", "created", "cve", "cvssv3", "cvssv3_score", "cvssv4", "cvssv4_score", "cwe", "date", "defect_review_requested_by_id", "description", "duplicate", "duplicate_finding_id", "dynamic_finding", "effort_for_fixing", "epss_percentile", "epss_score", "false_p", "file_path", "fix_available", "fix_version", "hash_code", "id", "impact", "is_mitigated", "kev_date", "known_exploited", "last_reviewed", "last_reviewed_by_id", "last_status_update", "line", "mitigated", "mitigated_by_id", "mitigation", "nb_occurences", "numerical_severity", "out_of_scope", "param", "payload", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "planned_remediation_date", "planned_remediation_version", "publish_date", "ransomware_used", "refs", "reporter_id", "review_requested_by_id", "risk_accepted", "sast_sink_object", "sast_source_file_path", "sast_source_line", "sast_source_object", "scanner_confidence", "service", "severity", "severity_justification", "sla_expiration_date", "sla_start_date", "sonarqube_issue_id", "static_finding", "steps_to_reproduce", "test_id", "thread_id", "title", "under_defect_review", "under_review", "unique_id_from_tool", "updated", "url", "verified", "vuln_id_from_tool") VALUES (NEW."active", NEW."component_name", NEW."component_version", NEW."created", NEW."cve", NEW."cvssv3", NEW."cvssv3_score", NEW."cvssv4", NEW."cvssv4_score", NEW."cwe", NEW."date", NEW."defect_review_requested_by_id", NEW."description", NEW."duplicate", NEW."duplicate_finding_id", NEW."dynamic_finding", NEW."effort_for_fixing", NEW."epss_percentile", NEW."epss_score", NEW."false_p", NEW."file_path", NEW."fix_available", NEW."fix_version", NEW."hash_code", NEW."id", NEW."impact", NEW."is_mitigated", NEW."kev_date", NEW."known_exploited", NEW."last_reviewed", NEW."last_reviewed_by_id", NEW."last_status_update", NEW."line", NEW."mitigated", NEW."mitigated_by_id", NEW."mitigation", NEW."nb_occurences", NEW."numerical_severity", NEW."out_of_scope", NEW."param", NEW."payload", _pgh_attach_context(), NOW(), 'insert', NEW."id", NEW."planned_remediation_date", NEW."planned_remediation_version", NEW."publish_date", NEW."ransomware_used", NEW."refs", NEW."reporter_id", NEW."review_requested_by_id", NEW."risk_accepted", NEW."sast_sink_object", NEW."sast_source_file_path", NEW."sast_source_line", NEW."sast_source_object", NEW."scanner_confidence", NEW."service", NEW."severity", NEW."severity_justification", NEW."sla_expiration_date", NEW."sla_start_date", NEW."sonarqube_issue_id", NEW."static_finding", NEW."steps_to_reproduce", NEW."test_id", NEW."thread_id", NEW."title", NEW."under_defect_review", NEW."under_review", NEW."unique_id_from_tool", NEW."updated", NEW."url", NEW."verified", NEW."vuln_id_from_tool"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_insert_insert_2fbbb ON "dojo_finding"; + CREATE TRIGGER pgtrigger_insert_insert_2fbbb + AFTER INSERT ON "dojo_finding" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_insert_insert_2fbbb(); + + COMMENT ON TRIGGER pgtrigger_insert_insert_2fbbb ON "dojo_finding" IS 'b8eea714d4e92bd779ff82939552ad55346d949f'; + +-- +-- Remove trigger delete_delete from model engagement +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_delete_delete_9f4df() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_engagementevent" ("active", "api_test", "branch_tag", "build_id", "check_list", "cicd_build_server_id", "cicd_orchestration_engine_id", "cicd_scm_server_id", "commit_hash", "created", "deduplication_on_engagement", "description", "done_testing", "engagement_type", "first_contacted", "id", "lead_id", "name", "pen_test", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preset_id", "product_id", "progress", "reason", "report_type_id", "requester_id", "source_code_management_uri", "status", "target_end", "target_start", "test_strategy", "threat_model", "tmodel_path", "tracker", "updated", "version") VALUES (OLD."active", OLD."api_test", OLD."branch_tag", OLD."build_id", OLD."check_list", OLD."cicd_build_server_id", OLD."cicd_orchestration_engine_id", OLD."cicd_scm_server_id", OLD."commit_hash", OLD."created", OLD."deduplication_on_engagement", OLD."description", OLD."done_testing", OLD."engagement_type", OLD."first_contacted", OLD."id", OLD."lead_id", OLD."name", OLD."pen_test", _pgh_attach_context(), NOW(), 'delete', OLD."id", OLD."preset_id", OLD."product_id", OLD."progress", OLD."reason", OLD."report_type_id", OLD."requester_id", OLD."source_code_management_uri", OLD."status", OLD."target_end", OLD."target_start", OLD."test_strategy", OLD."threat_model", OLD."tmodel_path", OLD."tracker", OLD."updated", OLD."version"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_delete_delete_9f4df ON "dojo_engagement"; + CREATE TRIGGER pgtrigger_delete_delete_9f4df + AFTER DELETE ON "dojo_engagement" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_delete_delete_9f4df(); + + COMMENT ON TRIGGER pgtrigger_delete_delete_9f4df ON "dojo_engagement" IS 'de64abfdac94fadfbe7a8cd33212b1dc26ad9600'; + +-- +-- Remove trigger update_update from model engagement +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_update_update_65136() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_engagementevent" ("active", "api_test", "branch_tag", "build_id", "check_list", "cicd_build_server_id", "cicd_orchestration_engine_id", "cicd_scm_server_id", "commit_hash", "created", "deduplication_on_engagement", "description", "done_testing", "engagement_type", "first_contacted", "id", "lead_id", "name", "pen_test", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preset_id", "product_id", "progress", "reason", "report_type_id", "requester_id", "source_code_management_uri", "status", "target_end", "target_start", "test_strategy", "threat_model", "tmodel_path", "tracker", "updated", "version") VALUES (NEW."active", NEW."api_test", NEW."branch_tag", NEW."build_id", NEW."check_list", NEW."cicd_build_server_id", NEW."cicd_orchestration_engine_id", NEW."cicd_scm_server_id", NEW."commit_hash", NEW."created", NEW."deduplication_on_engagement", NEW."description", NEW."done_testing", NEW."engagement_type", NEW."first_contacted", NEW."id", NEW."lead_id", NEW."name", NEW."pen_test", _pgh_attach_context(), NOW(), 'update', NEW."id", NEW."preset_id", NEW."product_id", NEW."progress", NEW."reason", NEW."report_type_id", NEW."requester_id", NEW."source_code_management_uri", NEW."status", NEW."target_end", NEW."target_start", NEW."test_strategy", NEW."threat_model", NEW."tmodel_path", NEW."tracker", NEW."updated", NEW."version"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_update_update_65136 ON "dojo_engagement"; + CREATE TRIGGER pgtrigger_update_update_65136 + AFTER UPDATE ON "dojo_engagement" + + + FOR EACH ROW WHEN (OLD."active" IS DISTINCT FROM (NEW."active") OR OLD."api_test" IS DISTINCT FROM (NEW."api_test") OR OLD."branch_tag" IS DISTINCT FROM (NEW."branch_tag") OR OLD."build_id" IS DISTINCT FROM (NEW."build_id") OR OLD."check_list" IS DISTINCT FROM (NEW."check_list") OR OLD."cicd_build_server_id" IS DISTINCT FROM (NEW."cicd_build_server_id") OR OLD."cicd_orchestration_engine_id" IS DISTINCT FROM (NEW."cicd_orchestration_engine_id") OR OLD."cicd_scm_server_id" IS DISTINCT FROM (NEW."cicd_scm_server_id") OR OLD."commit_hash" IS DISTINCT FROM (NEW."commit_hash") OR OLD."deduplication_on_engagement" IS DISTINCT FROM (NEW."deduplication_on_engagement") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."done_testing" IS DISTINCT FROM (NEW."done_testing") OR OLD."engagement_type" IS DISTINCT FROM (NEW."engagement_type") OR OLD."first_contacted" IS DISTINCT FROM (NEW."first_contacted") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."lead_id" IS DISTINCT FROM (NEW."lead_id") OR OLD."name" IS DISTINCT FROM (NEW."name") OR OLD."pen_test" IS DISTINCT FROM (NEW."pen_test") OR OLD."preset_id" IS DISTINCT FROM (NEW."preset_id") OR OLD."product_id" IS DISTINCT FROM (NEW."product_id") OR OLD."progress" IS DISTINCT FROM (NEW."progress") OR OLD."reason" IS DISTINCT FROM (NEW."reason") OR OLD."report_type_id" IS DISTINCT FROM (NEW."report_type_id") OR OLD."requester_id" IS DISTINCT FROM (NEW."requester_id") OR OLD."source_code_management_uri" IS DISTINCT FROM (NEW."source_code_management_uri") OR OLD."status" IS DISTINCT FROM (NEW."status") OR OLD."target_end" IS DISTINCT FROM (NEW."target_end") OR OLD."target_start" IS DISTINCT FROM (NEW."target_start") OR OLD."test_strategy" IS DISTINCT FROM (NEW."test_strategy") OR OLD."threat_model" IS DISTINCT FROM (NEW."threat_model") OR OLD."tmodel_path" IS DISTINCT FROM (NEW."tmodel_path") OR OLD."tracker" IS DISTINCT FROM (NEW."tracker") OR OLD."version" IS DISTINCT FROM (NEW."version")) + EXECUTE PROCEDURE pgtrigger_update_update_65136(); + + COMMENT ON TRIGGER pgtrigger_update_update_65136 ON "dojo_engagement" IS '6a9569fa21d5d7ad16eb018bc4e6236e8401bced'; + +-- +-- Remove trigger insert_insert from model engagement +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_insert_insert_125f1() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_engagementevent" ("active", "api_test", "branch_tag", "build_id", "check_list", "cicd_build_server_id", "cicd_orchestration_engine_id", "cicd_scm_server_id", "commit_hash", "created", "deduplication_on_engagement", "description", "done_testing", "engagement_type", "first_contacted", "id", "lead_id", "name", "pen_test", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preset_id", "product_id", "progress", "reason", "report_type_id", "requester_id", "source_code_management_uri", "status", "target_end", "target_start", "test_strategy", "threat_model", "tmodel_path", "tracker", "updated", "version") VALUES (NEW."active", NEW."api_test", NEW."branch_tag", NEW."build_id", NEW."check_list", NEW."cicd_build_server_id", NEW."cicd_orchestration_engine_id", NEW."cicd_scm_server_id", NEW."commit_hash", NEW."created", NEW."deduplication_on_engagement", NEW."description", NEW."done_testing", NEW."engagement_type", NEW."first_contacted", NEW."id", NEW."lead_id", NEW."name", NEW."pen_test", _pgh_attach_context(), NOW(), 'insert', NEW."id", NEW."preset_id", NEW."product_id", NEW."progress", NEW."reason", NEW."report_type_id", NEW."requester_id", NEW."source_code_management_uri", NEW."status", NEW."target_end", NEW."target_start", NEW."test_strategy", NEW."threat_model", NEW."tmodel_path", NEW."tracker", NEW."updated", NEW."version"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_insert_insert_125f1 ON "dojo_engagement"; + CREATE TRIGGER pgtrigger_insert_insert_125f1 + AFTER INSERT ON "dojo_engagement" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_insert_insert_125f1(); + + COMMENT ON TRIGGER pgtrigger_insert_insert_125f1 ON "dojo_engagement" IS 'a217ec77b975020749afc350ee463c5867cfea27'; + +-- +-- Remove trigger delete_delete from model endpoint +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_delete_delete_dd1f9() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_endpointevent" ("fragment", "host", "id", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "port", "product_id", "protocol", "query", "userinfo") VALUES (OLD."fragment", OLD."host", OLD."id", OLD."path", _pgh_attach_context(), NOW(), 'delete', OLD."id", OLD."port", OLD."product_id", OLD."protocol", OLD."query", OLD."userinfo"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_delete_delete_dd1f9 ON "dojo_endpoint"; + CREATE TRIGGER pgtrigger_delete_delete_dd1f9 + AFTER DELETE ON "dojo_endpoint" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_delete_delete_dd1f9(); + + COMMENT ON TRIGGER pgtrigger_delete_delete_dd1f9 ON "dojo_endpoint" IS '5e88416165d3e62cc6717b7d7f5d82933929de32'; + +-- +-- Remove trigger update_update from model endpoint +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_update_update_2b19a() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_endpointevent" ("fragment", "host", "id", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "port", "product_id", "protocol", "query", "userinfo") VALUES (NEW."fragment", NEW."host", NEW."id", NEW."path", _pgh_attach_context(), NOW(), 'update', NEW."id", NEW."port", NEW."product_id", NEW."protocol", NEW."query", NEW."userinfo"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_update_update_2b19a ON "dojo_endpoint"; + CREATE TRIGGER pgtrigger_update_update_2b19a + AFTER UPDATE ON "dojo_endpoint" + + + FOR EACH ROW WHEN (OLD.* IS DISTINCT FROM NEW.*) + EXECUTE PROCEDURE pgtrigger_update_update_2b19a(); + + COMMENT ON TRIGGER pgtrigger_update_update_2b19a ON "dojo_endpoint" IS 'fce28bcbd64850a383950a720a72f134da7989d9'; + +-- +-- Remove trigger insert_insert from model endpoint +-- + + CREATE OR REPLACE FUNCTION "public"._pgtrigger_should_ignore( + trigger_name NAME + ) + RETURNS BOOLEAN AS $$ + DECLARE + _pgtrigger_ignore TEXT[]; + _result BOOLEAN; + BEGIN + BEGIN + SELECT INTO _pgtrigger_ignore + CURRENT_SETTING('pgtrigger.ignore'); + EXCEPTION WHEN OTHERS THEN + END; + IF _pgtrigger_ignore IS NOT NULL THEN + SELECT trigger_name = ANY(_pgtrigger_ignore) + INTO _result; + RETURN _result; + ELSE + RETURN FALSE; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION pgtrigger_insert_insert_c7973() + RETURNS TRIGGER AS $$ + + BEGIN + IF ("public"._pgtrigger_should_ignore(TG_NAME) IS TRUE) THEN + IF (TG_OP = 'DELETE') THEN + RETURN OLD; + ELSE + RETURN NEW; + END IF; + END IF; + INSERT INTO "dojo_endpointevent" ("fragment", "host", "id", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "port", "product_id", "protocol", "query", "userinfo") VALUES (NEW."fragment", NEW."host", NEW."id", NEW."path", _pgh_attach_context(), NOW(), 'insert', NEW."id", NEW."port", NEW."product_id", NEW."protocol", NEW."query", NEW."userinfo"); RETURN NULL; + END; + $$ LANGUAGE plpgsql; + + DROP TRIGGER IF EXISTS pgtrigger_insert_insert_c7973 ON "dojo_endpoint"; + CREATE TRIGGER pgtrigger_insert_insert_c7973 + AFTER INSERT ON "dojo_endpoint" + + + FOR EACH ROW + EXECUTE PROCEDURE pgtrigger_insert_insert_c7973(); + + COMMENT ON TRIGGER pgtrigger_insert_insert_c7973 ON "dojo_endpoint" IS '68e589c514e437831f4da3e802ca18e4cced175d'; +""" + + +class Migration(migrations.Migration): + + """ + Archive columns on the hierarchy and its child rows, with their audit triggers. + + The operations are applied to the migration state only; the database work is the + frozen SQL they generate, run as one statement batch at the end. Django rebuilds its + model state after every operation, which takes most of a minute for these, and in a + plain migration each table's ACCESS EXCLUSIVE lock is taken by its ALTER and held + through all the rebuilding that follows. On 10 million findings that blocked every + read of dojo_finding for about 40 seconds. Done this way the locks are held only for + the SQL itself. + """ + + dependencies = [ + ("dojo", "0303_system_settings_install_id"), + ] + + operations = [ + migrations.SeparateDatabaseAndState( + state_operations=[ + pgtrigger.migrations.RemoveTrigger( + model_name="endpoint", + name="insert_insert", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="endpoint", + name="update_update", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="endpoint", + name="delete_delete", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="engagement", + name="insert_insert", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="engagement", + name="update_update", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="engagement", + name="delete_delete", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="finding", + name="insert_insert", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="finding", + name="update_update", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="finding", + name="delete_delete", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="finding_group", + name="insert_insert", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="finding_group", + name="update_update", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="finding_group", + name="delete_delete", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="product", + name="insert_insert", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="product", + name="update_update", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="product", + name="delete_delete", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="product_type", + name="insert_insert", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="product_type", + name="update_update", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="product_type", + name="delete_delete", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="test", + name="insert_insert", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="test", + name="update_update", + ), + pgtrigger.migrations.RemoveTrigger( + model_name="test", + name="delete_delete", + ), + migrations.AddField( + model_name="app_analysis", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="app_analysis", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="dojometa", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="dojometa", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="endpoint", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="endpoint", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="endpoint_status", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="endpoint_status", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="endpointevent", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="endpointevent", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="engagement", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="engagement", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="engagement_presets", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="engagement_presets", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="engagementevent", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="engagementevent", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="finding", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="finding", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="finding_group", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="finding_group", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="finding_groupevent", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="finding_groupevent", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="findingevent", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="findingevent", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="findingvulnerabilityreference", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="findingvulnerabilityreference", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="jira_issue", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="jira_issue", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="jira_project", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="jira_project", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="languages", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="languages", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="locationfindingreference", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="locationfindingreference", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="locationproductreference", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="locationproductreference", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="objects_product", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="objects_product", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="product", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="product", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="product_api_scan_configuration", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="product_api_scan_configuration", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="product_type", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="product_type", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="product_typeevent", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="product_typeevent", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="productevent", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="productevent", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="test", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="test", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="test_import", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="test_import", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="test_import_finding_action", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="test_import_finding_action", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="testevent", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="testevent", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="tool_product_settings", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="tool_product_settings", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="vulnerability_id", + name="archive_batch", + field=models.UUIDField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name="vulnerability_id", + name="archived_at", + field=models.DateTimeField(blank=True, editable=False, null=True), + ), + pgtrigger.migrations.AddTrigger( + model_name="endpoint", + trigger=pgtrigger.compiler.Trigger(name="insert_insert", sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_endpointevent" ("archive_batch", "archived_at", "fragment", "host", "id", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "port", "product_id", "protocol", "query", "userinfo") VALUES (NEW."archive_batch", NEW."archived_at", NEW."fragment", NEW."host", NEW."id", NEW."path", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."port", NEW."product_id", NEW."protocol", NEW."query", NEW."userinfo"); RETURN NULL;', hash="4aa86c99a53e2afc8a2e7bbc9dce7e87cfe93c99", operation="INSERT", pgid="pgtrigger_insert_insert_c7973", table="dojo_endpoint", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="endpoint", + trigger=pgtrigger.compiler.Trigger(name="update_update", sql=pgtrigger.compiler.UpsertTriggerSql(condition="WHEN (OLD.* IS DISTINCT FROM NEW.*)", func='INSERT INTO "dojo_endpointevent" ("archive_batch", "archived_at", "fragment", "host", "id", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "port", "product_id", "protocol", "query", "userinfo") VALUES (NEW."archive_batch", NEW."archived_at", NEW."fragment", NEW."host", NEW."id", NEW."path", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."port", NEW."product_id", NEW."protocol", NEW."query", NEW."userinfo"); RETURN NULL;', hash="187a4d67775847794459c2d4249fe13e333b2118", operation="UPDATE", pgid="pgtrigger_update_update_2b19a", table="dojo_endpoint", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="endpoint", + trigger=pgtrigger.compiler.Trigger(name="delete_delete", sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_endpointevent" ("archive_batch", "archived_at", "fragment", "host", "id", "path", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "port", "product_id", "protocol", "query", "userinfo") VALUES (OLD."archive_batch", OLD."archived_at", OLD."fragment", OLD."host", OLD."id", OLD."path", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."port", OLD."product_id", OLD."protocol", OLD."query", OLD."userinfo"); RETURN NULL;', hash="c30fc33fac55999df702b7c9c249f64513bf7497", operation="DELETE", pgid="pgtrigger_delete_delete_dd1f9", table="dojo_endpoint", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="engagement", + trigger=pgtrigger.compiler.Trigger(name="insert_insert", sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_engagementevent" ("active", "api_test", "archive_batch", "archived_at", "branch_tag", "build_id", "check_list", "cicd_build_server_id", "cicd_orchestration_engine_id", "cicd_scm_server_id", "commit_hash", "created", "deduplication_on_engagement", "description", "done_testing", "engagement_type", "first_contacted", "id", "lead_id", "name", "pen_test", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preset_id", "product_id", "progress", "reason", "report_type_id", "requester_id", "source_code_management_uri", "status", "target_end", "target_start", "test_strategy", "threat_model", "tmodel_path", "tracker", "updated", "version") VALUES (NEW."active", NEW."api_test", NEW."archive_batch", NEW."archived_at", NEW."branch_tag", NEW."build_id", NEW."check_list", NEW."cicd_build_server_id", NEW."cicd_orchestration_engine_id", NEW."cicd_scm_server_id", NEW."commit_hash", NEW."created", NEW."deduplication_on_engagement", NEW."description", NEW."done_testing", NEW."engagement_type", NEW."first_contacted", NEW."id", NEW."lead_id", NEW."name", NEW."pen_test", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."preset_id", NEW."product_id", NEW."progress", NEW."reason", NEW."report_type_id", NEW."requester_id", NEW."source_code_management_uri", NEW."status", NEW."target_end", NEW."target_start", NEW."test_strategy", NEW."threat_model", NEW."tmodel_path", NEW."tracker", NEW."updated", NEW."version"); RETURN NULL;', hash="5b48232cfe7dc67a91eebfd023a4ccb6eb920c39", operation="INSERT", pgid="pgtrigger_insert_insert_125f1", table="dojo_engagement", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="engagement", + trigger=pgtrigger.compiler.Trigger(name="update_update", sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD."active" IS DISTINCT FROM (NEW."active") OR OLD."api_test" IS DISTINCT FROM (NEW."api_test") OR OLD."archive_batch" IS DISTINCT FROM (NEW."archive_batch") OR OLD."archived_at" IS DISTINCT FROM (NEW."archived_at") OR OLD."branch_tag" IS DISTINCT FROM (NEW."branch_tag") OR OLD."build_id" IS DISTINCT FROM (NEW."build_id") OR OLD."check_list" IS DISTINCT FROM (NEW."check_list") OR OLD."cicd_build_server_id" IS DISTINCT FROM (NEW."cicd_build_server_id") OR OLD."cicd_orchestration_engine_id" IS DISTINCT FROM (NEW."cicd_orchestration_engine_id") OR OLD."cicd_scm_server_id" IS DISTINCT FROM (NEW."cicd_scm_server_id") OR OLD."commit_hash" IS DISTINCT FROM (NEW."commit_hash") OR OLD."deduplication_on_engagement" IS DISTINCT FROM (NEW."deduplication_on_engagement") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."done_testing" IS DISTINCT FROM (NEW."done_testing") OR OLD."engagement_type" IS DISTINCT FROM (NEW."engagement_type") OR OLD."first_contacted" IS DISTINCT FROM (NEW."first_contacted") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."lead_id" IS DISTINCT FROM (NEW."lead_id") OR OLD."name" IS DISTINCT FROM (NEW."name") OR OLD."pen_test" IS DISTINCT FROM (NEW."pen_test") OR OLD."preset_id" IS DISTINCT FROM (NEW."preset_id") OR OLD."product_id" IS DISTINCT FROM (NEW."product_id") OR OLD."progress" IS DISTINCT FROM (NEW."progress") OR OLD."reason" IS DISTINCT FROM (NEW."reason") OR OLD."report_type_id" IS DISTINCT FROM (NEW."report_type_id") OR OLD."requester_id" IS DISTINCT FROM (NEW."requester_id") OR OLD."source_code_management_uri" IS DISTINCT FROM (NEW."source_code_management_uri") OR OLD."status" IS DISTINCT FROM (NEW."status") OR OLD."target_end" IS DISTINCT FROM (NEW."target_end") OR OLD."target_start" IS DISTINCT FROM (NEW."target_start") OR OLD."test_strategy" IS DISTINCT FROM (NEW."test_strategy") OR OLD."threat_model" IS DISTINCT FROM (NEW."threat_model") OR OLD."tmodel_path" IS DISTINCT FROM (NEW."tmodel_path") OR OLD."tracker" IS DISTINCT FROM (NEW."tracker") OR OLD."version" IS DISTINCT FROM (NEW."version"))', func='INSERT INTO "dojo_engagementevent" ("active", "api_test", "archive_batch", "archived_at", "branch_tag", "build_id", "check_list", "cicd_build_server_id", "cicd_orchestration_engine_id", "cicd_scm_server_id", "commit_hash", "created", "deduplication_on_engagement", "description", "done_testing", "engagement_type", "first_contacted", "id", "lead_id", "name", "pen_test", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preset_id", "product_id", "progress", "reason", "report_type_id", "requester_id", "source_code_management_uri", "status", "target_end", "target_start", "test_strategy", "threat_model", "tmodel_path", "tracker", "updated", "version") VALUES (NEW."active", NEW."api_test", NEW."archive_batch", NEW."archived_at", NEW."branch_tag", NEW."build_id", NEW."check_list", NEW."cicd_build_server_id", NEW."cicd_orchestration_engine_id", NEW."cicd_scm_server_id", NEW."commit_hash", NEW."created", NEW."deduplication_on_engagement", NEW."description", NEW."done_testing", NEW."engagement_type", NEW."first_contacted", NEW."id", NEW."lead_id", NEW."name", NEW."pen_test", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."preset_id", NEW."product_id", NEW."progress", NEW."reason", NEW."report_type_id", NEW."requester_id", NEW."source_code_management_uri", NEW."status", NEW."target_end", NEW."target_start", NEW."test_strategy", NEW."threat_model", NEW."tmodel_path", NEW."tracker", NEW."updated", NEW."version"); RETURN NULL;', hash="8990b78ab9370e3cd1ec2b85dd89d52834d5aaf6", operation="UPDATE", pgid="pgtrigger_update_update_65136", table="dojo_engagement", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="engagement", + trigger=pgtrigger.compiler.Trigger(name="delete_delete", sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_engagementevent" ("active", "api_test", "archive_batch", "archived_at", "branch_tag", "build_id", "check_list", "cicd_build_server_id", "cicd_orchestration_engine_id", "cicd_scm_server_id", "commit_hash", "created", "deduplication_on_engagement", "description", "done_testing", "engagement_type", "first_contacted", "id", "lead_id", "name", "pen_test", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "preset_id", "product_id", "progress", "reason", "report_type_id", "requester_id", "source_code_management_uri", "status", "target_end", "target_start", "test_strategy", "threat_model", "tmodel_path", "tracker", "updated", "version") VALUES (OLD."active", OLD."api_test", OLD."archive_batch", OLD."archived_at", OLD."branch_tag", OLD."build_id", OLD."check_list", OLD."cicd_build_server_id", OLD."cicd_orchestration_engine_id", OLD."cicd_scm_server_id", OLD."commit_hash", OLD."created", OLD."deduplication_on_engagement", OLD."description", OLD."done_testing", OLD."engagement_type", OLD."first_contacted", OLD."id", OLD."lead_id", OLD."name", OLD."pen_test", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."preset_id", OLD."product_id", OLD."progress", OLD."reason", OLD."report_type_id", OLD."requester_id", OLD."source_code_management_uri", OLD."status", OLD."target_end", OLD."target_start", OLD."test_strategy", OLD."threat_model", OLD."tmodel_path", OLD."tracker", OLD."updated", OLD."version"); RETURN NULL;', hash="0de881cdff9409808f24eb0258bdd61ae9ddc60e", operation="DELETE", pgid="pgtrigger_delete_delete_9f4df", table="dojo_engagement", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="finding", + trigger=pgtrigger.compiler.Trigger(name="insert_insert", sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_findingevent" ("active", "archive_batch", "archived_at", "component_name", "component_version", "created", "cve", "cvssv3", "cvssv3_score", "cvssv4", "cvssv4_score", "cwe", "date", "defect_review_requested_by_id", "description", "duplicate", "duplicate_finding_id", "dynamic_finding", "effort_for_fixing", "epss_percentile", "epss_score", "false_p", "file_path", "fix_available", "fix_version", "hash_code", "id", "impact", "is_mitigated", "kev_date", "known_exploited", "last_reviewed", "last_reviewed_by_id", "last_status_update", "line", "mitigated", "mitigated_by_id", "mitigation", "nb_occurences", "numerical_severity", "out_of_scope", "param", "payload", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "planned_remediation_date", "planned_remediation_version", "publish_date", "ransomware_used", "refs", "reporter_id", "review_requested_by_id", "risk_accepted", "sast_sink_object", "sast_source_file_path", "sast_source_line", "sast_source_object", "scanner_confidence", "service", "severity", "severity_justification", "sla_expiration_date", "sla_start_date", "sonarqube_issue_id", "static_finding", "steps_to_reproduce", "test_id", "thread_id", "title", "under_defect_review", "under_review", "unique_id_from_tool", "updated", "url", "verified", "vuln_id_from_tool") VALUES (NEW."active", NEW."archive_batch", NEW."archived_at", NEW."component_name", NEW."component_version", NEW."created", NEW."cve", NEW."cvssv3", NEW."cvssv3_score", NEW."cvssv4", NEW."cvssv4_score", NEW."cwe", NEW."date", NEW."defect_review_requested_by_id", NEW."description", NEW."duplicate", NEW."duplicate_finding_id", NEW."dynamic_finding", NEW."effort_for_fixing", NEW."epss_percentile", NEW."epss_score", NEW."false_p", NEW."file_path", NEW."fix_available", NEW."fix_version", NEW."hash_code", NEW."id", NEW."impact", NEW."is_mitigated", NEW."kev_date", NEW."known_exploited", NEW."last_reviewed", NEW."last_reviewed_by_id", NEW."last_status_update", NEW."line", NEW."mitigated", NEW."mitigated_by_id", NEW."mitigation", NEW."nb_occurences", NEW."numerical_severity", NEW."out_of_scope", NEW."param", NEW."payload", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."planned_remediation_date", NEW."planned_remediation_version", NEW."publish_date", NEW."ransomware_used", NEW."refs", NEW."reporter_id", NEW."review_requested_by_id", NEW."risk_accepted", NEW."sast_sink_object", NEW."sast_source_file_path", NEW."sast_source_line", NEW."sast_source_object", NEW."scanner_confidence", NEW."service", NEW."severity", NEW."severity_justification", NEW."sla_expiration_date", NEW."sla_start_date", NEW."sonarqube_issue_id", NEW."static_finding", NEW."steps_to_reproduce", NEW."test_id", NEW."thread_id", NEW."title", NEW."under_defect_review", NEW."under_review", NEW."unique_id_from_tool", NEW."updated", NEW."url", NEW."verified", NEW."vuln_id_from_tool"); RETURN NULL;', hash="bfca827e3bf9d38085346f34984b62bfc9efa036", operation="INSERT", pgid="pgtrigger_insert_insert_2fbbb", table="dojo_finding", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="finding", + trigger=pgtrigger.compiler.Trigger(name="update_update", sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD."active" IS DISTINCT FROM (NEW."active") OR OLD."archive_batch" IS DISTINCT FROM (NEW."archive_batch") OR OLD."archived_at" IS DISTINCT FROM (NEW."archived_at") OR OLD."component_name" IS DISTINCT FROM (NEW."component_name") OR OLD."component_version" IS DISTINCT FROM (NEW."component_version") OR OLD."cve" IS DISTINCT FROM (NEW."cve") OR OLD."cvssv3" IS DISTINCT FROM (NEW."cvssv3") OR OLD."cvssv3_score" IS DISTINCT FROM (NEW."cvssv3_score") OR OLD."cvssv4" IS DISTINCT FROM (NEW."cvssv4") OR OLD."cvssv4_score" IS DISTINCT FROM (NEW."cvssv4_score") OR OLD."cwe" IS DISTINCT FROM (NEW."cwe") OR OLD."date" IS DISTINCT FROM (NEW."date") OR OLD."defect_review_requested_by_id" IS DISTINCT FROM (NEW."defect_review_requested_by_id") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."duplicate" IS DISTINCT FROM (NEW."duplicate") OR OLD."duplicate_finding_id" IS DISTINCT FROM (NEW."duplicate_finding_id") OR OLD."dynamic_finding" IS DISTINCT FROM (NEW."dynamic_finding") OR OLD."effort_for_fixing" IS DISTINCT FROM (NEW."effort_for_fixing") OR OLD."epss_percentile" IS DISTINCT FROM (NEW."epss_percentile") OR OLD."epss_score" IS DISTINCT FROM (NEW."epss_score") OR OLD."false_p" IS DISTINCT FROM (NEW."false_p") OR OLD."file_path" IS DISTINCT FROM (NEW."file_path") OR OLD."fix_available" IS DISTINCT FROM (NEW."fix_available") OR OLD."fix_version" IS DISTINCT FROM (NEW."fix_version") OR OLD."hash_code" IS DISTINCT FROM (NEW."hash_code") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."impact" IS DISTINCT FROM (NEW."impact") OR OLD."is_mitigated" IS DISTINCT FROM (NEW."is_mitigated") OR OLD."kev_date" IS DISTINCT FROM (NEW."kev_date") OR OLD."known_exploited" IS DISTINCT FROM (NEW."known_exploited") OR OLD."last_reviewed" IS DISTINCT FROM (NEW."last_reviewed") OR OLD."last_reviewed_by_id" IS DISTINCT FROM (NEW."last_reviewed_by_id") OR OLD."line" IS DISTINCT FROM (NEW."line") OR OLD."mitigated" IS DISTINCT FROM (NEW."mitigated") OR OLD."mitigated_by_id" IS DISTINCT FROM (NEW."mitigated_by_id") OR OLD."mitigation" IS DISTINCT FROM (NEW."mitigation") OR OLD."nb_occurences" IS DISTINCT FROM (NEW."nb_occurences") OR OLD."numerical_severity" IS DISTINCT FROM (NEW."numerical_severity") OR OLD."out_of_scope" IS DISTINCT FROM (NEW."out_of_scope") OR OLD."param" IS DISTINCT FROM (NEW."param") OR OLD."payload" IS DISTINCT FROM (NEW."payload") OR OLD."planned_remediation_date" IS DISTINCT FROM (NEW."planned_remediation_date") OR OLD."planned_remediation_version" IS DISTINCT FROM (NEW."planned_remediation_version") OR OLD."publish_date" IS DISTINCT FROM (NEW."publish_date") OR OLD."ransomware_used" IS DISTINCT FROM (NEW."ransomware_used") OR OLD."refs" IS DISTINCT FROM (NEW."refs") OR OLD."reporter_id" IS DISTINCT FROM (NEW."reporter_id") OR OLD."review_requested_by_id" IS DISTINCT FROM (NEW."review_requested_by_id") OR OLD."risk_accepted" IS DISTINCT FROM (NEW."risk_accepted") OR OLD."sast_sink_object" IS DISTINCT FROM (NEW."sast_sink_object") OR OLD."sast_source_file_path" IS DISTINCT FROM (NEW."sast_source_file_path") OR OLD."sast_source_line" IS DISTINCT FROM (NEW."sast_source_line") OR OLD."sast_source_object" IS DISTINCT FROM (NEW."sast_source_object") OR OLD."scanner_confidence" IS DISTINCT FROM (NEW."scanner_confidence") OR OLD."service" IS DISTINCT FROM (NEW."service") OR OLD."severity" IS DISTINCT FROM (NEW."severity") OR OLD."severity_justification" IS DISTINCT FROM (NEW."severity_justification") OR OLD."sla_expiration_date" IS DISTINCT FROM (NEW."sla_expiration_date") OR OLD."sla_start_date" IS DISTINCT FROM (NEW."sla_start_date") OR OLD."sonarqube_issue_id" IS DISTINCT FROM (NEW."sonarqube_issue_id") OR OLD."static_finding" IS DISTINCT FROM (NEW."static_finding") OR OLD."steps_to_reproduce" IS DISTINCT FROM (NEW."steps_to_reproduce") OR OLD."test_id" IS DISTINCT FROM (NEW."test_id") OR OLD."thread_id" IS DISTINCT FROM (NEW."thread_id") OR OLD."title" IS DISTINCT FROM (NEW."title") OR OLD."under_defect_review" IS DISTINCT FROM (NEW."under_defect_review") OR OLD."under_review" IS DISTINCT FROM (NEW."under_review") OR OLD."unique_id_from_tool" IS DISTINCT FROM (NEW."unique_id_from_tool") OR OLD."url" IS DISTINCT FROM (NEW."url") OR OLD."verified" IS DISTINCT FROM (NEW."verified") OR OLD."vuln_id_from_tool" IS DISTINCT FROM (NEW."vuln_id_from_tool"))', func='INSERT INTO "dojo_findingevent" ("active", "archive_batch", "archived_at", "component_name", "component_version", "created", "cve", "cvssv3", "cvssv3_score", "cvssv4", "cvssv4_score", "cwe", "date", "defect_review_requested_by_id", "description", "duplicate", "duplicate_finding_id", "dynamic_finding", "effort_for_fixing", "epss_percentile", "epss_score", "false_p", "file_path", "fix_available", "fix_version", "hash_code", "id", "impact", "is_mitigated", "kev_date", "known_exploited", "last_reviewed", "last_reviewed_by_id", "last_status_update", "line", "mitigated", "mitigated_by_id", "mitigation", "nb_occurences", "numerical_severity", "out_of_scope", "param", "payload", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "planned_remediation_date", "planned_remediation_version", "publish_date", "ransomware_used", "refs", "reporter_id", "review_requested_by_id", "risk_accepted", "sast_sink_object", "sast_source_file_path", "sast_source_line", "sast_source_object", "scanner_confidence", "service", "severity", "severity_justification", "sla_expiration_date", "sla_start_date", "sonarqube_issue_id", "static_finding", "steps_to_reproduce", "test_id", "thread_id", "title", "under_defect_review", "under_review", "unique_id_from_tool", "updated", "url", "verified", "vuln_id_from_tool") VALUES (NEW."active", NEW."archive_batch", NEW."archived_at", NEW."component_name", NEW."component_version", NEW."created", NEW."cve", NEW."cvssv3", NEW."cvssv3_score", NEW."cvssv4", NEW."cvssv4_score", NEW."cwe", NEW."date", NEW."defect_review_requested_by_id", NEW."description", NEW."duplicate", NEW."duplicate_finding_id", NEW."dynamic_finding", NEW."effort_for_fixing", NEW."epss_percentile", NEW."epss_score", NEW."false_p", NEW."file_path", NEW."fix_available", NEW."fix_version", NEW."hash_code", NEW."id", NEW."impact", NEW."is_mitigated", NEW."kev_date", NEW."known_exploited", NEW."last_reviewed", NEW."last_reviewed_by_id", NEW."last_status_update", NEW."line", NEW."mitigated", NEW."mitigated_by_id", NEW."mitigation", NEW."nb_occurences", NEW."numerical_severity", NEW."out_of_scope", NEW."param", NEW."payload", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."planned_remediation_date", NEW."planned_remediation_version", NEW."publish_date", NEW."ransomware_used", NEW."refs", NEW."reporter_id", NEW."review_requested_by_id", NEW."risk_accepted", NEW."sast_sink_object", NEW."sast_source_file_path", NEW."sast_source_line", NEW."sast_source_object", NEW."scanner_confidence", NEW."service", NEW."severity", NEW."severity_justification", NEW."sla_expiration_date", NEW."sla_start_date", NEW."sonarqube_issue_id", NEW."static_finding", NEW."steps_to_reproduce", NEW."test_id", NEW."thread_id", NEW."title", NEW."under_defect_review", NEW."under_review", NEW."unique_id_from_tool", NEW."updated", NEW."url", NEW."verified", NEW."vuln_id_from_tool"); RETURN NULL;', hash="7b1c5da1fc67ea4d10b0d6e9ec42fbf428ad6d47", operation="UPDATE", pgid="pgtrigger_update_update_92175", table="dojo_finding", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="finding", + trigger=pgtrigger.compiler.Trigger(name="delete_delete", sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_findingevent" ("active", "archive_batch", "archived_at", "component_name", "component_version", "created", "cve", "cvssv3", "cvssv3_score", "cvssv4", "cvssv4_score", "cwe", "date", "defect_review_requested_by_id", "description", "duplicate", "duplicate_finding_id", "dynamic_finding", "effort_for_fixing", "epss_percentile", "epss_score", "false_p", "file_path", "fix_available", "fix_version", "hash_code", "id", "impact", "is_mitigated", "kev_date", "known_exploited", "last_reviewed", "last_reviewed_by_id", "last_status_update", "line", "mitigated", "mitigated_by_id", "mitigation", "nb_occurences", "numerical_severity", "out_of_scope", "param", "payload", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "planned_remediation_date", "planned_remediation_version", "publish_date", "ransomware_used", "refs", "reporter_id", "review_requested_by_id", "risk_accepted", "sast_sink_object", "sast_source_file_path", "sast_source_line", "sast_source_object", "scanner_confidence", "service", "severity", "severity_justification", "sla_expiration_date", "sla_start_date", "sonarqube_issue_id", "static_finding", "steps_to_reproduce", "test_id", "thread_id", "title", "under_defect_review", "under_review", "unique_id_from_tool", "updated", "url", "verified", "vuln_id_from_tool") VALUES (OLD."active", OLD."archive_batch", OLD."archived_at", OLD."component_name", OLD."component_version", OLD."created", OLD."cve", OLD."cvssv3", OLD."cvssv3_score", OLD."cvssv4", OLD."cvssv4_score", OLD."cwe", OLD."date", OLD."defect_review_requested_by_id", OLD."description", OLD."duplicate", OLD."duplicate_finding_id", OLD."dynamic_finding", OLD."effort_for_fixing", OLD."epss_percentile", OLD."epss_score", OLD."false_p", OLD."file_path", OLD."fix_available", OLD."fix_version", OLD."hash_code", OLD."id", OLD."impact", OLD."is_mitigated", OLD."kev_date", OLD."known_exploited", OLD."last_reviewed", OLD."last_reviewed_by_id", OLD."last_status_update", OLD."line", OLD."mitigated", OLD."mitigated_by_id", OLD."mitigation", OLD."nb_occurences", OLD."numerical_severity", OLD."out_of_scope", OLD."param", OLD."payload", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."planned_remediation_date", OLD."planned_remediation_version", OLD."publish_date", OLD."ransomware_used", OLD."refs", OLD."reporter_id", OLD."review_requested_by_id", OLD."risk_accepted", OLD."sast_sink_object", OLD."sast_source_file_path", OLD."sast_source_line", OLD."sast_source_object", OLD."scanner_confidence", OLD."service", OLD."severity", OLD."severity_justification", OLD."sla_expiration_date", OLD."sla_start_date", OLD."sonarqube_issue_id", OLD."static_finding", OLD."steps_to_reproduce", OLD."test_id", OLD."thread_id", OLD."title", OLD."under_defect_review", OLD."under_review", OLD."unique_id_from_tool", OLD."updated", OLD."url", OLD."verified", OLD."vuln_id_from_tool"); RETURN NULL;', hash="0b0ae774839868caab7908236804d2b14393a699", operation="DELETE", pgid="pgtrigger_delete_delete_72933", table="dojo_finding", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="finding_group", + trigger=pgtrigger.compiler.Trigger(name="insert_insert", sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_finding_groupevent" ("archive_batch", "archived_at", "created", "creator_id", "id", "modified", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "test_id") VALUES (NEW."archive_batch", NEW."archived_at", NEW."created", NEW."creator_id", NEW."id", NEW."modified", NEW."name", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."test_id"); RETURN NULL;', hash="bcad4bd5b2d4c90eede0665d18169e85f5f14257", operation="INSERT", pgid="pgtrigger_insert_insert_94bd2", table="dojo_finding_group", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="finding_group", + trigger=pgtrigger.compiler.Trigger(name="update_update", sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD."archive_batch" IS DISTINCT FROM (NEW."archive_batch") OR OLD."archived_at" IS DISTINCT FROM (NEW."archived_at") OR OLD."creator_id" IS DISTINCT FROM (NEW."creator_id") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."name" IS DISTINCT FROM (NEW."name") OR OLD."test_id" IS DISTINCT FROM (NEW."test_id"))', func='INSERT INTO "dojo_finding_groupevent" ("archive_batch", "archived_at", "created", "creator_id", "id", "modified", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "test_id") VALUES (NEW."archive_batch", NEW."archived_at", NEW."created", NEW."creator_id", NEW."id", NEW."modified", NEW."name", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."test_id"); RETURN NULL;', hash="bc709885f7ae1a3e00bb01e453bfd461ea3ac298", operation="UPDATE", pgid="pgtrigger_update_update_37b5f", table="dojo_finding_group", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="finding_group", + trigger=pgtrigger.compiler.Trigger(name="delete_delete", sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_finding_groupevent" ("archive_batch", "archived_at", "created", "creator_id", "id", "modified", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "test_id") VALUES (OLD."archive_batch", OLD."archived_at", OLD."created", OLD."creator_id", OLD."id", OLD."modified", OLD."name", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."test_id"); RETURN NULL;', hash="e573c51b932f142bef508cc995f8f4e6657c1d62", operation="DELETE", pgid="pgtrigger_delete_delete_24b41", table="dojo_finding_group", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="product", + trigger=pgtrigger.compiler.Trigger(name="insert_insert", sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_productevent" ("archive_batch", "archived_at", "async_updating", "business_criticality", "created", "description", "disable_sla_breach_notifications", "enable_full_risk_acceptance", "enable_product_tag_inheritance", "enable_simple_risk_acceptance", "external_audience", "id", "internet_accessible", "lifecycle_id", "name", "origin_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "platform_id", "prod_numeric_grade", "prod_type_id", "product_manager_id", "revenue", "sla_configuration_id", "team_manager_id", "technical_contact_id", "tid", "updated", "user_records") VALUES (NEW."archive_batch", NEW."archived_at", NEW."async_updating", NEW."business_criticality", NEW."created", NEW."description", NEW."disable_sla_breach_notifications", NEW."enable_full_risk_acceptance", NEW."enable_product_tag_inheritance", NEW."enable_simple_risk_acceptance", NEW."external_audience", NEW."id", NEW."internet_accessible", NEW."lifecycle_id", NEW."name", NEW."origin_id", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."platform_id", NEW."prod_numeric_grade", NEW."prod_type_id", NEW."product_manager_id", NEW."revenue", NEW."sla_configuration_id", NEW."team_manager_id", NEW."technical_contact_id", NEW."tid", NEW."updated", NEW."user_records"); RETURN NULL;', hash="b5851c1970f7b7c91e09d80dc365cbe2579c1220", operation="INSERT", pgid="pgtrigger_insert_insert_d5d32", table="dojo_product", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="product", + trigger=pgtrigger.compiler.Trigger(name="update_update", sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD."archive_batch" IS DISTINCT FROM (NEW."archive_batch") OR OLD."archived_at" IS DISTINCT FROM (NEW."archived_at") OR OLD."async_updating" IS DISTINCT FROM (NEW."async_updating") OR OLD."business_criticality" IS DISTINCT FROM (NEW."business_criticality") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."disable_sla_breach_notifications" IS DISTINCT FROM (NEW."disable_sla_breach_notifications") OR OLD."enable_full_risk_acceptance" IS DISTINCT FROM (NEW."enable_full_risk_acceptance") OR OLD."enable_product_tag_inheritance" IS DISTINCT FROM (NEW."enable_product_tag_inheritance") OR OLD."enable_simple_risk_acceptance" IS DISTINCT FROM (NEW."enable_simple_risk_acceptance") OR OLD."external_audience" IS DISTINCT FROM (NEW."external_audience") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."internet_accessible" IS DISTINCT FROM (NEW."internet_accessible") OR OLD."lifecycle_id" IS DISTINCT FROM (NEW."lifecycle_id") OR OLD."name" IS DISTINCT FROM (NEW."name") OR OLD."origin_id" IS DISTINCT FROM (NEW."origin_id") OR OLD."platform_id" IS DISTINCT FROM (NEW."platform_id") OR OLD."prod_numeric_grade" IS DISTINCT FROM (NEW."prod_numeric_grade") OR OLD."prod_type_id" IS DISTINCT FROM (NEW."prod_type_id") OR OLD."product_manager_id" IS DISTINCT FROM (NEW."product_manager_id") OR OLD."revenue" IS DISTINCT FROM (NEW."revenue") OR OLD."sla_configuration_id" IS DISTINCT FROM (NEW."sla_configuration_id") OR OLD."team_manager_id" IS DISTINCT FROM (NEW."team_manager_id") OR OLD."technical_contact_id" IS DISTINCT FROM (NEW."technical_contact_id") OR OLD."tid" IS DISTINCT FROM (NEW."tid") OR OLD."user_records" IS DISTINCT FROM (NEW."user_records"))', func='INSERT INTO "dojo_productevent" ("archive_batch", "archived_at", "async_updating", "business_criticality", "created", "description", "disable_sla_breach_notifications", "enable_full_risk_acceptance", "enable_product_tag_inheritance", "enable_simple_risk_acceptance", "external_audience", "id", "internet_accessible", "lifecycle_id", "name", "origin_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "platform_id", "prod_numeric_grade", "prod_type_id", "product_manager_id", "revenue", "sla_configuration_id", "team_manager_id", "technical_contact_id", "tid", "updated", "user_records") VALUES (NEW."archive_batch", NEW."archived_at", NEW."async_updating", NEW."business_criticality", NEW."created", NEW."description", NEW."disable_sla_breach_notifications", NEW."enable_full_risk_acceptance", NEW."enable_product_tag_inheritance", NEW."enable_simple_risk_acceptance", NEW."external_audience", NEW."id", NEW."internet_accessible", NEW."lifecycle_id", NEW."name", NEW."origin_id", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."platform_id", NEW."prod_numeric_grade", NEW."prod_type_id", NEW."product_manager_id", NEW."revenue", NEW."sla_configuration_id", NEW."team_manager_id", NEW."technical_contact_id", NEW."tid", NEW."updated", NEW."user_records"); RETURN NULL;', hash="b5793f361239e1329aea5ee48f4b68ba6d9c5f0f", operation="UPDATE", pgid="pgtrigger_update_update_e7040", table="dojo_product", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="product", + trigger=pgtrigger.compiler.Trigger(name="delete_delete", sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_productevent" ("archive_batch", "archived_at", "async_updating", "business_criticality", "created", "description", "disable_sla_breach_notifications", "enable_full_risk_acceptance", "enable_product_tag_inheritance", "enable_simple_risk_acceptance", "external_audience", "id", "internet_accessible", "lifecycle_id", "name", "origin_id", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "platform_id", "prod_numeric_grade", "prod_type_id", "product_manager_id", "revenue", "sla_configuration_id", "team_manager_id", "technical_contact_id", "tid", "updated", "user_records") VALUES (OLD."archive_batch", OLD."archived_at", OLD."async_updating", OLD."business_criticality", OLD."created", OLD."description", OLD."disable_sla_breach_notifications", OLD."enable_full_risk_acceptance", OLD."enable_product_tag_inheritance", OLD."enable_simple_risk_acceptance", OLD."external_audience", OLD."id", OLD."internet_accessible", OLD."lifecycle_id", OLD."name", OLD."origin_id", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."platform_id", OLD."prod_numeric_grade", OLD."prod_type_id", OLD."product_manager_id", OLD."revenue", OLD."sla_configuration_id", OLD."team_manager_id", OLD."technical_contact_id", OLD."tid", OLD."updated", OLD."user_records"); RETURN NULL;', hash="b680a761758c22da5777b8b70f718169dfaf58b5", operation="DELETE", pgid="pgtrigger_delete_delete_064dd", table="dojo_product", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="product_type", + trigger=pgtrigger.compiler.Trigger(name="insert_insert", sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_product_typeevent" ("archive_batch", "archived_at", "created", "critical_product", "description", "id", "key_product", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "updated") VALUES (NEW."archive_batch", NEW."archived_at", NEW."created", NEW."critical_product", NEW."description", NEW."id", NEW."key_product", NEW."name", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."updated"); RETURN NULL;', hash="c22c7686452daedc2b9216ac67df591501e319f2", operation="INSERT", pgid="pgtrigger_insert_insert_2d109", table="dojo_product_type", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="product_type", + trigger=pgtrigger.compiler.Trigger(name="update_update", sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD."archive_batch" IS DISTINCT FROM (NEW."archive_batch") OR OLD."archived_at" IS DISTINCT FROM (NEW."archived_at") OR OLD."critical_product" IS DISTINCT FROM (NEW."critical_product") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."key_product" IS DISTINCT FROM (NEW."key_product") OR OLD."name" IS DISTINCT FROM (NEW."name"))', func='INSERT INTO "dojo_product_typeevent" ("archive_batch", "archived_at", "created", "critical_product", "description", "id", "key_product", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "updated") VALUES (NEW."archive_batch", NEW."archived_at", NEW."created", NEW."critical_product", NEW."description", NEW."id", NEW."key_product", NEW."name", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."updated"); RETURN NULL;', hash="f0b2de3b71e7b546d0efe2cb7950638c095d85fd", operation="UPDATE", pgid="pgtrigger_update_update_a0136", table="dojo_product_type", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="product_type", + trigger=pgtrigger.compiler.Trigger(name="delete_delete", sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_product_typeevent" ("archive_batch", "archived_at", "created", "critical_product", "description", "id", "key_product", "name", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "updated") VALUES (OLD."archive_batch", OLD."archived_at", OLD."created", OLD."critical_product", OLD."description", OLD."id", OLD."key_product", OLD."name", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."updated"); RETURN NULL;', hash="7d8aada72ff77dd0e91e25d2324c57b8d8500509", operation="DELETE", pgid="pgtrigger_delete_delete_66b18", table="dojo_product_type", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="test", + trigger=pgtrigger.compiler.Trigger(name="insert_insert", sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_testevent" ("api_scan_configuration_id", "archive_batch", "archived_at", "branch_tag", "build_id", "commit_hash", "created", "description", "engagement_id", "environment_id", "id", "lead_id", "percent_complete", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "scan_type", "target_end", "target_start", "test_type_id", "title", "updated", "version") VALUES (NEW."api_scan_configuration_id", NEW."archive_batch", NEW."archived_at", NEW."branch_tag", NEW."build_id", NEW."commit_hash", NEW."created", NEW."description", NEW."engagement_id", NEW."environment_id", NEW."id", NEW."lead_id", NEW."percent_complete", _pgh_attach_context(), NOW(), \'insert\', NEW."id", NEW."scan_type", NEW."target_end", NEW."target_start", NEW."test_type_id", NEW."title", NEW."updated", NEW."version"); RETURN NULL;', hash="477f6a297658d54b13ef83b4f4b19b0e76f2bb27", operation="INSERT", pgid="pgtrigger_insert_insert_ecfc1", table="dojo_test", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="test", + trigger=pgtrigger.compiler.Trigger(name="update_update", sql=pgtrigger.compiler.UpsertTriggerSql(condition='WHEN (OLD."api_scan_configuration_id" IS DISTINCT FROM (NEW."api_scan_configuration_id") OR OLD."archive_batch" IS DISTINCT FROM (NEW."archive_batch") OR OLD."archived_at" IS DISTINCT FROM (NEW."archived_at") OR OLD."branch_tag" IS DISTINCT FROM (NEW."branch_tag") OR OLD."build_id" IS DISTINCT FROM (NEW."build_id") OR OLD."commit_hash" IS DISTINCT FROM (NEW."commit_hash") OR OLD."description" IS DISTINCT FROM (NEW."description") OR OLD."engagement_id" IS DISTINCT FROM (NEW."engagement_id") OR OLD."environment_id" IS DISTINCT FROM (NEW."environment_id") OR OLD."id" IS DISTINCT FROM (NEW."id") OR OLD."lead_id" IS DISTINCT FROM (NEW."lead_id") OR OLD."percent_complete" IS DISTINCT FROM (NEW."percent_complete") OR OLD."scan_type" IS DISTINCT FROM (NEW."scan_type") OR OLD."target_end" IS DISTINCT FROM (NEW."target_end") OR OLD."target_start" IS DISTINCT FROM (NEW."target_start") OR OLD."test_type_id" IS DISTINCT FROM (NEW."test_type_id") OR OLD."title" IS DISTINCT FROM (NEW."title") OR OLD."version" IS DISTINCT FROM (NEW."version"))', func='INSERT INTO "dojo_testevent" ("api_scan_configuration_id", "archive_batch", "archived_at", "branch_tag", "build_id", "commit_hash", "created", "description", "engagement_id", "environment_id", "id", "lead_id", "percent_complete", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "scan_type", "target_end", "target_start", "test_type_id", "title", "updated", "version") VALUES (NEW."api_scan_configuration_id", NEW."archive_batch", NEW."archived_at", NEW."branch_tag", NEW."build_id", NEW."commit_hash", NEW."created", NEW."description", NEW."engagement_id", NEW."environment_id", NEW."id", NEW."lead_id", NEW."percent_complete", _pgh_attach_context(), NOW(), \'update\', NEW."id", NEW."scan_type", NEW."target_end", NEW."target_start", NEW."test_type_id", NEW."title", NEW."updated", NEW."version"); RETURN NULL;', hash="50e41edef10c98f5e20b9c43a324c49f5b897f88", operation="UPDATE", pgid="pgtrigger_update_update_c40f8", table="dojo_test", when="AFTER")), + ), + pgtrigger.migrations.AddTrigger( + model_name="test", + trigger=pgtrigger.compiler.Trigger(name="delete_delete", sql=pgtrigger.compiler.UpsertTriggerSql(func='INSERT INTO "dojo_testevent" ("api_scan_configuration_id", "archive_batch", "archived_at", "branch_tag", "build_id", "commit_hash", "created", "description", "engagement_id", "environment_id", "id", "lead_id", "percent_complete", "pgh_context_id", "pgh_created_at", "pgh_label", "pgh_obj_id", "scan_type", "target_end", "target_start", "test_type_id", "title", "updated", "version") VALUES (OLD."api_scan_configuration_id", OLD."archive_batch", OLD."archived_at", OLD."branch_tag", OLD."build_id", OLD."commit_hash", OLD."created", OLD."description", OLD."engagement_id", OLD."environment_id", OLD."id", OLD."lead_id", OLD."percent_complete", _pgh_attach_context(), NOW(), \'delete\', OLD."id", OLD."scan_type", OLD."target_end", OLD."target_start", OLD."test_type_id", OLD."title", OLD."updated", OLD."version"); RETURN NULL;', hash="0b9eda2793d6208cb855751dc6c39f8d6819cd2d", operation="DELETE", pgid="pgtrigger_delete_delete_66d18", table="dojo_test", when="AFTER")), + ), + ], + database_operations=[ + migrations.RunSQL(sql=[FORWARD_SQL], reverse_sql=[BACKWARD_SQL]), + ], + ), + ] diff --git a/dojo/db_migrations/0305_archive_batch_indexes.py b/dojo/db_migrations/0305_archive_batch_indexes.py new file mode 100644 index 00000000000..16919c925a6 --- /dev/null +++ b/dojo/db_migrations/0305_archive_batch_indexes.py @@ -0,0 +1,60 @@ +from django.contrib.postgres.operations import AddIndexConcurrently +from django.db import migrations, models + + +class Migration(migrations.Migration): + + # CREATE INDEX CONCURRENTLY cannot run inside a transaction, and a plain + # CREATE INDEX would lock dojo_finding and dojo_endpoint_status for the whole build. + atomic = False + + dependencies = [ + ("dojo", "0304_archive_columns"), + ] + + operations = [ + AddIndexConcurrently( + model_name="findingvulnerabilityreference", + index=models.Index(condition=models.Q(("archive_batch__isnull", False)), fields=["archive_batch"], name="dojo_findvulnref_archive_batch"), + ), + AddIndexConcurrently( + model_name="endpoint_status", + index=models.Index(condition=models.Q(("archive_batch__isnull", False)), fields=["archive_batch"], name="dojo_endpstatus_archive_batch"), + ), + AddIndexConcurrently( + model_name="engagement", + index=models.Index(condition=models.Q(("archive_batch__isnull", False)), fields=["archive_batch"], name="dojo_engagement_archive_batch"), + ), + AddIndexConcurrently( + model_name="finding", + index=models.Index(condition=models.Q(("archive_batch__isnull", False)), fields=["archive_batch"], name="dojo_finding_archive_batch"), + ), + AddIndexConcurrently( + model_name="jira_issue", + index=models.Index(condition=models.Q(("archive_batch__isnull", False)), fields=["archive_batch"], name="dojo_jiraissue_archive_batch"), + ), + AddIndexConcurrently( + model_name="locationfindingreference", + index=models.Index(condition=models.Q(("archive_batch__isnull", False)), fields=["archive_batch"], name="dojo_locfindref_archive_batch"), + ), + AddIndexConcurrently( + model_name="product", + index=models.Index(condition=models.Q(("archive_batch__isnull", False)), fields=["archive_batch"], name="dojo_product_archive_batch"), + ), + AddIndexConcurrently( + model_name="product_type", + index=models.Index(condition=models.Q(("archive_batch__isnull", False)), fields=["archive_batch"], name="dojo_prod_type_archive_batch"), + ), + AddIndexConcurrently( + model_name="test", + index=models.Index(condition=models.Q(("archive_batch__isnull", False)), fields=["archive_batch"], name="dojo_test_archive_batch"), + ), + AddIndexConcurrently( + model_name="test_import_finding_action", + index=models.Index(condition=models.Q(("archive_batch__isnull", False)), fields=["archive_batch"], name="dojo_tifa_archive_batch"), + ), + AddIndexConcurrently( + model_name="vulnerability_id", + index=models.Index(condition=models.Q(("archive_batch__isnull", False)), fields=["archive_batch"], name="dojo_vulnid_archive_batch"), + ), + ] diff --git a/dojo/endpoint/models.py b/dojo/endpoint/models.py index 628b28b9f2f..0963340ddfb 100644 --- a/dojo/endpoint/models.py +++ b/dojo/endpoint/models.py @@ -20,6 +20,7 @@ # before the re-export that loads this module — resolves despite partial circular load. # Must keep their dojo.models.* path for Django migration serialization. from dojo.models import copy_model_util, get_current_date, get_current_datetime +from dojo.query_hooks import HookedManager, UnfilteredManager logger = logging.getLogger(__name__) @@ -44,8 +45,17 @@ class Endpoint_Status(models.Model): endpoint = models.ForeignKey("dojo.Endpoint", null=False, blank=False, on_delete=models.CASCADE, related_name="status_endpoint") finding = models.ForeignKey("dojo.Finding", null=False, blank=False, on_delete=models.CASCADE, related_name="status_finding") + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedManager() + unfiltered = UnfilteredManager() + class Meta: indexes = [ + models.Index(fields=["archive_batch"], condition=models.Q(archive_batch__isnull=False), name="dojo_endpstatus_archive_batch"), models.Index(fields=["finding", "mitigated"]), models.Index(fields=["endpoint", "mitigated"]), # Optimize frequent lookups of "active" statuses (mitigated/flags all False) @@ -141,6 +151,14 @@ class Endpoint(models.Model): tags = TagField(blank=True, force_lowercase=True, help_text=_("Add tags that help describe this endpoint. Choose from the list or add new tags. Press Enter key to add.")) inherited_tags = TagField(blank=True, force_lowercase=True, help_text=_("Internal use tags sepcifically for maintaining parity with product. This field will be present as a subset in the tags field")) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedManager() + unfiltered = UnfilteredManager() + class Meta: ordering = ["product", "host", "protocol", "port", "userinfo", "path", "query", "fragment"] indexes = [ diff --git a/dojo/engagement/models.py b/dojo/engagement/models.py index 07492938afd..cd84840af3c 100644 --- a/dojo/engagement/models.py +++ b/dojo/engagement/models.py @@ -10,7 +10,8 @@ from django.utils.translation import gettext as _ from django_tagulous.models import TagField -from dojo.base_models.base import BaseModel +from dojo.base_models.base import BaseManager, BaseModel +from dojo.query_hooks import HookedBaseManager, HookedManager, UnfilteredManager logger = logging.getLogger(__name__) @@ -24,6 +25,14 @@ class Engagement_Presets(models.Model): product = models.ForeignKey("dojo.Product", on_delete=models.CASCADE) created = models.DateTimeField(auto_now_add=True, null=False) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedManager() + unfiltered = UnfilteredManager() + class Meta: ordering = ["title"] @@ -94,9 +103,18 @@ class Engagement(BaseModel): tags = TagField(blank=True, force_lowercase=True, help_text=_("Add tags that help describe this engagement. Choose from the list or add new tags. Press Enter key to add.")) inherited_tags = TagField(blank=True, force_lowercase=True, help_text=_("Internal use tags sepcifically for maintaining parity with product. This field will be present as a subset in the tags field")) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedBaseManager() + unfiltered = BaseManager() + class Meta: ordering = ["-target_start"] indexes = [ + models.Index(fields=["archive_batch"], condition=models.Q(archive_batch__isnull=False), name="dojo_engagement_archive_batch"), models.Index(fields=["product", "active"]), # Global search (pro/search/): weighted tsvector FTS + trigram fuzzy match. GinIndex( diff --git a/dojo/filters.py b/dojo/filters.py index 13b2b870100..79e00084507 100644 --- a/dojo/filters.py +++ b/dojo/filters.py @@ -117,7 +117,30 @@ def tag_exists(model, field_name, lookup, value): For a related model's tags (``test__engagement__tags__name``) the subquery runs on that model's through table and correlates on the relation's id. + + The matching tags are resolved to their ids first (one indexed read of the tag table), + and the subquery tests the through table against those ids. Matching the name through + a join instead hides which tag the query is about: the planner then assumes the + average number of rows per tag, so for a tag carried by most rows it expected hundreds + where there were hundreds of thousands, and chose a plan that probed every one of them + (20 s instead of 2.5 s for one COUNT over 400k tagged findings). A lookup that matches + more than TAG_ID_LITERAL_LIMIT tags (a short ``icontains`` value) keeps the join. """ + path, tags_field = _tags_field(model, field_name) + name_lookup = "name" if lookup == "exact" else f"name__{lookup}" + tag_ids = list(tags_field.related_model.objects.filter(**{name_lookup: value}).values_list("pk", flat=True)[ + :TAG_ID_LITERAL_LIMIT + 1]) + if len(tag_ids) > TAG_ID_LITERAL_LIMIT: + return _tags_exist(path, tags_field, {f"{tags_field.m2m_reverse_field_name()}__{name_lookup}": value}) + return _tags_exist(path, tags_field, {f"{tags_field.m2m_reverse_field_name()}__in": tag_ids}) + + +# How many tag ids tag_exists writes into the query as literals before it matches by name instead. +TAG_ID_LITERAL_LIMIT = 1000 + + +def _tags_field(model, field_name): + """The relation path to the tagged model and its tags field, for a ``[__]tags__name`` field.""" *path, tags_field_name, name = field_name.split("__") if name != "name": msg = f"tag_exists expects a __tags__name field, got {field_name}" @@ -125,13 +148,13 @@ def tag_exists(model, field_name, lookup, value): tagged_model = model for part in path: tagged_model = tagged_model._meta.get_field(part).related_model - tags_field = tagged_model._meta.get_field(tags_field_name) - tag_lookup = f"{tags_field.m2m_reverse_field_name()}__name" - if lookup != "exact": - tag_lookup += f"__{lookup}" + return path, tagged_model._meta.get_field(tags_field_name) + + +def _tags_exist(path, tags_field, tag_condition): outer = "__".join([*path, "pk"]) if path else "pk" return Exists(tags_field.remote_field.through.objects.filter( - **{tags_field.m2m_field_name(): OuterRef(outer), tag_lookup: value}, + **{tags_field.m2m_field_name(): OuterRef(outer), **tag_condition}, )) @@ -164,15 +187,26 @@ class TagNameInFilter(TagExistsFilterMixin, CharFieldInFilter): class TagNameANDFilter(CharFieldFilterANDExpression): - """Every listed tag must be present: one Exists() per tag instead of GROUP BY ... HAVING COUNT.""" + """ + Every listed tag must be present: one Exists() per tag instead of GROUP BY ... HAVING COUNT. + + The listed names are resolved to tag ids in one read (see tag_exists for why the subqueries name + ids). A name that is no tag cannot be present, so the whole filter matches nothing. + """ multiplies_rows = False def filter(self, queryset, value): if not value: return queryset - for tag in set(value.split(",")): - queryset = queryset.filter(tag_exists(queryset.model, self.field_name, "exact", tag)) + names = set(value.split(",")) + path, tags_field = _tags_field(queryset.model, self.field_name) + tag_ids = dict(tags_field.related_model.objects.filter(name__in=names).values_list("name", "pk")) + if len(tag_ids) < len(names): + return queryset.none() + for tag_id in tag_ids.values(): + queryset = queryset.filter( + _tags_exist(path, tags_field, {f"{tags_field.m2m_reverse_field_name()}__in": [tag_id]})) return queryset diff --git a/dojo/finding/api/filters.py b/dojo/finding/api/filters.py index f09608fa137..446d51fe10f 100644 --- a/dojo/finding/api/filters.py +++ b/dojo/finding/api/filters.py @@ -117,6 +117,11 @@ class ApiFindingFilter(DojoFilter): test__engagement = NumberInFilter(field_name="test__engagement", lookup_expr="in") test__engagement__product = NumberInFilter(field_name="test__engagement__product", lookup_expr="in") test__engagement__product__prod_type = NumberInFilter(field_name="test__engagement__product__prod_type", lookup_expr="in") + # Short names for the three filters above. A Finding has no engagement / product / prod_type field, so + # without these django-filter dropped ?engagement= silently and listed every finding the caller can see. + engagement = NumberInFilter(field_name="test__engagement", lookup_expr="in", label="Engagement") + product = NumberInFilter(field_name="test__engagement__product", lookup_expr="in", label=labels.ASSET_LABEL) + prod_type = NumberInFilter(field_name="test__engagement__product__prod_type", lookup_expr="in", label=labels.ORG_LABEL) finding_group = NumberInFilter(method="filter_finding_group") # ExistsRiskAcceptanceFilter diff --git a/dojo/finding/api/views.py b/dojo/finding/api/views.py index d689d195f54..4d3d10c357a 100644 --- a/dojo/finding/api/views.py +++ b/dojo/finding/api/views.py @@ -80,6 +80,7 @@ get_system_setting, process_tag_notifications, ) +from dojo.vulnerability.queries import prefetch_vulnerability_ids logger = logging.getLogger(__name__) @@ -206,6 +207,15 @@ def destroy(self, request, *args, **kwargs): finding_helper.delete_finding_with_conflict_retry(instance, push_to_jira=push_to_jira) return Response(status=status.HTTP_204_NO_CONTENT) + def paginate_queryset(self, queryset): + page = super().paginate_queryset(queryset) + # vulnerability_ids is serialized on every row; unprefetched it costs a query per finding. + # Loaded for the page instead of in get_queryset(), so a subclass that adds its own + # vulnerability_id_prefetch() to the queryset doesn't get a duplicate-lookup error. + if page is not None: + prefetch_vulnerability_ids(page) + return page + def get_queryset(self): if locations_enabled(): findings = get_authorized_findings( diff --git a/dojo/finding/models.py b/dojo/finding/models.py index 3a8601d66dd..3c0001b058d 100644 --- a/dojo/finding/models.py +++ b/dojo/finding/models.py @@ -24,7 +24,7 @@ from django_tagulous.models import TagField from titlecase import titlecase -from dojo.base_models.base import BaseModel +from dojo.base_models.base import BaseManager, BaseModel from dojo.finding.cwe import cwe_label, finding_cwe_labels from dojo.finding.vulnerability_id import resolve_vulnerability_id_type @@ -32,6 +32,7 @@ # re-export that loads this module — so this resolves despite the partial circular load, and # keeps their dojo.models.* path for Django migration serialization (used as field defaults). from dojo.models import copy_model_util, get_current_date, tomorrow +from dojo.query_hooks import HookedBaseManager, HookedManager, UnfilteredManager from dojo.validators import cvss3_validator, cvss4_validator if TYPE_CHECKING: @@ -454,9 +455,18 @@ class Finding(BaseModel): SEVERITIES = {"Info": 4, "Low": 3, "Medium": 2, "High": 1, "Critical": 0} + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedBaseManager() + unfiltered = BaseManager() + class Meta: ordering = ("numerical_severity", "-date", "title", "epss_score", "epss_percentile") indexes = [ + models.Index(fields=["archive_batch"], condition=models.Q(archive_batch__isnull=False), name="dojo_finding_archive_batch"), # Global search (pro/search/): weighted tsvector FTS + trigram fuzzy match. GinIndex( SearchVector("title", weight="A", config="english") @@ -1562,11 +1572,20 @@ class Vulnerability_Id(models.Model): # prefix. Denormalized/indexed so type-scoped queries (e.g. GROUP BY type) stay cheap. vulnerability_id_type = models.CharField(max_length=20, null=True, blank=True, editable=False, db_index=True) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedManager() + unfiltered = UnfilteredManager() + class Meta: constraints = [ models.UniqueConstraint(fields=["finding", "vulnerability_id"], name="unique_finding_vulnerability_id"), ] indexes = [ + models.Index(fields=["archive_batch"], condition=models.Q(archive_batch__isnull=False), name="dojo_vulnid_archive_batch"), # Leading on vulnerability_id (the unique constraint's index leads on finding), for the # vulnerability-id Explorer's GROUP BY vulnerability_id / lookups by exact id. models.Index(fields=["vulnerability_id"], name="dojo_vuln_id_lookup_idx"), @@ -1635,6 +1654,14 @@ class Finding_Group(TimeStampedModel): findings = models.ManyToManyField("dojo.Finding") creator = models.ForeignKey("dojo.Dojo_User", on_delete=models.RESTRICT) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedManager() + unfiltered = UnfilteredManager() + def __str__(self): return self.name diff --git a/dojo/jira/models.py b/dojo/jira/models.py index 17b742f98b5..a2d7f998edb 100644 --- a/dojo/jira/models.py +++ b/dojo/jira/models.py @@ -5,6 +5,8 @@ from django.db import models from django.utils.translation import gettext as _ +from dojo.query_hooks import HookedManager, UnfilteredManager + class JIRA_Instance(models.Model): configuration_name = models.CharField(max_length=2000, help_text=_("Enter a name to give to this configuration"), default="") @@ -139,6 +141,14 @@ class JIRA_Project(models.Model): default=True, blank=True) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedManager() + unfiltered = UnfilteredManager() + class Meta: app_label = "dojo" @@ -197,7 +207,18 @@ class JIRA_Issue(models.Model): verbose_name=_("Jira last update"), help_text=_("The date the linked Jira issue was last modified.")) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedManager() + unfiltered = UnfilteredManager() + class Meta: + indexes = [ + models.Index(fields=["archive_batch"], condition=models.Q(archive_batch__isnull=False), name="dojo_jiraissue_archive_batch"), + ] app_label = "dojo" def __str__(self): diff --git a/dojo/location/manager.py b/dojo/location/manager.py index 56b8bd6b445..1cf940257f0 100644 --- a/dojo/location/manager.py +++ b/dojo/location/manager.py @@ -4,6 +4,7 @@ from dojo.base_models.base import BaseManager, BaseQuerySet from dojo.location.status import rollup_finding_location_status +from dojo.query_hooks import HookedManagerMixin class LocationQueryset(BaseQuerySet): @@ -72,3 +73,13 @@ class LocationFindingReferenceManager(BaseManager): """LocationFindingReference manager to manipulate all objects with.""" QUERY_SET_CLASS = LocationFindingReferenceQueryset + + +class HookedLocationProductReferenceManager(HookedManagerMixin, LocationProductReferenceManager): + + """LocationProductReference manager that applies the registered default-queryset filters.""" + + +class HookedLocationFindingReferenceManager(HookedManagerMixin, LocationFindingReferenceManager): + + """LocationFindingReference manager that applies the registered default-queryset filters.""" diff --git a/dojo/location/models.py b/dojo/location/models.py index f59d7264753..1d0b24c564b 100644 --- a/dojo/location/models.py +++ b/dojo/location/models.py @@ -23,6 +23,7 @@ QuerySet, TextChoices, UniqueConstraint, + UUIDField, ) from django.utils.translation import gettext_lazy as _ from django_tagulous.models import TagField @@ -30,6 +31,8 @@ from dojo.base_models.base import BaseModel, BaseModelWithoutTimeMeta from dojo.base_models.validators import validate_not_empty from dojo.location.manager import ( + HookedLocationFindingReferenceManager, + HookedLocationProductReferenceManager, LocationFindingReferenceManager, LocationFindingReferenceQueryset, LocationManager, @@ -654,7 +657,13 @@ class LocationFindingReference(BaseModel, ReferenceDataMixin): help_text=_("The status of the the given Location"), ) - objects = LocationFindingReferenceManager().from_queryset(LocationFindingReferenceQueryset)() + # Read and written only by plugins; open source never sets them. + archived_at = DateTimeField(null=True, blank=True, editable=False) + archive_batch = UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedLocationFindingReferenceManager().from_queryset(LocationFindingReferenceQueryset)() + unfiltered = LocationFindingReferenceManager().from_queryset(LocationFindingReferenceQueryset)() class Meta: verbose_name = "Locations - FindingReference" @@ -666,6 +675,7 @@ class Meta: ), ] indexes = [ + Index(fields=["archive_batch"], condition=Q(archive_batch__isnull=False), name="dojo_locfindref_archive_batch"), Index(fields=["location"]), Index(fields=["finding"]), ] @@ -717,7 +727,13 @@ class LocationProductReference(BaseModel, ReferenceDataMixin): help_text=_("The status of the the given Location"), ) - objects = LocationProductReferenceManager().from_queryset(LocationProductReferenceQueryset)() + # Read and written only by plugins; open source never sets them. + archived_at = DateTimeField(null=True, blank=True, editable=False) + archive_batch = UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedLocationProductReferenceManager().from_queryset(LocationProductReferenceQueryset)() + unfiltered = LocationProductReferenceManager().from_queryset(LocationProductReferenceQueryset)() class Meta: verbose_name = "Locations - ProductReference" diff --git a/dojo/models.py b/dojo/models.py index c813004454a..8f936e6f63d 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -173,6 +173,7 @@ def get_current_datetime(): Product_Line, # noqa: F401 -- re-export ) from dojo.product_type.models import Product_Type # noqa: E402, F401 -- re-export +from dojo.query_hooks import HookedManager, UnfilteredManager # noqa: E402 from dojo.reports.models import Report_Type # noqa: E402, F401 -- re-export from dojo.test.models import ( # noqa: E402 -- re-export; class-body FKs below reference these IMPORT_ACTIONS, # noqa: F401 -- re-export @@ -218,6 +219,14 @@ class DojoMeta(models.Model): editable=False, related_name="location_meta_scope") + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedManager() + unfiltered = UnfilteredManager() + class Meta: unique_together = (("product", "name"), ("endpoint", "name"), @@ -545,6 +554,14 @@ class Languages(models.Model): code = models.IntegerField(blank=True, null=True, verbose_name=_("Number of code lines")) created = models.DateTimeField(auto_now_add=True, null=False) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedManager() + unfiltered = UnfilteredManager() + class Meta: unique_together = [("language", "product")] @@ -565,6 +582,14 @@ class App_Analysis(models.Model): tags = TagField(blank=True, force_lowercase=True) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedManager() + unfiltered = UnfilteredManager() + class Meta: indexes = [ # Global search (pro/search/): weighted tsvector FTS + trigram fuzzy match. diff --git a/dojo/object/models.py b/dojo/object/models.py index 30b086ba544..5f1fb2b3d60 100644 --- a/dojo/object/models.py +++ b/dojo/object/models.py @@ -2,6 +2,8 @@ from django.utils.translation import gettext as _ from django_tagulous.models import TagField +from dojo.query_hooks import HookedManager, UnfilteredManager + class Objects_Review(models.Model): name = models.CharField(max_length=100, null=True, blank=True) @@ -25,6 +27,14 @@ class Objects_Product(models.Model): tags = TagField(blank=True, force_lowercase=True, help_text=_("Add tags that help describe this object. Choose from the list or add new tags. Press Enter key to add.")) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedManager() + unfiltered = UnfilteredManager() + def __str__(self): name = None if self.path is not None: diff --git a/dojo/product/models.py b/dojo/product/models.py index 8fd475308eb..52d00ce79c1 100644 --- a/dojo/product/models.py +++ b/dojo/product/models.py @@ -11,7 +11,8 @@ from django.utils.translation import gettext as _ from django_tagulous.models import TagField -from dojo.base_models.base import BaseModel +from dojo.base_models.base import BaseManager, BaseModel +from dojo.query_hooks import HookedBaseManager, HookedManager, UnfilteredManager class Product_Line(models.Model): @@ -131,9 +132,18 @@ class Product(BaseModel): async_updating = models.BooleanField(default=False, help_text=_("Findings under this Product or SLA configuration are asynchronously being updated")) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedBaseManager() + unfiltered = BaseManager() + class Meta: ordering = ("name",) indexes = [ + models.Index(fields=["archive_batch"], condition=models.Q(archive_batch__isnull=False), name="dojo_product_archive_batch"), # Serves the case-insensitive product-name lookup # (WHERE UPPER(name) = UPPER(%s)) used when filtering findings by # product name; the plain unique btree on name can't (Sentry DJANGO-D2M). @@ -307,6 +317,14 @@ class Product_API_Scan_Configuration(models.Model): service_key_2 = models.CharField(max_length=200, null=True, blank=True) service_key_3 = models.CharField(max_length=200, null=True, blank=True) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedManager() + unfiltered = UnfilteredManager() + def __str__(self): name = self.tool_configuration.name if self.service_key_1 or self.service_key_2 or self.service_key_3: diff --git a/dojo/product_type/models.py b/dojo/product_type/models.py index e9394d7e45a..7cfa3450e61 100644 --- a/dojo/product_type/models.py +++ b/dojo/product_type/models.py @@ -4,7 +4,8 @@ from django.urls import reverse from django.utils.functional import cached_property -from dojo.base_models.base import BaseModel +from dojo.base_models.base import BaseManager, BaseModel +from dojo.query_hooks import HookedBaseManager class Product_Type(BaseModel): @@ -25,9 +26,18 @@ class Product_Type(BaseModel): key_product = models.BooleanField(default=False) authorized_users = models.ManyToManyField("dojo.Dojo_User", related_name="authorized_product_types", blank=True) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedBaseManager() + unfiltered = BaseManager() + class Meta: ordering = ("name",) indexes = [ + models.Index(fields=["archive_batch"], condition=models.Q(archive_batch__isnull=False), name="dojo_prod_type_archive_batch"), # Global search (pro/search/): weighted tsvector FTS + trigram fuzzy match. GinIndex( SearchVector("name", weight="A", config="english") diff --git a/dojo/query_hooks.py b/dojo/query_hooks.py new file mode 100644 index 00000000000..ca0d0e955f9 --- /dev/null +++ b/dojo/query_hooks.py @@ -0,0 +1,53 @@ +""" +Default-queryset hooks for plugins. + +A model whose ``objects`` manager is a ``HookedManager`` (or ``HookedBaseManager``) +applies every filter registered for its label, in registration order, to the +queryset it returns. With nothing registered the queryset is unchanged, so the +open source behaviour is exactly that of a plain manager. + +``objects`` is declared first on those models, so it is also ``_default_manager``: +the filters reach reverse relations, ``get()`` and uniqueness checks as well. Each +model also carries an ``unfiltered`` manager, the explicit way to bypass them. +``_base_manager`` stays a plain manager, so forward foreign key access and the +deletion collector are never filtered. +""" + +from django.db.models import Manager + +from dojo.base_models.base import BaseManager + +_DEFAULT_QUERYSET_FILTERS = {} + + +def register_default_queryset_filter(model_label, key, func): + # Keyed so a repeated ready() (tests, autoreload) replaces rather than stacks. + _DEFAULT_QUERYSET_FILTERS.setdefault(model_label.lower(), {})[key] = func + + +def get_default_queryset_filters(model_label): + return tuple(_DEFAULT_QUERYSET_FILTERS.get(model_label.lower(), {}).values()) + + +def apply_default_queryset_filters(model, queryset): + for func in get_default_queryset_filters(model._meta.label): + queryset = func(queryset) + return queryset + + +class HookedManagerMixin: + def get_queryset(self): + return apply_default_queryset_filters(self.model, super().get_queryset()) + + +class HookedManager(HookedManagerMixin, Manager): + pass + + +class HookedBaseManager(HookedManagerMixin, BaseManager): + pass + + +class UnfilteredManager(Manager): + + """A plain manager, declared as ``unfiltered`` beside a hooked ``objects``: the explicit bypass.""" diff --git a/dojo/settings/settings.dist.py b/dojo/settings/settings.dist.py index 6f2a0c40c66..0705f8e63de 100644 --- a/dojo/settings/settings.dist.py +++ b/dojo/settings/settings.dist.py @@ -159,6 +159,8 @@ DD_DATABASE_PORT=(int, 3306), DD_DATABASE_USER=(str, "defectdojo"), DD_SECRET_KEY=(str, ""), + # Older secret keys, comma separated, kept readable while DD_SECRET_KEY changes + DD_SECRET_KEY_FALLBACKS=(list, []), DD_CREDENTIAL_AES_256_KEY=(str, "."), DD_DATA_UPLOAD_MAX_MEMORY_SIZE=(int, 8388608), # Max post size set to 8mb DD_DATA_UPLOAD_MAX_NUMBER_FIELDS=(int, 10240), # Max number of GET/POST parameters in a request @@ -382,6 +384,7 @@ def generate_url(scheme, double_slashes, user, password, host, port, path, param # Raises django's ImproperlyConfigured exception if SECRET_KEY not in os.environ SECRET_KEY = env("DD_SECRET_KEY") +SECRET_KEY_FALLBACKS = env("DD_SECRET_KEY_FALLBACKS") # Default cache backend (django.core.cache). Redis when DD_CACHE_URL is set, # else per-process LocMemCache. General framework caching only; the singleton diff --git a/dojo/tasks.py b/dojo/tasks.py index deb9b6028dd..0bd8d9cab3e 100644 --- a/dojo/tasks.py +++ b/dojo/tasks.py @@ -29,6 +29,23 @@ # Findings locked and re-checked per transaction by _delete_excess_duplicates. DUPE_DELETE_CHUNK_SIZE = 1000 +# A plugin may set excess duplicates aside instead of deleting them. The handler gets each +# chunk's queryset inside the chunk's transaction, with the rows already locked, and returns +# True when it has dealt with them. It must leave them out of Finding.objects, or the next run +# selects them again. With no handler, or one that returns False, the chunk is deleted. +_excess_duplicate_handler = None + + +def register_excess_duplicate_handler(func): + global _excess_duplicate_handler # noqa: PLW0603 + _excess_duplicate_handler = func + + +def _remove_excess_duplicates(findings): + if _excess_duplicate_handler is not None and _excess_duplicate_handler(findings): + return + bulk_delete_findings(findings, order_desc=True) + from dojo.notifications.tasks import ( # noqa: E402, F401 -- backward compat add_alerts, @@ -216,7 +233,7 @@ def _delete_excess_duplicates(ids_to_delete): if skipped := len(chunk) - len(still_settled): logger.info("skipping %s excess duplicates that an import is writing, or that were saved or deleted since they were selected", skipped) if still_settled: - bulk_delete_findings(Finding.objects.filter(id__in=still_settled), order_desc=True) + _remove_excess_duplicates(Finding.objects.filter(id__in=still_settled)) deleted_ids.extend(still_settled) return deleted_ids diff --git a/dojo/test/models.py b/dojo/test/models.py index 92c1e44ceab..15ef11ae108 100644 --- a/dojo/test/models.py +++ b/dojo/test/models.py @@ -11,6 +11,8 @@ from django_extensions.db.models import TimeStampedModel from django_tagulous.models import TagField +from dojo.query_hooks import HookedManager, UnfilteredManager + logger = logging.getLogger(__name__) deduplicationLogger = logging.getLogger("dojo.specific-loggers.deduplication") @@ -80,8 +82,17 @@ class Test(models.Model): null=True, blank=True, help_text=_("Tag or branch that was tested, a reimport may update this field."), verbose_name=_("Branch/Tag")) api_scan_configuration = models.ForeignKey("dojo.Product_API_Scan_Configuration", null=True, editable=True, blank=True, on_delete=models.CASCADE, verbose_name=_("API Scan Configuration")) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedManager() + unfiltered = UnfilteredManager() + class Meta: indexes = [ + models.Index(fields=["archive_batch"], condition=models.Q(archive_batch__isnull=False), name="dojo_test_archive_batch"), models.Index(fields=["engagement", "test_type"]), # Global search (pro/search/): weighted tsvector FTS + trigram fuzzy match. GinIndex( @@ -249,6 +260,14 @@ class Test_Import(TimeStampedModel): branch_tag = models.CharField(editable=True, max_length=150, null=True, blank=True, help_text=_("Tag or branch that was tested, a reimport may update this field."), verbose_name=_("Branch/Tag")) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedManager() + unfiltered = UnfilteredManager() + def get_queryset(self): logger.debug("prefetch test_import counts") super_query = super().get_queryset() @@ -288,8 +307,17 @@ class Test_Import_Finding_Action(TimeStampedModel): finding = models.ForeignKey("dojo.Finding", editable=False, null=False, blank=False, on_delete=models.CASCADE) action = models.CharField(max_length=100, null=True, blank=True, choices=IMPORT_ACTIONS) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedManager() + unfiltered = UnfilteredManager() + class Meta: indexes = [ + models.Index(fields=["archive_batch"], condition=models.Q(archive_batch__isnull=False), name="dojo_tifa_archive_batch"), models.Index(fields=["finding", "action", "test_import"]), ] unique_together = (("test_import", "finding")) diff --git a/dojo/tool_product/models.py b/dojo/tool_product/models.py index 2f77ea89a6e..a68130581bc 100644 --- a/dojo/tool_product/models.py +++ b/dojo/tool_product/models.py @@ -2,6 +2,8 @@ from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ +from dojo.query_hooks import HookedManager, UnfilteredManager + class Tool_Product_Settings(models.Model): name = models.CharField(max_length=200, null=False) @@ -13,6 +15,14 @@ class Tool_Product_Settings(models.Model): tool_project_id = models.CharField(max_length=200, null=True, blank=True) notes = models.ManyToManyField("dojo.Notes", blank=True, editable=False) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedManager() + unfiltered = UnfilteredManager() + class Meta: ordering = ["name"] diff --git a/dojo/tools/locations.py b/dojo/tools/locations.py index b04794907fb..0c3b26418ad 100644 --- a/dojo/tools/locations.py +++ b/dojo/tools/locations.py @@ -140,6 +140,92 @@ def host( }, ) + @classmethod + def domain( + cls, + *, + fqdn: str, + apex: str = "", + ) -> LocationData: + """ + A DNS name: a subdomain, an apex domain or a wildcard name such as ``*.example.com``. + Report it as the scanner or certificate gives it; the consumer lowercases it, drops a + trailing dot and converts an internationalized name to its ASCII form, so identity is + that normalized name. ``apex`` is the registrable domain the name sits under when the + tool knows it, and describes the name rather than identifying it. + + DefectDojo Pro stores domain, ip and cert locations; Community Edition does not register + these types and drops them, as it does container images. + + A DNS name is not a host. A machine known by its name is ``host``; a name a + recon tool resolved or a certificate covers is ``domain``. Never pass an IP address here. + """ + return cls(type="domain", data={"fqdn": fqdn, "apex": apex}) + + @classmethod + def ip( + cls, + *, + address: str, + asn: int | None = None, + asn_organization: str = "", + network: str = "", + provider: str = "", + ) -> LocationData: + """ + An IP address, IPv4 or IPv6, with no port or scheme. Identity is the canonical + address, so ``2001:DB8::1`` and ``2001:db8:0:0::1`` are one location. The other keys + describe the address when the tool knows them: the announcing ``asn`` and its + ``asn_organization``, the registry ``network`` it sits in as CIDR, and the cloud or CDN + ``provider`` that serves it. An ``asn`` outside 1 to 4294967295 is stored as unknown. A web + service on the address stays a ``url`` location. + """ + return cls( + type="ip", + data={ + "address": address, + "asn": asn, + "asn_organization": asn_organization, + "network": network, + "provider": provider, + }, + ) + + @classmethod + def cert( + cls, + *, + sha256: str, + subject_cn: str = "", + issuer: str = "", + serial_number: str = "", + not_before: str = "", + not_after: str = "", + sans: list[str] | str | None = None, + ) -> LocationData: + """ + An X.509 certificate, identified by the SHA-256 fingerprint of its DER encoding, + written as hex with or without colons. The other keys describe the certificate: + the subject common name, the issuer, the serial number, the validity window as + ISO 8601 timestamps (read as UTC when they carry no offset; a time that cannot be + read is stored as unknown) and the subject alternative names, a list of names or a + single name. Report the names a certificate covers as ``domain`` locations too when + they matter on their own. + """ + return cls( + type="cert", + data={ + "sha256": sha256, + "subject_cn": subject_cn, + "issuer": issuer, + "serial_number": serial_number, + "not_before": not_before, + "not_after": not_after, + # One name passed as a string is one name, not a list of its characters. + "sans": [sans] if isinstance(sans, str) else list(sans or []), + }, + ) + @classmethod def image( cls, diff --git a/dojo/tools/qualys/parser.py b/dojo/tools/qualys/parser.py index 465301310f4..c8e3cc53778 100644 --- a/dojo/tools/qualys/parser.py +++ b/dojo/tools/qualys/parser.py @@ -201,7 +201,7 @@ def split_cvss(value, _temp): _temp["CVSS_value"] = float(value) -def parse_finding(host, tree): +def parse_finding(host, glossary): ret_rows = [] issue_row = {} @@ -265,8 +265,12 @@ def parse_finding(host, tree): # DefectDojo does not support cvssv2 temp["CVSS_vector"] = None - search = f".//GLOSSARY/VULN_DETAILS_LIST/VULN_DETAILS[@id='{gid}']" - vuln_item = tree.find(search) + vuln_item = glossary.get(gid) + if vuln_item is None: + # A finding whose QID has no GLOSSARY entry has neither severity nor title to + # import, so skip it with a warning instead of aborting the whole import. + logger.warning("Qualys report has no GLOSSARY entry for QID %s, skipping its finding", gid) + continue if vuln_item is not None: finding = Finding() # Vuln name @@ -373,6 +377,21 @@ def qualys_parser(qualys_xml_file): host_list = tree.find("HOST_LIST") finding_list = [] if host_list is not None: + glossary = vuln_details_by_id(tree) for host in host_list: - finding_list += parse_finding(host, tree) + finding_list += parse_finding(host, glossary) return finding_list + + +def vuln_details_by_id(tree): + """ + The report's glossary entries by QID, read once per report. + + Looking a QID up with a search from the document root walks every host before it reaches the + glossary, so doing that for each finding made a parse cost findings x report size. The first + entry for an id wins, as that search returned it. + """ + glossary = {} + for vuln_item in tree.iterfind(".//GLOSSARY/VULN_DETAILS_LIST/VULN_DETAILS"): + glossary.setdefault(vuln_item.get("id"), vuln_item) + return glossary diff --git a/dojo/vulnerability/models.py b/dojo/vulnerability/models.py index 6ae4cb9b935..d919b63af3a 100644 --- a/dojo/vulnerability/models.py +++ b/dojo/vulnerability/models.py @@ -4,6 +4,8 @@ from django.db import models from django.db.models.functions import Upper +from dojo.query_hooks import HookedManager, UnfilteredManager + class Vulnerability(models.Model): @@ -80,6 +82,14 @@ class FindingVulnerabilityReference(models.Model): ) created = models.DateTimeField(auto_now_add=True) + # Read and written only by plugins; open source never sets them. + archived_at = models.DateTimeField(null=True, blank=True, editable=False) + archive_batch = models.UUIDField(null=True, blank=True, editable=False) + + # objects is declared first, so it is the default manager; see dojo.query_hooks. + objects = HookedManager() + unfiltered = UnfilteredManager() + class Meta: ordering = ["order"] constraints = [ @@ -89,7 +99,11 @@ class Meta: # insert write path, which assigns 0..n-1 in one transaction (see manager.py). models.UniqueConstraint(fields=["finding", "order"], name="unique_finding_order"), ] - indexes = [models.Index(fields=["finding", "order"]), models.Index(fields=["vulnerability"])] + indexes = [ + models.Index(fields=["finding", "order"]), + models.Index(fields=["vulnerability"]), + models.Index(fields=["archive_batch"], condition=models.Q(archive_batch__isnull=False), name="dojo_findvulnref_archive_batch"), + ] def __str__(self): return self.vulnerability.vulnerability_id diff --git a/dojo/vulnerability/queries.py b/dojo/vulnerability/queries.py index 3d1ccd310c2..09ef6cbffec 100644 --- a/dojo/vulnerability/queries.py +++ b/dojo/vulnerability/queries.py @@ -1,6 +1,6 @@ import logging -from django.db.models import OuterRef, Prefetch +from django.db.models import OuterRef, Prefetch, prefetch_related_objects try: from dojo.authorization.query_filters import get_auth_filter @@ -47,6 +47,25 @@ def vulnerability_id_prefetch(prefix=""): ) +def prefetch_vulnerability_ids(findings): + """ + Load the vulnerability id references of ``findings`` (saved Finding instances, e.g. a list page) + in one query, so ``finding_vulnerability_id_strings()`` reads them without a query per finding. + + Findings whose ``vulnerability_references`` are already prefetched are left as they are. This is + applied to a page of instances rather than to a queryset because a queryset that already carries + a ``vulnerability_id_prefetch()`` (a subclass or plugin may add one) raises Django's "lookup was + already seen with a different queryset" error when a second one is added. + """ + missing = [ + finding for finding in findings + if "vulnerability_references" not in getattr(finding, "_prefetched_objects_cache", {}) + ] + if missing: + prefetch_related_objects(missing, vulnerability_id_prefetch()) + return findings + + def finding_vulnerability_id_strings(finding): """ Ordered vulnerability id strings for a saved finding (raw reference relation — no cve merge, diff --git a/helm/defectdojo/Chart.yaml b/helm/defectdojo/Chart.yaml index b560418c774..3ca827f9b94 100644 --- a/helm/defectdojo/Chart.yaml +++ b/helm/defectdojo/Chart.yaml @@ -1,8 +1,8 @@ apiVersion: v2 -appVersion: "3.4.304" +appVersion: "3.5.0-dev" description: A Helm chart for Kubernetes to install DefectDojo name: defectdojo -version: 1.9.64 +version: 1.9.65-dev icon: https://defectdojo.com/hubfs/DefectDojo_favicon.png maintainers: - name: madchap @@ -33,5 +33,5 @@ dependencies: # - kind: security # description: Critical bug annotations: - artifacthub.io/prerelease: "false" - artifacthub.io/changes: "- kind: changed\n description: Bump DefectDojo to 3.4.304\n" + artifacthub.io/prerelease: "true" + artifacthub.io/changes: "" diff --git a/helm/defectdojo/README.md b/helm/defectdojo/README.md index d6e306186ed..93b3368c437 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 -![Version: 1.9.64](https://img.shields.io/badge/Version-1.9.64-informational?style=flat-square) ![AppVersion: 3.4.304](https://img.shields.io/badge/AppVersion-3.4.304-informational?style=flat-square) +![Version: 1.9.65-dev](https://img.shields.io/badge/Version-1.9.65--dev-informational?style=flat-square) ![AppVersion: 3.5.0-dev](https://img.shields.io/badge/AppVersion-3.5.0--dev-informational?style=flat-square) A Helm chart for Kubernetes to install DefectDojo diff --git a/unittests/test_api_finding_relation_filters.py b/unittests/test_api_finding_relation_filters.py new file mode 100644 index 00000000000..6155b15f7bf --- /dev/null +++ b/unittests/test_api_finding_relation_filters.py @@ -0,0 +1,150 @@ +from django.urls import reverse +from django.utils.timezone import now +from rest_framework.test import APIClient + +from dojo.authorization.roles_permissions import Roles +from dojo.filters import ApiFindingFilter +from dojo.models import ( + Dojo_User, + Engagement, + Finding, + Product, + Product_Member, + Product_Type, + Role, + Test, + Test_Type, +) + +from .dojo_test_case import DojoTestCase + + +class TestApiFindingRelationFilters(DojoTestCase): + + """ + Regression: ?engagement= on /api/v2/findings/ was not a declared filter. A Finding has no + engagement field, so django-filter dropped the parameter without an error and the list answered + with every finding the caller could see. engagement, product and prod_type are now short names + for the test__engagement, test__engagement__product and test__engagement__product__prod_type + filters. + """ + + @classmethod + def setUpTestData(cls): + cls.user = Dojo_User.objects.create(username="rel_filter_user", is_staff=True, is_superuser=True) + cls.test_type = Test_Type.objects.create(name="Relation Filter Test Type") + cls.type_a = Product_Type.objects.create(name="Relation Filter PT A") + cls.type_b = Product_Type.objects.create(name="Relation Filter PT B") + cls.product_a = Product.objects.create(name="Relation Filter A", prod_type=cls.type_a, description="a") + cls.product_b = Product.objects.create(name="Relation Filter B", prod_type=cls.type_b, description="b") + cls.findings = {} + # Two engagements on product A, so engagement and product answer different sets. + for key, product, severities in ( + ("a1", cls.product_a, ["High", "High", "Low"]), + ("a2", cls.product_a, ["High", "Medium"]), + ("b", cls.product_b, ["High", "Critical", "Low", "Info"]), + ): + engagement = Engagement.objects.create( + name=f"Relation Filter {key}", product=product, target_start=now(), target_end=now(), + ) + setattr(cls, f"eng_{key}", engagement) + test = Test.objects.create( + engagement=engagement, test_type=cls.test_type, target_start=now(), target_end=now(), + ) + cls.findings[key] = { + Finding.objects.create( + title=f"Relation Filter {key} {index}", test=test, reporter=cls.user, severity=severity, + ).id + for index, severity in enumerate(severities) + } + cls.scope = Finding.objects.filter(test__engagement__product__in=[cls.product_a, cls.product_b]) + + def _filtered(self, data): + filterset = ApiFindingFilter(data=data, queryset=self.scope) + self.assertTrue(filterset.is_valid(), msg=filterset.errors) + return set(filterset.qs.values_list("id", flat=True)) + + def test_short_names_are_declared_on_the_long_form_fields(self): + for alias, field_name in ( + ("engagement", "test__engagement"), + ("product", "test__engagement__product"), + ("prod_type", "test__engagement__product__prod_type"), + ): + with self.subTest(alias=alias): + declared = ApiFindingFilter.base_filters.get(alias) + self.assertIsNotNone(declared, msg=f"{alias} is not declared, so django-filter drops it") + self.assertEqual(declared.field_name, field_name) + self.assertEqual(declared.lookup_expr, "in") + + def test_engagement(self): + self.assertEqual(self._filtered({"engagement": str(self.eng_a1.id)}), self.findings["a1"]) + self.assertEqual( + self._filtered({"engagement": f"{self.eng_a1.id},{self.eng_b.id}"}), + self.findings["a1"] | self.findings["b"], + ) + + def test_product(self): + self.assertEqual( + self._filtered({"product": str(self.product_a.id)}), self.findings["a1"] | self.findings["a2"], + ) + + def test_prod_type(self): + self.assertEqual(self._filtered({"prod_type": str(self.type_b.id)}), self.findings["b"]) + + def test_short_names_match_the_long_forms(self): + for alias, long_form, value in ( + ("engagement", "test__engagement", self.eng_a2.id), + ("product", "test__engagement__product", self.product_b.id), + ("prod_type", "test__engagement__product__prod_type", self.type_a.id), + ): + with self.subTest(alias=alias): + self.assertEqual(self._filtered({alias: str(value)}), self._filtered({long_form: str(value)})) + + def test_combines_with_other_filters(self): + high_a1 = set(self.scope.filter(id__in=self.findings["a1"], severity="High").values_list("id", flat=True)) + self.assertEqual(self._filtered({"engagement": str(self.eng_a1.id), "severity": "High"}), high_a1) + self.assertEqual(self._filtered({"engagement": str(self.eng_b.id), "product": str(self.product_a.id)}), set()) + + def test_non_numeric_value_is_invalid(self): + filterset = ApiFindingFilter(data={"engagement": "abc"}, queryset=self.scope) + self.assertFalse(filterset.is_valid()) + self.assertIn("engagement", filterset.errors) + + def test_api_list_is_scoped_and_counted(self): + client = APIClient() + client.force_authenticate(user=self.user) + url = reverse("finding-list") + page1 = client.get(url, {"engagement": self.eng_b.id, "limit": 3}, secure=True) + page2 = client.get(url, {"engagement": self.eng_b.id, "limit": 3, "offset": 3}, secure=True) + self.assertEqual(page1.status_code, 200, page1.content[:500]) + self.assertEqual(page1.data["count"], 4) + self.assertEqual(page2.data["count"], 4) + self.assertIsNone(page2.data["next"]) + ids = {f["id"] for f in page1.data["results"]} | {f["id"] for f in page2.data["results"]} + self.assertEqual(ids, self.findings["b"]) + # A parameter that is not a filter is still ignored rather than rejected. + unknown = client.get(url, {"engagement": self.eng_b.id, "not_a_filter": "1", "limit": 100}, secure=True) + self.assertEqual({f["id"] for f in unknown.data["results"]}, self.findings["b"]) + + def test_api_does_not_widen_for_a_member_of_one_product(self): + # A Reader of product A only. The short names narrow what the caller may already see, so asking for + # product B's engagement, product or product type is an empty page, while A's own ids still answer. + reader = Dojo_User.objects.create(username="rel_filter_reader") + Product_Member.objects.create(user=reader, product=self.product_a, role=Role.objects.get(id=Roles.Reader)) + self.product_a.authorized_users.add(reader) + client = APIClient() + client.force_authenticate(user=reader) + url = reverse("finding-list") + for params in ( + {"engagement": self.eng_b.id}, + {"product": self.product_b.id}, + {"prod_type": self.type_b.id}, + ): + with self.subTest(params=params): + response = client.get(url, {**params, "limit": 100}, secure=True) + self.assertEqual(response.status_code, 200, response.content[:500]) + self.assertEqual(response.data["count"], 0) + self.assertEqual(response.data["results"], []) + own = client.get(url, {"engagement": self.eng_a2.id, "limit": 100}, secure=True) + self.assertEqual(own.data["count"], len(self.findings["a2"])) + self.assertEqual({f["id"] for f in own.data["results"]}, self.findings["a2"]) diff --git a/unittests/test_apiv2_list_query_scaling.py b/unittests/test_apiv2_list_query_scaling.py index 0507e5c5fd4..55b886e8956 100644 --- a/unittests/test_apiv2_list_query_scaling.py +++ b/unittests/test_apiv2_list_query_scaling.py @@ -24,6 +24,8 @@ from rest_framework.test import APIClient from dojo.api_v2.prefetch.prefetcher import _Prefetcher # noqa: PLC2701 -- the class under test +from dojo.finding.api.views import FindingViewSet +from dojo.finding.helper import save_vulnerability_ids from dojo.models import ( Dojo_User, Engagement, @@ -36,6 +38,7 @@ Test_Type, ) from dojo.notes.api.serializer import NoteSerializer +from dojo.vulnerability.queries import vulnerability_id_prefetch from .dojo_test_case import DojoAPITestCase @@ -95,11 +98,6 @@ def _assert_flat(self, user, url, params, add_row, small=2, large=6): for idx in range(small, large): add_row(idx) large_sqls, payload = self._count(user, url, params) - # vulnerability_ids still reads per row here. A plugin that extends this queryset (Pro) adds its own - # vulnerability_id_prefetch(), and a second Prefetch on the same lookup raises, so it is left out of the - # count rather than prefetched in this queryset. - small_sqls = [sql for sql in small_sqls if "dojo_findingvulnerabilityreference" not in sql] - large_sqls = [sql for sql in large_sqls if "dojo_findingvulnerabilityreference" not in sql] grown = _shapes(large_sqls) - _shapes(small_sqls) self.assertEqual( len(small_sqls), len(large_sqls), @@ -116,6 +114,8 @@ def _add_finding(self, idx): public = Notes.objects.create(entry=f"public {idx}", author=self.admin) private = Notes.objects.create(entry=f"private {idx}", author=self.admin, private=True) finding.notes.add(public, private) + # Two ids, so the page has vulnerability ids to load and an order to keep + save_vulnerability_ids(finding, [f"CVE-2026-{idx:04d}", f"GHSA-list-scaling-{idx}"]) return finding def _add_engagement(self, idx): @@ -130,6 +130,33 @@ def test_findings_list_is_flat_for_a_member_with_notes(self): for row in payload["results"]: self.assertEqual(["public"], [note["entry"].split()[0] for note in row["notes"]]) + def test_findings_list_vulnerability_ids_are_flat(self): + payload = self._assert_flat( + self.admin, "/api/v2/findings/", {"limit": 100, "test": self.test.id}, self._add_finding, + ) + by_id = {row["id"]: row["vulnerability_ids"] for row in payload["results"]} + # _add_finding creates them in idx order + ids = list(Finding.objects.filter(test=self.test).order_by("id").values_list("id", flat=True)) + self.assertEqual(6, len(ids)) + for idx, finding_id in enumerate(ids): + self.assertEqual( + [{"vulnerability_id": f"CVE-2026-{idx:04d}"}, {"vulnerability_id": f"GHSA-list-scaling-{idx}"}], + by_id[finding_id], + ) + + def test_findings_list_with_a_queryset_that_already_prefetches_vulnerability_ids(self): + """A subclass (a plugin) that adds its own vulnerability_id_prefetch() keeps working and stays flat.""" + original = FindingViewSet.get_queryset + + def get_queryset(viewset): + return original(viewset).prefetch_related(vulnerability_id_prefetch()) + + with patch.object(FindingViewSet, "get_queryset", get_queryset): + payload = self._assert_flat( + self.admin, "/api/v2/findings/", {"limit": 100, "test": self.test.id}, self._add_finding, + ) + self.assertEqual(6, sum(1 for row in payload["results"] if len(row["vulnerability_ids"]) == 2)) + def test_findings_list_prefetch_test_is_flat_for_a_member(self): payload = self._assert_flat( self.member, "/api/v2/findings/", {"limit": 100, "prefetch": "test"}, self._add_finding, diff --git a/unittests/test_excess_duplicate_handler.py b/unittests/test_excess_duplicate_handler.py new file mode 100644 index 00000000000..5a6f344e1b9 --- /dev/null +++ b/unittests/test_excess_duplicate_handler.py @@ -0,0 +1,47 @@ +""" +A plugin can set excess duplicates aside instead of deleting them. + +``register_excess_duplicate_handler`` installs one handler for the chunks the excess-duplicate +task removes. When the handler returns True it has dealt with the chunk and nothing is deleted; +with no handler, or one that returns False, the chunk is deleted as before. Only the hand-off is +tested here, so ``bulk_delete_findings`` is mocked and no database is touched. +""" +from unittest.mock import Mock, patch + +from django.test import SimpleTestCase + +from dojo import tasks + + +class ExcessDuplicateHandlerTest(SimpleTestCase): + def setUp(self): + self.addCleanup(tasks.register_excess_duplicate_handler, None) + self.chunk = Mock(name="chunk") + + @patch("dojo.tasks.bulk_delete_findings") + def test_without_a_handler_the_chunk_is_deleted(self, delete): + tasks.register_excess_duplicate_handler(None) + + tasks._remove_excess_duplicates(self.chunk) + + delete.assert_called_once_with(self.chunk, order_desc=True) + + @patch("dojo.tasks.bulk_delete_findings") + def test_a_handler_that_takes_the_chunk_stops_the_delete(self, delete): + handler = Mock(return_value=True) + tasks.register_excess_duplicate_handler(handler) + + tasks._remove_excess_duplicates(self.chunk) + + handler.assert_called_once_with(self.chunk) + delete.assert_not_called() + + @patch("dojo.tasks.bulk_delete_findings") + def test_a_handler_that_declines_leaves_the_delete_in_place(self, delete): + handler = Mock(return_value=False) + tasks.register_excess_duplicate_handler(handler) + + tasks._remove_excess_duplicates(self.chunk) + + handler.assert_called_once_with(self.chunk) + delete.assert_called_once_with(self.chunk, order_desc=True) diff --git a/unittests/test_finding_tag_filters.py b/unittests/test_finding_tag_filters.py index cc4e6cb909c..caa12799d92 100644 --- a/unittests/test_finding_tag_filters.py +++ b/unittests/test_finding_tag_filters.py @@ -7,11 +7,14 @@ of SQL per page. Each filter now tests membership with Exists() on the tags through table. These tests pin the result sets to the old JOIN semantics and pin the SQL shape. """ +from unittest import mock from urllib.parse import parse_qsl from django.contrib.auth.models import User +from django.db import connection from django.db.models import Count from django.test import override_settings +from django.test.utils import CaptureQueriesContext from django.urls import reverse from django.utils import timezone from django_filters import BooleanFilter, CharFilter @@ -144,6 +147,27 @@ def test_tags_and(self): for value in ["alpha", "alpha,beta", "alpha,beta,gamma", "beta,gamma-ray", "alpha,alpha", "ALPHA,beta", "alpha,"]: self.assert_same_as_legacy({"tags__and": value}) + def test_unknown_tags_through_the_filterset(self): + # A name that is no tag: tags__and cannot be satisfied, not_tags excludes nothing. + for params, expected in [ + ({"tags__and": "no-such-tag"}, []), + ({"tags__and": "alpha,no-such-tag"}, []), + ({"test__engagement__tags__and": "shared,no-such-tag"}, []), + ({"not_tags": "no-such-tag"}, _ids(self.base)), + ({"not_tags": "no-such-tag,beta"}, _ids(self.base.exclude(tags__name="beta"))), + ]: + with self.subTest(params=params): + self.assertEqual(_ids(self.filtered(**params)), expected) + self.assert_same_as_legacy(params) + + def test_tags_and_reads_the_tag_table_once(self): + # Every AND-ed name is resolved in one read, however many are listed; the page query then names ids. + tag_table = Finding._meta.get_field("tags").related_model._meta.db_table + with CaptureQueriesContext(connection) as queries: + ids = _ids(self.filtered(tags__and="alpha,beta,gamma")) + self.assertEqual(sum(f'FROM "{tag_table}"' in query["sql"] for query in queries.captured_queries), 1) + self.assertEqual(ids, _ids(self.filtered(LegacyJoinTagFilter, tags__and="alpha,beta,gamma"))) + def test_has_tags(self): for value in ["true", "false"]: self.assert_same_as_legacy({"has_tags": value}) @@ -402,3 +426,55 @@ def test_related_paths_resolve(self): (Test, "engagement__tags__name"), (Engagement, "product__tags__name"), (Product, "tags__name")]: with self.subTest(model=model.__name__, field=field): self.assertIsNotNone(list(model.objects.filter(tag_exists(model, field, "in", ["x"])))) + + def _finding_with_tags(self, name, tags): + finding = Finding.objects.create( + title=f"{name} finding", severity="High", reporter=User.objects.create(username=f"{name}-reporter"), + test=Test.objects.create( + engagement=Engagement.objects.create( + name=f"{name} engagement", target_start=timezone.now(), target_end=timezone.now(), + product=Product.objects.create( + name=f"{name} asset", description="x", prod_type=Product_Type.objects.create(name=f"{name} org"))), + test_type=Test_Type.objects.get_or_create(name=f"{name} scan")[0], + target_start=timezone.now(), target_end=timezone.now())) + finding.tags = tags + finding.save() + return finding + + def test_matches_on_tag_ids_not_on_names(self): + # The planner can only see how common a tag is when the query names its id. Joined by name, it + # assumed the average tag and mis-planned popular ones (a 20 s COUNT instead of 2.5 s). + finding = self._finding_with_tags("Tag id", ["tag-id-one", "tag-id-two"]) + tag_model = Finding._meta.get_field("tags").related_model + wanted = tag_model.objects.get(name="tag-id-one").pk + for lookup, value in [("exact", "tag-id-one"), ("in", ["tag-id-one"]), ("icontains", "ID-ONE")]: + with self.subTest(lookup=lookup): + qs = Finding.objects.filter(tag_exists(Finding, "tags__name", lookup, value)) + sql = str(qs.query) + subquery = sql[sql.index("EXISTS"):] + self.assertNotIn(tag_model._meta.db_table, subquery) + self.assertIn(f"IN ({wanted})", subquery) + self.assertEqual(list(qs), [finding]) + self.assertNotIn(finding, Finding.objects.exclude(tag_exists(Finding, "tags__name", lookup, value))) + + def test_an_unknown_tag_matches_nothing_and_its_negation_everything(self): + everything = Finding.objects.count() + self.assertEqual(Finding.objects.filter(tag_exists(Finding, "tags__name", "in", ["no-such-tag"])).count(), 0) + self.assertEqual( + Finding.objects.filter(~tag_exists(Finding, "tags__name", "in", ["no-such-tag"])).count(), everything) + + def test_a_lookup_matching_many_tags_keeps_the_name_join(self): + tagged = self._finding_with_tags("Many tags", ["many-tags-0"]) + untagged = self._finding_with_tags("Few tags", ["other-tag"]) + tag_model = Finding._meta.get_field("tags").related_model + tag_model.objects.bulk_create( + [tag_model(name=f"many-tags-{n}", slug=f"many-tags-{n}") for n in range(1, 4)]) + with mock.patch("dojo.filters.TAG_ID_LITERAL_LIMIT", 3): + condition = tag_exists(Finding, "tags__name", "icontains", "many-tags") + matched = Finding.objects.filter(condition) + self.assertIn(tag_model._meta.db_table, str(matched.query)[str(matched.query).index("EXISTS"):]) + # The join form matches the same rows the id form would. + self.assertEqual(list(matched), [tagged]) + excluded = set(Finding.objects.exclude(condition)) + self.assertNotIn(tagged, excluded) + self.assertIn(untagged, excluded) diff --git a/unittests/test_metrics_queries.py b/unittests/test_metrics_queries.py index 94ec80f2035..81f4a15242f 100644 --- a/unittests/test_metrics_queries.py +++ b/unittests/test_metrics_queries.py @@ -337,12 +337,12 @@ def test_endpoint_queries(self, mock_now): self.assertCountEqual( endpoint_queries["all"].values(), [ - {"id": 1, "date": date(2020, 7, 1), "last_modified": datetime(2020, 7, 1, 17, 45, 39, 791907, tzinfo=zoneinfo.ZoneInfo("UTC")), "mitigated": False, "mitigated_time": None, "mitigated_by_id": None, "false_positive": False, "out_of_scope": False, "risk_accepted": False, "endpoint_id": 2, "finding_id": 2}, - {"id": 3, "date": date(2020, 7, 1), "last_modified": datetime(2020, 7, 1, 17, 45, 39, 791907, tzinfo=zoneinfo.ZoneInfo("UTC")), "mitigated": False, "mitigated_time": None, "mitigated_by_id": None, "false_positive": True, "out_of_scope": False, "risk_accepted": False, "endpoint_id": 5, "finding_id": 228}, - {"id": 4, "date": date(2020, 7, 1), "last_modified": datetime(2020, 7, 1, 17, 45, 39, 791907, tzinfo=zoneinfo.ZoneInfo("UTC")), "mitigated": False, "mitigated_time": None, "mitigated_by_id": None, "false_positive": False, "out_of_scope": True, "risk_accepted": False, "endpoint_id": 5, "finding_id": 229}, - {"id": 5, "date": date(2020, 7, 1), "last_modified": datetime(2020, 7, 1, 17, 45, 39, 791907, tzinfo=zoneinfo.ZoneInfo("UTC")), "mitigated": False, "mitigated_time": None, "mitigated_by_id": None, "false_positive": False, "out_of_scope": False, "risk_accepted": True, "endpoint_id": 5, "finding_id": 230}, - {"id": 7, "date": date(2020, 7, 1), "last_modified": datetime(2020, 7, 1, 17, 45, 39, 791907, tzinfo=zoneinfo.ZoneInfo("UTC")), "mitigated": False, "mitigated_time": None, "mitigated_by_id": None, "false_positive": False, "out_of_scope": False, "risk_accepted": False, "endpoint_id": 7, "finding_id": 227}, - {"id": 8, "date": date(2020, 7, 1), "last_modified": datetime(2020, 7, 1, 17, 45, 39, 791907, tzinfo=zoneinfo.ZoneInfo("UTC")), "mitigated": False, "mitigated_time": None, "mitigated_by_id": None, "false_positive": False, "out_of_scope": False, "risk_accepted": False, "endpoint_id": 8, "finding_id": 231}, + {"id": 1, "date": date(2020, 7, 1), "last_modified": datetime(2020, 7, 1, 17, 45, 39, 791907, tzinfo=zoneinfo.ZoneInfo("UTC")), "mitigated": False, "mitigated_time": None, "mitigated_by_id": None, "false_positive": False, "out_of_scope": False, "risk_accepted": False, "endpoint_id": 2, "finding_id": 2, "archived_at": None, "archive_batch": None}, + {"id": 3, "date": date(2020, 7, 1), "last_modified": datetime(2020, 7, 1, 17, 45, 39, 791907, tzinfo=zoneinfo.ZoneInfo("UTC")), "mitigated": False, "mitigated_time": None, "mitigated_by_id": None, "false_positive": True, "out_of_scope": False, "risk_accepted": False, "endpoint_id": 5, "finding_id": 228, "archived_at": None, "archive_batch": None}, + {"id": 4, "date": date(2020, 7, 1), "last_modified": datetime(2020, 7, 1, 17, 45, 39, 791907, tzinfo=zoneinfo.ZoneInfo("UTC")), "mitigated": False, "mitigated_time": None, "mitigated_by_id": None, "false_positive": False, "out_of_scope": True, "risk_accepted": False, "endpoint_id": 5, "finding_id": 229, "archived_at": None, "archive_batch": None}, + {"id": 5, "date": date(2020, 7, 1), "last_modified": datetime(2020, 7, 1, 17, 45, 39, 791907, tzinfo=zoneinfo.ZoneInfo("UTC")), "mitigated": False, "mitigated_time": None, "mitigated_by_id": None, "false_positive": False, "out_of_scope": False, "risk_accepted": True, "endpoint_id": 5, "finding_id": 230, "archived_at": None, "archive_batch": None}, + {"id": 7, "date": date(2020, 7, 1), "last_modified": datetime(2020, 7, 1, 17, 45, 39, 791907, tzinfo=zoneinfo.ZoneInfo("UTC")), "mitigated": False, "mitigated_time": None, "mitigated_by_id": None, "false_positive": False, "out_of_scope": False, "risk_accepted": False, "endpoint_id": 7, "finding_id": 227, "archived_at": None, "archive_batch": None}, + {"id": 8, "date": date(2020, 7, 1), "last_modified": datetime(2020, 7, 1, 17, 45, 39, 791907, tzinfo=zoneinfo.ZoneInfo("UTC")), "mitigated": False, "mitigated_time": None, "mitigated_by_id": None, "false_positive": False, "out_of_scope": False, "risk_accepted": False, "endpoint_id": 8, "finding_id": 231, "archived_at": None, "archive_batch": None}, ], ) self.assertSequenceEqual( @@ -351,7 +351,7 @@ def test_endpoint_queries(self, mock_now): ) self.assertSequenceEqual( endpoint_queries["accepted"].values(), - [{"id": 5, "date": date(2020, 7, 1), "last_modified": datetime(2020, 7, 1, 17, 45, 39, 791907, tzinfo=zoneinfo.ZoneInfo("UTC")), "mitigated": False, "mitigated_time": None, "mitigated_by_id": None, "false_positive": False, "out_of_scope": False, "risk_accepted": True, "endpoint_id": 5, "finding_id": 230}], + [{"id": 5, "date": date(2020, 7, 1), "last_modified": datetime(2020, 7, 1, 17, 45, 39, 791907, tzinfo=zoneinfo.ZoneInfo("UTC")), "mitigated": False, "mitigated_time": None, "mitigated_by_id": None, "false_positive": False, "out_of_scope": False, "risk_accepted": True, "endpoint_id": 5, "finding_id": 230, "archived_at": None, "archive_batch": None}], ) self.assertSequenceEqual( list(endpoint_queries["accepted_count"].values()), diff --git a/unittests/test_query_hooks.py b/unittests/test_query_hooks.py new file mode 100644 index 00000000000..a394f43b20b --- /dev/null +++ b/unittests/test_query_hooks.py @@ -0,0 +1,47 @@ +from dojo.models import Engagement, Finding, Product, Product_Type, Test +from dojo.query_hooks import _DEFAULT_QUERYSET_FILTERS, register_default_queryset_filter # noqa: PLC2701 +from unittests.dojo_test_case import DojoTestCase, versioned_fixtures + + +@versioned_fixtures +class TestDefaultQuerysetHooks(DojoTestCase): + fixtures = ["dojo_testdata.json"] + + def setUp(self): + self._saved = {label: dict(funcs) for label, funcs in _DEFAULT_QUERYSET_FILTERS.items()} + _DEFAULT_QUERYSET_FILTERS.clear() + + def tearDown(self): + _DEFAULT_QUERYSET_FILTERS.clear() + _DEFAULT_QUERYSET_FILTERS.update(self._saved) + + def test_nothing_registered_leaves_querysets_unchanged(self): + for model in (Product_Type, Product, Engagement, Test, Finding): + with self.subTest(model=model.__name__): + self.assertEqual(model.objects.count(), model.unfiltered.count()) + + def test_hooked_manager_is_the_default_manager(self): + for model in (Product_Type, Product, Engagement, Test, Finding): + with self.subTest(model=model.__name__): + self.assertIs(model._default_manager, model.objects) + self.assertIsNot(model._base_manager, model.objects) + + def test_registered_filter_applies_to_objects_get_and_reverse_relations(self): + finding = Finding.objects.filter(test__isnull=False).first() + register_default_queryset_filter("dojo.Finding", "hide-one", lambda qs: qs.exclude(pk=finding.pk)) + + with self.subTest("objects"): + self.assertFalse(Finding.objects.filter(pk=finding.pk).exists()) + with self.subTest("get"), self.assertRaises(Finding.DoesNotExist): + Finding.objects.get(pk=finding.pk) + with self.subTest("reverse relation"): + self.assertNotIn(finding.pk, finding.test.finding_set.values_list("pk", flat=True)) + with self.subTest("unfiltered bypasses"): + self.assertTrue(Finding.unfiltered.filter(pk=finding.pk).exists()) + with self.subTest("base manager bypasses"): + self.assertTrue(Finding._base_manager.filter(pk=finding.pk).exists()) + + def test_registering_the_same_key_replaces_the_filter(self): + register_default_queryset_filter("dojo.Product", "k", lambda qs: qs.none()) + register_default_queryset_filter("dojo.Product", "k", lambda qs: qs) + self.assertEqual(Product.objects.count(), Product.unfiltered.count()) diff --git a/unittests/test_surface_location_data.py b/unittests/test_surface_location_data.py new file mode 100644 index 00000000000..57af2f5ac57 --- /dev/null +++ b/unittests/test_surface_location_data.py @@ -0,0 +1,65 @@ +from dojo.tools.locations import LocationData +from unittests.dojo_test_case import DojoTestCase + + +class TestSurfaceLocationData(DojoTestCase): + def test_domain_factory_emits_the_name_and_its_apex(self): + location = LocationData.domain(fqdn="WWW.Example.com.", apex="example.com") + self.assertEqual("domain", location.type) + # Reported as given: normalizing the name is the consumer's job, so every parser + # emits the same shape whatever case or trailing dot its tool used. + self.assertEqual({"fqdn": "WWW.Example.com.", "apex": "example.com"}, location.data) + + def test_domain_factory_defaults_the_apex_to_empty(self): + self.assertEqual({"fqdn": "api.example.com", "apex": ""}, LocationData.domain(fqdn="api.example.com").data) + + def test_ip_factory_emits_exactly_five_keys(self): + location = LocationData.ip(address="192.0.2.10") + self.assertEqual("ip", location.type) + self.assertEqual( + {"address": "192.0.2.10", "asn": None, "asn_organization": "", "network": "", "provider": ""}, + location.data, + ) + + def test_ip_factory_carries_what_the_tool_knows(self): + location = LocationData.ip( + address="2001:db8::1", + asn=64500, + asn_organization="Example Net", + network="2001:db8::/32", + provider="aws", + ) + self.assertEqual(64500, location.data["asn"]) + self.assertEqual("2001:db8::/32", location.data["network"]) + self.assertEqual("aws", location.data["provider"]) + + def test_cert_factory_emits_exactly_seven_keys(self): + fingerprint = "ab" * 32 + location = LocationData.cert( + sha256=fingerprint, + subject_cn="example.com", + sans=["example.com", "www.example.com"], + ) + self.assertEqual("cert", location.type) + self.assertEqual( + { + "sha256": fingerprint, + "subject_cn": "example.com", + "issuer": "", + "serial_number": "", + "not_before": "", + "not_after": "", + "sans": ["example.com", "www.example.com"], + }, + location.data, + ) + + def test_cert_factory_copies_the_names_it_is_given(self): + sans = ["example.com"] + location = LocationData.cert(sha256="ab" * 32, sans=sans) + sans.append("later.example.com") + self.assertEqual(["example.com"], location.data["sans"]) + self.assertEqual([], LocationData.cert(sha256="ab" * 32).data["sans"]) + + def test_cert_factory_reads_one_name_as_one_name(self): + self.assertEqual(["example.com"], LocationData.cert(sha256="ab" * 32, sans="example.com").data["sans"]) diff --git a/unittests/tools/test_qualys_parser.py b/unittests/tools/test_qualys_parser.py index 060b6b9fcc0..a4efaa049a4 100644 --- a/unittests/tools/test_qualys_parser.py +++ b/unittests/tools/test_qualys_parser.py @@ -1,4 +1,8 @@ import datetime +import io +import re +from unittest import mock +from xml.etree import ElementPath from django.test import override_settings @@ -7,8 +11,63 @@ from unittests.dojo_test_case import DojoTestCase, get_unit_tests_scans_path +def _report_with_hosts(count): + """The sample report with its hosts repeated to ``count`` hosts, each with its own address.""" + sample = (get_unit_tests_scans_path("qualys") / "Qualys_Sample_Report.xml").read_text(encoding="utf-8") + head, rest = sample.split("", 1) + hosts, tail = rest.split("", 1) + blocks = re.findall(r".*?", hosts, flags=re.DOTALL) + repeated = [ + re.sub(r"[^<]*", f"192.0.2.{index + 1}", blocks[index % len(blocks)], count=1) + for index in range(count) + ] + return _named(f"{head}{''.join(repeated)}{tail}") + + +def _named(content): + report = io.BytesIO(content.encode()) + report.name = "report.xml" + return report + + class TestQualysParser(DojoTestCase): + def test_glossary_is_searched_once_per_report_not_once_per_finding(self): + # Every finding used to look its QID up with a search from the document root, which walks + # all the hosts before it reaches the glossary: the parse was findings x document size. + # Every find, findall, findtext and iterfind, on an element or on the whole tree, resolves its + # path through ElementPath.iterfind, so a per-QID lookup from any starting point is counted. + searches = [] + original = ElementPath.iterfind + + def counting(elem, path, namespaces=None): + if "VULN_DETAILS" in path: + searches.append(path) + return original(elem, path, namespaces) + + patcher = mock.patch.object(ElementPath, "iterfind", counting) + patcher.start() + self.addCleanup(patcher.stop) + + small = QualysParser().get_findings(_report_with_hosts(2), Test()) + small_searches = len(searches) + searches.clear() + large = QualysParser().get_findings(_report_with_hosts(14), Test()) + + self.assertGreater(len(large), len(small)) + self.assertEqual(small_searches, len(searches)) + + def test_repeated_glossary_entry_keeps_the_first(self): + sample = (get_unit_tests_scans_path("qualys") / "Qualys_Sample_Report.xml").read_text(encoding="utf-8") + details = re.search(r".*?", sample, flags=re.DOTALL).group(0) + shadow = re.sub(r"<!\[CDATA\[[^\]]*\]\]>", "<![CDATA[Shadowed]]>", details, count=1) + report = _named(sample.replace("", f"{shadow}", 1)) + + titles = {finding.title for finding in QualysParser().get_findings(report, Test())} + + self.assertIn("QID-6 | DNS Host Name", titles) + self.assertNotIn("QID-6 | Shadowed", titles) + @override_settings(USE_FIRST_SEEN=True) def test_parse_file_with_no_vuln_has_no_findings_first_seen(self): self.parse_file_with_no_vuln_has_no_findings() @@ -87,6 +146,31 @@ def parse_file_with_no_vuln_has_no_findings_csv(self): self.validate_locations(findings) self.assertEqual(0, len(findings)) + def test_finding_with_missing_glossary_entry_is_skipped(self): + # A QID that matches no GLOSSARY VULN_DETAILS has neither severity nor title to + # import; the parser used to raise AttributeError on it and abort the whole import. + sample = (get_unit_tests_scans_path("qualys") / "Qualys_Sample_Report.xml").read_text(encoding="utf-8") + without_qid_6_entry = re.sub( + r".*?", + "", + sample, + count=1, + flags=re.DOTALL, + ) + report = io.BytesIO(without_qid_6_entry.encode()) + report.name = "report.xml" + + with self.assertLogs("dojo.tools.qualys.parser", level="WARNING") as logged: + findings = QualysParser().get_findings(report, Test()) + + # Seven findings report QID 6 in the full sample's 301; each is skipped + # while everything else still imports. + self.validate_locations(findings) + self.assertEqual(294, len(findings)) + titles = {finding.title for finding in findings} + self.assertNotIn("QID-6 | DNS Host Name", titles) + self.assertTrue(any("qid_6" in message for message in logged.output)) + @override_settings(USE_FIRST_SEEN=True) def test_parse_file_with_multiple_vuln_has_multiple_findings_csv_first_seen(self): finding = self.parse_file_with_multiple_vuln_has_multiple_findings_csv()