Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Assets will always include the following components:
Optional Asset metadata includes:

- **Tags**
- **Personnel information** (e.g., Asset Manager, Team Manager, Technical Contact, etc.)
- **Personnel information** (e.g., Asset Manager, Team Manager, Technical Contact, etc.). Only active users can be assigned to these fields, in the UI and over the API. If a user is deactivated later, the assignment stays in place as a historical reference and continues to be readable, but that user cannot be assigned again until the account is reactivated. An update that re-sends the value already stored is still accepted.
- **Regulations** (e.g., HIPAA, GLBA, OPPA, etc.)
- **Business criticality**
- **Platform** (e.g., API, Desktop, IoT, Mobile, Web, etc.)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,14 @@ Optional Asset metadata includes:
- **Business criticality**
- **User records** (i.e., the estimated number of user records in the Asset)
- **Revenue**
- **Personnel information** (e.g., Asset Manager, Team Manager, Technical Contact, etc.)
- **Personnel information** (e.g., Asset Manager, Team Manager, Technical Contact, etc.). Only active users can be assigned to these fields, in the UI and over the API. If a user is deactivated later, the assignment stays in place as a historical reference and continues to be readable, but that user cannot be assigned again until the account is reactivated. An update that re-sends the value already stored is still accepted.
- **Regulations** (e.g., HIPAA, GLBA, OPPA, etc.)
- **Platform** (e.g., API, Desktop, IoT, Mobile, Web, etc.)
- **Lifecycle** (e.g., Construction, Production, Retirement, etc.)
- **Origin** (e.g., Third-Party Library, Purchased, Open Source, etc.)

To send notifications to somebody who does not have an active DefectDojo account, use a Rules Engine 2 rule with an **Email** node instead. The node sends to any address named in its Email connection's recipients field, and the recipient needs no DefectDojo account at all. The personnel fields grant no notification rights of their own.

This metadata improves filtering, reporting, and prioritization across your security program, but most importantly, Assets also contain all of the Engagements, Tests, and Findings related to the testing efforts surrounding that Asset. All Findings from Tests ultimately roll up to the Asset level, enabling long-term tracking, trend analysis, and reporting.

Beyond these built-in fields, an administrator can define typed **Custom Fields** for Assets, which you fill in on the Asset's page and can turn on as opt-in columns on the All Assets table. See [Custom Fields](/asset_modelling/pro__custom_fields/).
Expand Down
5 changes: 3 additions & 2 deletions dojo/asset/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from dojo.api_v2.serializers import ProductMetaSerializer, TagListSerializerField
from dojo.authorization.serializer_guards import (
ActiveUserContactGuardMixin,
AuthorizedUsersMemberGuardMixin,
ToolConfigurationUseGuardMixin,
)
Expand All @@ -27,7 +28,7 @@ class Meta:
exclude = ("product",)


class AssetSerializer(AuthorizedUsersMemberGuardMixin, serializers.ModelSerializer):
class AssetSerializer(ActiveUserContactGuardMixin, AuthorizedUsersMemberGuardMixin, serializers.ModelSerializer):
findings_count = serializers.SerializerMethodField()
findings_list = serializers.SerializerMethodField()

Expand All @@ -40,7 +41,7 @@ class AssetSerializer(AuthorizedUsersMemberGuardMixin, serializers.ModelSerializ
enable_asset_tag_inheritance = serializers.BooleanField(source="enable_product_tag_inheritance", required=False, default=False)
asset_managers = serializers.PrimaryKeyRelatedField(
source="product_manager",
queryset=Dojo_User.objects.exclude(is_active=False),
queryset=Dojo_User.objects.all(),
required=False, allow_null=True,
)
business_criticality = serializers.ChoiceField(choices=Product.BUSINESS_CRITICALITY_CHOICES, allow_blank=True, allow_null=True, required=False)
Expand Down
61 changes: 61 additions & 0 deletions dojo/authorization/serializer_guards.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,64 @@ def _validate_tool_configuration_use(self, data):
if not get_authorized_tool_configurations(request_user).filter(pk=tool_configuration.pk).exists():
msg = "You do not have permission to use this tool configuration."
raise PermissionDenied(msg)


CONTACT_FIELDS = ("product_manager", "technical_contact", "team_manager")


class ActiveUserContactGuardMixin:

"""
Refuse a *new* contact assignment to a deactivated account.

An inactive account exists to keep a historical reference readable, such as
attribution for an import run by somebody who has since left. Picking one up as
a new contact turns that record back into a live relationship, which is what
the web UI's dropdowns have always refused (dojo.product.ui.forms.ProductForm).
This closes the same gap on the API.

Mix this into *every* serializer that writes one of ``CONTACT_FIELDS``,
including alias serializers over the same model, so the rule holds wherever the
field is reachable.

The check hangs off ``run_validation`` rather than ``validate`` on purpose: a
subclass that defines its own ``validate`` would otherwise shadow the mixin's
and silently drop the guard. It also means the Pro subclasses inherit the guard
without a change of their own.

No-ops when the field is absent (replay-safe on PATCH), when the value is null
(clearing a contact is always allowed), and when the submitted user already
holds that field on this row. That last case is what keeps an existing
assignment intact: a full PUT, and the Vue form's PATCH, both echo every
current value back, and rejecting the echo would delete a historical reference
rather than protect it.
"""

def run_validation(self, data=serializers.empty):
value = super().run_validation(data)
self._validate_contacts_are_active(value)
return value

def _validate_contacts_are_active(self, data):
errors = {}
for model_field in CONTACT_FIELDS:
if model_field not in data:
continue
# Field-level validation has already resolved the payload to a
# Dojo_User instance at this point.
user = data[model_field]
if user is None or user.is_active:
continue
if getattr(self.instance, f"{model_field}_id", None) == user.pk:
continue
# Key the error by the name the client actually sent: the assets
# endpoint calls this field ``asset_managers`` over the same column.
key = next(
(name for name, field in self.fields.items() if field.source == model_field),
model_field,
)
errors[key] = [
f"{user.get_username()} is not an active user and cannot be assigned to this field.",
]
if errors:
raise serializers.ValidationError(errors)
3 changes: 2 additions & 1 deletion dojo/product/api/serializer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from rest_framework import serializers

from dojo.authorization.serializer_guards import (
ActiveUserContactGuardMixin,
AuthorizedUsersMemberGuardMixin,
ToolConfigurationUseGuardMixin,
)
Expand All @@ -19,7 +20,7 @@ class Meta:
fields = "__all__"


class ProductSerializer(AuthorizedUsersMemberGuardMixin, serializers.ModelSerializer):
class ProductSerializer(ActiveUserContactGuardMixin, AuthorizedUsersMemberGuardMixin, serializers.ModelSerializer):
findings_count = serializers.SerializerMethodField()
findings_list = serializers.SerializerMethodField()

Expand Down
6 changes: 6 additions & 0 deletions dojo/product/ui/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ def __init__(self, *args, **kwargs):
if prod_type_id := kwargs.get("instance", Product()).prod_type_id: # we are editing existing instance
self.fields["prod_type"].queryset |= Product_Type.objects.filter(pk=prod_type_id) # even if user does not have permission for any other ProdType we need to add at least assign ProdType to make form submittable (otherwise empty list was here which generated invalid form)

# Same reason as prod_type above: a queryset that excludes the instance's
# own value renders it unselected, and saving then writes None.
for contact in ("product_manager", "technical_contact", "team_manager"):
if current_id := getattr(self.instance, f"{contact}_id", None):
self.fields[contact].queryset |= Dojo_User.objects.filter(pk=current_id)

# if this product has findings being asynchronously updated, disable the sla config field
if self.instance.async_updating:
self.fields["sla_configuration"].disabled = True
Expand Down
Loading
Loading