Skip to content

Commit

Permalink
Merge pull request #10921 from DefectDojo/master-into-dev/2.38.2-2.39…
Browse files Browse the repository at this point in the history
….0-dev

Release: Merge back 2.38.2 into dev from: master-into-dev/2.38.2-2.39.0-dev
  • Loading branch information
Maffooch authored Sep 16, 2024
2 parents d0985b9 + ef3040a commit 28f4182
Show file tree
Hide file tree
Showing 29 changed files with 5,812 additions and 191 deletions.
9 changes: 9 additions & 0 deletions docs/content/en/integrations/parsers/file/invicti.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
title: "Invicti"
toc_hide: true
---
Vulnerabilities List - JSON report

### Sample Scan Data

Sample Invicti scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/invicti).
3 changes: 3 additions & 0 deletions docs/content/en/integrations/parsers/file/netsparker.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,8 @@ toc_hide: true
---
Vulnerabilities List - JSON report

[Netsparker has now become Invicti](https://www.invicti.com/blog/news/netsparker-is-now-invicti-signaling-a-new-era-for-modern-appsec/). Please plan to migrate automation scripts to use the [Invicti Scan](../invicti.md)

### Sample Scan Data

Sample Netsparker scans can be found [here](https://github.com/DefectDojo/django-DefectDojo/tree/master/unittests/scans/netsparker).
7 changes: 6 additions & 1 deletion dojo/api_v2/exception_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from django.core.exceptions import ValidationError
from django.db.models.deletion import RestrictedError
from rest_framework.exceptions import ParseError
from rest_framework.response import Response
from rest_framework.status import (
HTTP_400_BAD_REQUEST,
Expand All @@ -20,7 +21,11 @@ def custom_exception_handler(exc, context):
# to get the standard error response.
response = exception_handler(exc, context)

if isinstance(exc, RestrictedError):
if isinstance(exc, ParseError) and "JSON parse error" in str(exc):
response = Response()
response.status_code = HTTP_400_BAD_REQUEST
response.data = {"message": "JSON request content is malformed"}
elif isinstance(exc, RestrictedError):
# An object cannot be deleted because it has dependent objects.
response = Response()
response.status_code = HTTP_409_CONFLICT
Expand Down
2 changes: 1 addition & 1 deletion dojo/metrics/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,7 @@ def aggregate_counts_by_period(
)
desired_values += ("closed",)

return severities_by_period.values(*desired_values)
return severities_by_period.order_by("grouped_date").values(*desired_values)


def findings_by_product(
Expand Down
2 changes: 1 addition & 1 deletion dojo/settings/.settings.dist.py.sha256sum
Original file line number Diff line number Diff line change
@@ -1 +1 @@
8cd4668bdc4dec192dd5bd3fd767b87a4f6d5441ae8d4a001d2ba61c452e59aa
5885fb4d328a6468766c17c54ae2d906511102cd9c79d86273e85fb24c95791b
4 changes: 3 additions & 1 deletion dojo/settings/settings.dist.py
Original file line number Diff line number Diff line change
Expand Up @@ -1282,6 +1282,7 @@ def saml2_attrib_map_format(dict):
"AppCheck Web Application Scanner": ["title", "severity"],
"Legitify Scan": ["title", "endpoints", "severity"],
"ThreatComposer Scan": ["title", "description"],
"Invicti Scan": ["title", "description", "severity"],
}

# Override the hardcoded settings here via the env var
Expand Down Expand Up @@ -1498,14 +1499,15 @@ def saml2_attrib_map_format(dict):
"OSV Scan": DEDUPE_ALGO_HASH_CODE,
"Nosey Parker Scan": DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL_OR_HASH_CODE,
"Bearer CLI": DEDUPE_ALGO_HASH_CODE,
"Wiz Scan": DEDUPE_ALGO_HASH_CODE,
"Wiz Scan": DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL_OR_HASH_CODE,
"Deepfence Threatmapper Report": DEDUPE_ALGO_HASH_CODE,
"Kubescape JSON Importer": DEDUPE_ALGO_HASH_CODE,
"Kiuwan SCA Scan": DEDUPE_ALGO_HASH_CODE,
"Rapplex Scan": DEDUPE_ALGO_HASH_CODE,
"AppCheck Web Application Scanner": DEDUPE_ALGO_HASH_CODE,
"Legitify Scan": DEDUPE_ALGO_HASH_CODE,
"ThreatComposer Scan": DEDUPE_ALGO_UNIQUE_ID_FROM_TOOL_OR_HASH_CODE,
"Invicti Scan": DEDUPE_ALGO_HASH_CODE,
}

# Override the hardcoded settings here via the env var
Expand Down
20 changes: 11 additions & 9 deletions dojo/templates/dojo/product.html
Original file line number Diff line number Diff line change
Expand Up @@ -122,18 +122,20 @@ <h3 class="has-filters">
<i class="fa-solid fa-pen-to-square"></i> Edit Custom Fields
</a>
</li>
<li role="separator" class="divider"></li>
<li role="presentation">
<a class="" href="{% url 'add_api_scan_configuration' prod.id %}">
<i class="fa-solid fa-rectangle-list"></i> Add Scan API Configuration
</a>
</li>
{% endif %}
<li role="separator" class="divider"></li>
{% if prod|has_object_permission:"Product_API_Scan_Configuration_Edit" %}
<li role="presentation">
<a title="View API Scan configurations" href="{% url 'view_api_scan_configurations' prod.id %}">
<i class="fa-solid fa-clock-rotate-left"></i> View Scan API Configurations
</a>
<a class="" href="{% url 'add_api_scan_configuration' prod.id %}">
<i class="fa-solid fa-rectangle-list"></i> Add Scan API Configuration
</a>
</li>
{% endif %}
<li role="presentation">
<a title="View API Scan configurations" href="{% url 'view_api_scan_configurations' prod.id %}">
<i class="fa-solid fa-clock-rotate-left"></i> View Scan API Configurations
</a>
</li>
{% if system_settings.enable_product_tracking_files %}
<li role="separator" class="divider"></li>
{% if prod|has_object_permission:"Product_Tracking_Files_Add" %}
Expand Down
22 changes: 12 additions & 10 deletions dojo/templates/dojo/view_product_details.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,19 +41,21 @@ <h3 class="pull-left">{% trans "Description" %}</h3>
<i class="fa-solid fa-pen-to-square"></i>{% trans "Edit Custom Fields" %}
</a>
</li>
<li role="separator" class="divider"></li>
<li role="presentation">
<a class="" href="{% url 'add_api_scan_configuration' prod.id %}">
<i class="fa-solid fa-plus"></i>{% trans "Add API Scan Configuration" %}
</a>
</li>
{% endif %}
<li role="separator" class="divider"></li>
{% if prod|has_object_permission:"Product_API_Scan_Configuration_Add" %}
<li role="presentation">
<a title="View API Scan Configurations"
href="{% url 'view_api_scan_configurations' prod.id %}">
<i class="fa-solid fa-rectangle-list"></i>{% trans "View API Scan Configurations" %}
</a>
<a class="" href="{% url 'add_api_scan_configuration' prod.id %}">
<i class="fa-solid fa-plus"></i>{% trans "Add API Scan Configuration" %}
</a>
</li>
{% endif %}
<li role="presentation">
<a title="View API Scan Configurations"
href="{% url 'view_api_scan_configurations' prod.id %}">
<i class="fa-solid fa-rectangle-list"></i>{% trans "View API Scan Configurations" %}
</a>
</li>
{% if system_settings.enable_product_tracking_files %}
<li role="separator" class="divider"></li>
{% if prod|has_object_permission:"Product_Tracking_Files_Add" %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def extract_request_response(self, finding: Finding, value: dict[str, [str]]) ->
value.pop("Messages")
finding.unsaved_request, finding.unsaved_response = (d.strip() for d in rr_details[0])

def parse_details(self, finding: Finding, value: dict[str, Union[str, dict[str, [str]]]]) -> None:
def parse_details(self, finding: Finding, value: dict[str, Union[str, dict[str, list[str]]]]) -> None:
self.extract_request_response(finding, value)
# super's version adds everything else to the description field
return super().parse_details(finding, value)
93 changes: 73 additions & 20 deletions dojo/tools/appcheck_web_application_scanner/engines/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import cvss.parser
import dateutil.parser
from cpe import CPE
from cvss.exceptions import CVSSError
from django.core.exceptions import ImproperlyConfigured

from dojo.models import Endpoint, Finding
Expand Down Expand Up @@ -41,6 +42,35 @@ def escape_if_needed(x):
return "".join([escape_if_needed(c) for c in s])


def cvss_score_to_severity(score: float, version: int) -> str:
"""
Maps a CVSS score with a given version to a severity level.
Mapping from https://nvd.nist.gov/vuln-metrics/cvss (modified slightly to have "Info" in range [0.0, 0.1) for CVSS
v3/v4)
"""
cvss_score = float(score)
if version == 2:
if cvss_score >= 7.0:
severity = "High"
elif cvss_score >= 4.0:
severity = "Medium"
else:
severity = "Low"
else:
if cvss_score >= 9.0:
severity = "Critical"
elif cvss_score >= 7.0:
severity = "High"
elif cvss_score >= 4.0:
severity = "Medium"
elif cvss_score >= 0.1:
severity = "Low"
else:
severity = "Info"

return severity


#######
# Field parsing helper classes
#######
Expand Down Expand Up @@ -122,7 +152,6 @@ class BaseEngineParser:
* status -> active/false_p/risk_accepted (depending on value)
* cves -> unsaved_vulnerability_ids (vulnerability_ids)
* cpe -> component name/version
* cvss_vector -> severity (determined using CVSS package)
* notes -> appended to Finding description
* details -> appended to Finding description
Expand All @@ -143,7 +172,6 @@ class BaseEngineParser:
"status": Method("parse_status"),
"cves": Method("parse_cves"),
"cpe": Method("parse_components"),
"cvss_vector": Method("parse_severity"),
# These should be listed after the 'description' entry; they append to it
"notes": Method("parse_notes"),
"details": Method("parse_details")}
Expand Down Expand Up @@ -176,7 +204,7 @@ def parse_initial_date(self, finding: Finding, value: str) -> None:
def is_cve(self, c: str) -> bool:
return bool(c and isinstance(c, str) and self.CVE_PATTERN.fullmatch(c))

def parse_cves(self, finding: Finding, value: [str]) -> None:
def parse_cves(self, finding: Finding, value: list[str]) -> None:
finding.unsaved_vulnerability_ids = [c.upper() for c in value if self.is_cve(c)]

#####
Expand All @@ -192,19 +220,6 @@ def parse_status(self, finding: Finding, value: str) -> None:
elif value == "acceptable_risk":
finding.risk_accepted = True

#####
# For severity (extracted from cvss vector)
#####
def get_severity(self, value: str) -> Optional[str]:
if cvss_obj := cvss.parser.parse_cvss_from_text(value):
if (severity := cvss_obj[0].severities()[0].title()) in Finding.SEVERITIES:
return severity
return None

def parse_severity(self, finding: Finding, value: str) -> None:
if severity := self.get_severity(value):
finding.severity = severity

#####
# For parsing component data
#####
Expand All @@ -217,7 +232,7 @@ def parse_cpe(self, cpe_str: str) -> (Optional[str], Optional[str]):
(cpe_obj.get_version() and cpe_obj.get_version()[0]) or None,
)

def parse_components(self, finding: Finding, value: [str]) -> None:
def parse_components(self, finding: Finding, value: list[str]) -> None:
# Only use the first entry
finding.component_name, finding.component_version = self.parse_cpe(value[0])

Expand All @@ -236,12 +251,12 @@ def append_description(self, finding: Finding, addendum: dict[str, str]) -> None
def parse_notes(self, finding: Finding, value: str) -> None:
self.append_description(finding, {"Notes": value})

def extract_details(self, value: Union[str, dict[str, Union[str, dict[str, [str]]]]]) -> dict[str, str]:
def extract_details(self, value: Union[str, dict[str, Union[str, dict[str, list[str]]]]]) -> dict[str, str]:
if isinstance(value, dict):
return {k: v for k, v in value.items() if k != "_meta"}
return {"Details": str(value)}

def parse_details(self, finding: Finding, value: dict[str, Union[str, dict[str, [str]]]]) -> None:
def parse_details(self, finding: Finding, value: dict[str, Union[str, dict[str, list[str]]]]) -> None:
self.append_description(finding, self.extract_details(value))

#####
Expand Down Expand Up @@ -282,6 +297,44 @@ def set_endpoints(self, finding: Finding, item: Any) -> None:
endpoints = self.parse_endpoints(item)
finding.unsaved_endpoints.extend(endpoints)

#####
# For severity (extracted from various cvss vectors)
#####
def parse_cvss_vector(self, value: str) -> Optional[str]:
# CVSS4 vectors don't parse with the handy-danty parse method :(
try:
if (severity := cvss.CVSS4(value).severity) in Finding.SEVERITIES:
return severity
except CVSSError:
pass

if cvss_obj := cvss.parser.parse_cvss_from_text(value):
if (severity := cvss_obj[0].severities()[0].title()) in Finding.SEVERITIES:
return severity
return None

def set_severity(self, finding: Finding, item: Any) -> None:
for base_score_entry, cvss_version in [
("cvss_v4_base_score", 4),
("cvss_v3_base_score", 3),
("cvss_base_score", 2),
]:
if base_score := item.get(base_score_entry):
finding.severity = cvss_score_to_severity(base_score, cvss_version)
return

for vector_type in ["cvss_v4_vector", "cvss_v3_vector", "cvss_vector"]:
if vector := item.get(vector_type):
if severity := self.parse_cvss_vector(vector):
finding.severity = severity
return

finding.severity = "Info"

def process_whole_item(self, finding: Finding, item: Any) -> None:
self.set_severity(finding, item)
self.set_endpoints(finding, item)

# Returns the complete field processing map: common fields plus any engine-specific
def get_engine_fields(self) -> dict[str, FieldType]:
return {
Expand All @@ -302,7 +355,7 @@ def parse_finding(self, item: dict[str, Any]) -> Tuple[Finding, Tuple]:
# Check first whether the field even exists on this item entry; if not, skip it
if value := item.get(field):
field_handler(self, finding, value)
self.set_endpoints(finding, item)
self.process_whole_item(finding, item)
# Make a note of what scanning engine was used for this Finding
self.append_description(finding, {"Scanning Engine": self.SCANNING_ENGINE})
return finding, self.get_finding_key(finding)
Loading

0 comments on commit 28f4182

Please sign in to comment.