Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/content/releases/os_upgrading/2.58.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: 'Upgrading to DefectDojo Version 2.58.x'
toc_hide: true
weight: -20260504
description: 'Breaking change: parsers no longer set Finding.service directly'
---

## Breaking Change: Parsers No Longer Set `Finding.service`

Starting with DefectDojo 2.58.x, parsers no longer set the `service` field directly on findings.

### Why this is a breaking change

Whenever parsers set the `service` field on findings, this breaks `close_old_findings` functionality.

The reason is that import and reimport differ in a way that import uses the `service` field, however reimport does not include a `service` value. The `close_old_findings` feature only closes findings that match the service value provided in the request. As a result, findings with a non-empty parser-populated service value are not closed.

Also, if the application name changes, findings in the reimport report are no longer matched against existing findings.

### Required actions

- If your integrations relied on parser-populated `service` field, update your workflow to pass service explicitly at import/reimport time when needed.
- Review automation that depends on `close_old_findings` behavior and verify expected closure scope after upgrading.

For more information, check the [Release Notes](https://github.com/DefectDojo/django-DefectDojo/releases/tag/2.58.0).
95 changes: 95 additions & 0 deletions dojo/db_migrations/0264_clear_service_for_affected_parsers.py
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a service was supplied to these test types at import time, does that value override the value set by the parser? If so, could this migration potentially erase service fields that are from the user?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a question beyond my knowledge. Maybe @valentijnscholten has an opinion on this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The importers overwrite any fields set by the parsers:

if self.service is not None:
unsaved_finding.service = self.service

if self.service is not None:
unsaved_finding.service = self.service

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So it seems like this migration would erase data intentionally set by users...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a possibility to distinguish between the two ways?

Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import logging

from django.db import migrations
from django.db.models import Q

logger = logging.getLogger(__name__)


AFFECTED_PARSER_SCAN_TYPES = [
"Trivy Scan",
"Trivy Operator Scan",
"Hydra Scan",
"JFrog Xray API Summary Artifact Scan",
"Orca Security Alerts",
"OpenReports",
"StackHawk HawkScan",
]


def clear_service_and_rehash_findings(apps, schema_editor):
"""
Clear parser-populated service values for affected parser scan types and
recompute hash_code.

This migration only touches findings where:
- the finding belongs to an affected parser by test_type or scan_type
- service is set (not NULL and not empty)
"""
historical_finding = apps.get_model("dojo", "Finding")

affected_ids = set()
for scan_type in AFFECTED_PARSER_SCAN_TYPES:
findings = (
historical_finding.objects
.filter(
Q(test__test_type__name=scan_type)
| Q(test__scan_type=scan_type),
)
.exclude(service__isnull=True)
.exclude(service="")
)
count = findings.count()
if count:
logger.warning(
"Identified %d findings with parser-populated service for scan type '%s'",
count,
scan_type,
)
affected_ids.update(findings.values_list("id", flat=True))

if not affected_ids:
logger.warning("No findings found for parser service cleanup migration")
return

# Use live model here to access compute_hash_code() and save() behavior.
from dojo.models import Finding # noqa: PLC0415

migrated = 0
for finding in (
Finding.objects
.filter(id__in=affected_ids)
.select_related("test", "test__test_type")
.iterator(chunk_size=200)
):
finding.service = None
finding.hash_code = finding.compute_hash_code()
finding.save(
dedupe_option=False,
rules_option=False,
product_grading_option=False,
issue_updater_option=False,
push_to_jira=False,
)
migrated += 1

logger.warning(
"Parser service cleanup migration updated %d findings (service cleared, hash_code recomputed)",
migrated,
)


def noop_reverse(apps, schema_editor):
# Intentionally irreversible: previous parser-populated service values are not recoverable.
pass


class Migration(migrations.Migration):

dependencies = [
("dojo", "0263_language_type_unique_language"),
]

operations = [
migrations.RunPython(clear_service_and_rehash_findings, noop_reverse),
]
2 changes: 1 addition & 1 deletion dojo/tools/hydra/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def __extract_finding(
+ password,
static_finding=False,
dynamic_finding=True,
service=metadata.service_type,
component_name=metadata.service_type,
)
if settings.V3_FEATURE_LOCATIONS:
finding.unsaved_locations = [LocationData.url(host=host, port=port)]
Expand Down
7 changes: 3 additions & 4 deletions dojo/tools/jfrog_xray_api_summary_artifact/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ def get_items(self, tree, test):
service = decode_service(artifact_general["name"])
item = get_item(
node,
str(service),
test,
artifact.name,
str(service),
artifact.version,
artifact.sha256,
)
Expand All @@ -56,9 +56,9 @@ def get_items(self, tree, test):
# Retrieve the findings of the affected 1st level component (Artifact)
def get_item(
vulnerability,
service,
test,
artifact_name,
artifact_service,
artifact_version,
artifact_sha256,
):
Expand Down Expand Up @@ -114,7 +114,6 @@ def get_item(

finding = Finding(
vuln_id_from_tool=vuln_id_from_tool,
service=service,
title=vulnerability["summary"],
cwe=cwe,
cvssv3=cvssv3,
Expand All @@ -126,7 +125,7 @@ def get_item(
+ vulnerability["description"],
test=test,
file_path=impact_paths[0],
component_name=artifact_name,
component_name=artifact_name or artifact_service,
component_version=artifact_version,
static_finding=True,
dynamic_finding=False,
Expand Down
3 changes: 2 additions & 1 deletion dojo/tools/openreports/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

DESCRIPTION_TEMPLATE = """{message}

**Service:** {service}
**Category:** {category}
**Policy:** {policy}
**Result:** {result}
Expand Down Expand Up @@ -218,6 +219,7 @@ def _create_finding_from_result(self, test, result, service_name, report_name, r
# Create description
description = DESCRIPTION_TEMPLATE.format(
message=message,
service=service_name,
category=category,
policy=policy,
result=result_status,
Expand Down Expand Up @@ -250,7 +252,6 @@ def _create_finding_from_result(self, test, result, service_name, report_name, r
mitigation=mitigation,
component_name=pkg_name,
component_version=installed_version,
service=service_name,
active=active,
verified=verified,
static_finding=True,
Expand Down
1 change: 0 additions & 1 deletion dojo/tools/orca_security/csv_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,6 @@ def parse(self, content):
severity_justification=build_severity_justification(orca_score_raw),
static_finding=True, # CSPM scan data is static analysis
dynamic_finding=False,
service=source or None, # Source identifies the cloud resource/service
component_name=inventory_name or None, # Inventory is the specific resource
date=parse_date(created_at),
)
Expand Down
1 change: 0 additions & 1 deletion dojo/tools/orca_security/json_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ def parse(self, content):
severity_justification=build_severity_justification(orca_score),
static_finding=True, # CSPM scan data is static analysis
dynamic_finding=False,
service=source or None, # Source identifies the cloud resource/service
component_name=inventory_name or None, # Inventory is the specific resource
date=parse_date(created_at),
)
Expand Down
2 changes: 0 additions & 2 deletions dojo/tools/stackhawk/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ def __init__(self, completed_scan):
self.component_version = completed_scan["scan"]["env"]
self.static_finding = False
self.dynamic_finding = True
self.service = completed_scan["scan"]["application"]


class StackHawkParser:
Expand Down Expand Up @@ -106,7 +105,6 @@ def __extract_finding(
dynamic_finding=metadata.dynamic_finding,
vuln_id_from_tool=raw_finding["pluginId"],
nb_occurences=raw_finding["totalCount"],
service=metadata.service,
false_p=are_all_endpoints_false_positive,
risk_accepted=are_all_endpoints_risk_accepted,
)
Expand Down
22 changes: 14 additions & 8 deletions dojo/tools/trivy/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,28 +33,34 @@
**Type:** {type}
**Fixed version:** {fixed_version}

{description_text}
{service_text}{description_text}
"""

MISC_DESCRIPTION_TEMPLATE = """**Target:** {target}
**Type:** {type}

{description}
{service_text}{description}
{message}
"""

SECRET_DESCRIPTION_TEMPLATE = """{title}
**Category:** {category}
**Match:** {match}
{service_text}**Match:** {match}
""" # noqa: S105

LICENSE_DESCRIPTION_TEMPLATE = """{title}
**Category:** {category}
**Package:** {package}
{service_text}**Package:** {package}
"""


class TrivyParser:
@staticmethod
def _service_text(service_name):
if service_name:
return f"**Service:** {service_name}\n"
return ""

def get_scan_types(self):
return ["Trivy Scan"]

Expand Down Expand Up @@ -319,6 +325,7 @@ def get_result_items(self, test, results, service_name=None, artifact_name=""):
title=vuln.get("Title", ""),
target=target,
type=vul_type,
service_text=self._service_text(service_name),
fixed_version=mitigation,
description_text=vuln.get("Description", ""),
)
Expand All @@ -341,7 +348,6 @@ def get_result_items(self, test, results, service_name=None, artifact_name=""):
static_finding=True,
dynamic_finding=False,
fix_available=fix_available,
service=service_name,
**status_fields,
)
finding.unsaved_tags = [vul_type, target_class]
Expand Down Expand Up @@ -377,6 +383,7 @@ def get_result_items(self, test, results, service_name=None, artifact_name=""):
description = MISC_DESCRIPTION_TEMPLATE.format(
target=target_target,
type=misc_type,
service_text=self._service_text(service_name),
description=misc_description,
message=misc_message,
)
Expand All @@ -400,7 +407,6 @@ def get_result_items(self, test, results, service_name=None, artifact_name=""):
fix_available=True,
static_finding=True,
dynamic_finding=False,
service=service_name,
)
if misc_avdid:
finding.unsaved_vulnerability_ids = []
Expand All @@ -420,6 +426,7 @@ def get_result_items(self, test, results, service_name=None, artifact_name=""):
description = SECRET_DESCRIPTION_TEMPLATE.format(
title=secret_title,
category=secret_category,
service_text=self._service_text(service_name),
match=secret_match,
)
severity = TRIVY_SEVERITIES[secret_severity]
Expand All @@ -434,7 +441,6 @@ def get_result_items(self, test, results, service_name=None, artifact_name=""):
static_finding=True,
dynamic_finding=False,
fix_available=True,
service=service_name,
)
finding.unsaved_tags = [target_class]
items.append(finding)
Expand All @@ -453,6 +459,7 @@ def get_result_items(self, test, results, service_name=None, artifact_name=""):
description = LICENSE_DESCRIPTION_TEMPLATE.format(
title=license_name,
category=license_category,
service_text=self._service_text(service_name),
package=license_pkgname,
)
severity = TRIVY_SEVERITIES[license_severity]
Expand All @@ -468,7 +475,6 @@ def get_result_items(self, test, results, service_name=None, artifact_name=""):
static_finding=True,
dynamic_finding=False,
fix_available=True,
service=service_name,
)
finding.unsaved_tags = [target_class]
items.append(finding)
Expand Down
4 changes: 0 additions & 4 deletions dojo/tools/trivy_operator/checks_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ def handle_checks(self, labels, checks, test):
resource_kind = labels.get("trivy-operator.resource.kind", "")
resource_name = labels.get("trivy-operator.resource.name", "")
container_name = labels.get("trivy-operator.container.name", "")
service = f"{resource_namespace}/{resource_kind}/{resource_name}"
if container_name:
service = f"{service}/{container_name}"
for check in checks:
check_title = check.get("title")
check_severity = TRIVY_SEVERITIES[check.get("severity")]
Expand Down Expand Up @@ -55,7 +52,6 @@ def handle_checks(self, labels, checks, test):
description=check_description,
static_finding=True,
dynamic_finding=False,
service=service,
fix_available=True,
)
finding_tags = [resource_namespace, check_category]
Expand Down
4 changes: 0 additions & 4 deletions dojo/tools/trivy_operator/secrets_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ def handle_secrets(self, labels, secrets, test):
resource_kind = labels.get("trivy-operator.resource.kind", "")
resource_name = labels.get("trivy-operator.resource.name", "")
container_name = labels.get("trivy-operator.container.name", "")
service = f"{resource_namespace}/{resource_kind}/{resource_name}"
if container_name:
service = f"{service}/{container_name}"
for secret in secrets:
secret_title = secret.get("title")
secret_category = secret.get("category")
Expand Down Expand Up @@ -52,7 +49,6 @@ def handle_secrets(self, labels, secrets, test):
file_path=secret_target,
static_finding=True,
dynamic_finding=False,
service=service,
fix_available=True,
)
if resource_namespace:
Expand Down
4 changes: 0 additions & 4 deletions dojo/tools/trivy_operator/vulnerability_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ def handle_vulns(self, labels, vulnerabilities, test):
resource_kind = labels.get("trivy-operator.resource.kind", "")
resource_name = labels.get("trivy-operator.resource.name", "")
container_name = labels.get("trivy-operator.container.name", "")
service = f"{resource_namespace}/{resource_kind}/{resource_name}"
if container_name:
service = f"{service}/{container_name}"
for vulnerability in vulnerabilities:
vuln_id = vulnerability.get("vulnerabilityID", "0")
severity = TRIVY_SEVERITIES[vulnerability.get("severity")]
Expand Down Expand Up @@ -92,7 +89,6 @@ def handle_vulns(self, labels, vulnerabilities, test):
description=description,
static_finding=True,
dynamic_finding=False,
service=service,
file_path=file_path,
publish_date=publish_date,
fix_available=fix_available,
Expand Down
Loading
Loading