Skip to content

Add Simple Metrics API endpoint #12533

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from

Conversation

jamiesonio
Copy link

Description

This PR adds a REST API endpoint /api/v2/metrics/simple for programmatic access to DefectDojo's simple metrics functionality. This will enable automated reporting and external dashboard integration

Changes:

  • New API endpoint that mirrors the business logic of the existing UI /metrics/simple endpoint
  • Uses conditional aggregation to summarize product type counts in a single database query (as opposed to the UI's Python loops)
  • Fixes a bug in dojo/utils.py that caused crashes when anonymous users triggered logout events by handling null user cases

Test results

  • Created 10 test methods in 'dojo/unittests/test_apiv2_simple_metrics'
    uwsgi-1 | ----------------------------------------------------------------------
    uwsgi-1 | Ran 43 tests in 4.588s
    uwsgi-1 |
    uwsgi-1 | OK (skipped=32)

Documentation

  • Updated OpenAPI schema documentation
  • Feature summary added to docs (docs/content/en/api/metrics-endpoint.md)

Checklist

  • Make sure to rebase your PR against the very latest dev.
  • Features/Changes should be submitted against the dev.
  • Bugfixes should be submitted against the bugfix branch.
  • Give a meaningful name to your PR, as it may end up being used in the release notes.
  • Your code is flake8 compliant.
  • Your code is python 3.11 compliant.
  • If this is a new feature and not a bug fix, you've included the proper documentation in the docs at https://github.com/DefectDojo/django-DefectDojo/tree/dev/docs as part of this PR.
  • [N/A] Model changes must include the necessary migrations in the dojo/db_migrations folder.
  • Add applicable tests to the unit tests.
  • Add the proper label to categorize your PR.

Response

[
  {
    "product_type_id": 2,
    "product_type_name": "Platform A",
    "Total": 25,
    "S0": 13,
    "S1": 2,
    "S2": 10,
    "S3": 0,
    "S4": 0,
    "Opened": 25,
    "Closed": 0
  },
  {
    "product_type_id": 1,
    "product_type_name": "Research and Development",
    "Total": 7,
    "S0": 1,
    "S1": 3,
    "S2": 3,
    "S3": 0,
    "S4": 0,
    "Opened": 7,
    "Closed": 0
  }
]

Copy link

dryrunsecurity bot commented Jun 2, 2025

DryRun Security

🔴 Risk threshold exceeded.

This pull request introduces several security concerns, including potential information disclosure through error messages and metrics endpoints, user enumeration via logout logging, and a possible denial of service vulnerability in the metrics view. The changes affect multiple files in the dojo directory and require careful review to mitigate potential security risks.

⚠️ Configured Codepaths Edit in dojo/urls.py
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
⚠️ Configured Codepaths Edit in dojo/utils.py
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
⚠️ Configured Codepaths Edit in dojo/api_v2/serializers.py
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
⚠️ Configured Codepaths Edit in dojo/api_v2/views.py
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
⚠️ Information Disclosure via Error Messages in docs/content/en/api/metrics-endpoint.md
Vulnerability Information Disclosure via Error Messages
Description The documentation provides detailed error messages that could help an attacker understand backend validation logic. While informative for developers, these specific error messages reveal implementation details that could aid in probing the API's input validation mechanisms.

---
title: "Simple Metrics API Endpoint"
description: "API endpoint for retrieving finding metrics by product type with severity breakdown"
draft: false
weight: 3
---
## Simple Metrics API Endpoint
The Simple Metrics API endpoint provides finding counts by product type, broken down by severity levels and month status. This endpoint replicates the data from the UI's `/metrics/simple` page in JSON format, making it easier to integrate with other tools and dashboards.
### Endpoint Details
**URL:** `/api/v2/metrics/simple`
**Method:** `GET`
**Authentication:** Required (Token authentication)
**Authorization:** User must have `Product_Type_View` permission for the product types
### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `date` | String (YYYY-MM-DD) | No | Date to filter metrics by month/year (defaults to current month) |
| `product_type_id` | Integer | No | Optional product type ID to filter metrics. If not provided, returns all accessible product types |
### Response Format
The endpoint returns an array of objects, each representing metrics for a product type:
```json
[
{
"product_type_id": 1,
"product_type_name": "Web Application",
"Total": 150,
"S0": 5, // Critical
"S1": 25, // High
"S2": 75, // Medium
"S3": 40, // Low
"S4": 5, // Info
"Opened": 10,
"Closed": 8
},
{
"product_type_id": 2,
"product_type_name": "Mobile Application",
"Total": 89,
"S0": 2, // Critical
"S1": 15, // High
"S2": 45, // Medium
"S3": 25, // Low
"S4": 2, // Info
"Opened": 7,
"Closed": 5
}
]
```
### Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `product_type_id` | Integer | Unique identifier for the product type |
| `product_type_name` | String | Name of the product type |
| `Total` | Integer | Total number of findings for the product type in the specified month |
| `S0` | Integer | Number of Critical severity findings |
| `S1` | Integer | Number of High severity findings |
| `S2` | Integer | Number of Medium severity findings |
| `S3` | Integer | Number of Low severity findings |
| `S4` | Integer | Number of Info severity findings |
| `Opened` | Integer | Number of findings opened in the specified month |
| `Closed` | Integer | Number of findings closed in the specified month |
### Example Usage
#### Get current month metrics
```bash
GET /api/v2/metrics/simple
```
#### Get metrics for January 2024
```bash
GET /api/v2/metrics/simple?date=2024-01-15
```
#### Get metrics for a specific product type
```bash
GET /api/v2/metrics/simple?product_type_id=1
```
#### Get metrics for a specific product type and date
```bash
GET /api/v2/metrics/simple?date=2024-05-01&product_type_id=2
```
### Error Responses
#### 400 Bad Request - Invalid date characters
```json
{
"error": "Invalid date format. Only numbers and hyphens allowed."
}
```
#### 400 Bad Request - Invalid date format
```json
{
"error": "Invalid date format. Use YYYY-MM-DD format."
}
```
#### 400 Bad Request - Date out of range
```json
{
"error": "Date must be between 2000-01-01 and one year from now."
}
```
#### 400 Bad Request - Invalid product_type_id format
```json
{
"error": "Invalid product_type_id format."
}
```
#### 404 Not Found - Product type not found or access denied
```json
{
"error": "Product type not found or access denied."
}
```
#### 403 Unauthorized - Missing or invalid authentication
```json
{
"detail": "Authentication credentials were not provided."
}
```
#### 403 Forbidden - Insufficient permissions
```json
{
"detail": "You do not have permission to perform this action."
}
```
### Notes
- **Authorization Model**: This endpoint uses the same authorization model as the UI's `/metrics/simple` page, ensuring consistent access control
- **Performance**: The endpoint is optimized with database aggregation instead of Python loops for better performance
- **Date Handling**: If no date is provided, the current month is used by default
- **Timezone**: All dates are handled in the server's configured timezone
- **Product Type Access**: Users will only see metrics for product types they have permission to view
- **Data Consistency**: The data returned by this API endpoint matches exactly what is displayed on the `/metrics/simple` UI page
- **Field Naming**: The API uses specific field names (`S0`, `S1`, `S2`, `S3`, `S4` for severity levels and `Total`, `Opened`, `Closed` for counts) to maintain consistency with the internal data structure
- **URL Format**: The endpoint automatically redirects requests without trailing slash to include one (301 redirect)
- **Date Validation**: The API performs two levels of date validation: first checking for valid characters (only numbers and hyphens allowed), then validating the YYYY-MM-DD format
### Use Cases
This endpoint is useful for:
- **Dashboard Integration**: Integrating DefectDojo metrics into external dashboards and reporting tools
- **Automated Reporting**: Creating automated reports showing security metrics trends over time
- **CI/CD Integration**: Monitoring security metrics as part of continuous integration pipelines
- **Executive Reporting**: Generating high-level security metrics for management reporting
- **Data Analysis**: Performing custom analysis on security finding trends and patterns

⚠️ User Enumeration via Logout Logging in dojo/utils.py
Vulnerability User Enumeration via Logout Logging
Description The logout logging differentiates between authenticated and anonymous users, which could potentially provide a side channel for user enumeration if log entries are accessible or observable.

@receiver(user_logged_out)
def log_user_logout(sender, request, user, **kwargs):
if user:
logger.info("logout user: %s via ip: %s", user.username, request.META.get("REMOTE_ADDR"))
else:
logger.info("logout attempt for anonymous user via ip: %s", request.META.get("REMOTE_ADDR"))
@receiver(user_login_failed)

⚠️ Information Disclosure via New API Endpoint in dojo/urls.py
Vulnerability Information Disclosure via New API Endpoint
Description The new metrics endpoint could expose sensitive system information if not properly secured. The introduction of this endpoint requires careful review to ensure it does not leak operational details.

ReImportScanView,
RiskAcceptanceViewSet,
RoleViewSet,
SimpleMetricsViewSet,
SLAConfigurationViewset,
SonarqubeIssueTransitionViewSet,
SonarqubeIssueViewSet,

⚠️ Information Disclosure via Metrics in dojo/api_v2/serializers.py
Vulnerability Information Disclosure via Metrics
Description The serializer exposes detailed metrics about product types, including vulnerability counts by severity. If not properly protected, this could provide an attacker insights into the organization's security posture and vulnerability landscape.

class Meta:
model = Notification_Webhooks
fields = "__all__"
class SimpleMetricsSerializer(serializers.Serializer):
"""Serializer for simple metrics data grouped by product type."""
product_type_id = serializers.IntegerField(read_only=True)
product_type_name = serializers.CharField(read_only=True)
Total = serializers.IntegerField(read_only=True)
S0 = serializers.IntegerField(read_only=True) # Critical
S1 = serializers.IntegerField(read_only=True) # High
S2 = serializers.IntegerField(read_only=True) # Medium
S3 = serializers.IntegerField(read_only=True) # Low
S4 = serializers.IntegerField(read_only=True) # Info
Opened = serializers.IntegerField(read_only=True)
Closed = serializers.IntegerField(read_only=True)

⚠️ Potential Denial of Service in dojo/api_v2/views.py
Vulnerability Potential Denial of Service
Description The SimpleMetricsViewSet sets pagination_class = None, which could allow resource exhaustion if a large number of product types or findings are processed. This could lead to excessive server resource consumption.

from django.contrib.auth.models import Permission
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.db.models import Count, Q
from django.http import FileResponse, Http404, HttpResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone

⚠️ Information Disclosure via Error Messaging in dojo/api_v2/views.py
Vulnerability Information Disclosure via Error Messaging
Description The error message for product_type_id does not clearly distinguish between a non-existent product type and an unauthorized product type. This could potentially allow for subtle user enumeration.

from django.contrib.auth.models import Permission
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.db.models import Count, Q
from django.http import FileResponse, Http404, HttpResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone

We've notified @mtesauro.


All finding details can be found in the DryRun Security Dashboard.

Comment on lines 3101 to 3105
S0 = serializers.IntegerField(read_only=True) # Critical
S1 = serializers.IntegerField(read_only=True) # High
S2 = serializers.IntegerField(read_only=True) # Medium
S3 = serializers.IntegerField(read_only=True) # Low
S4 = serializers.IntegerField(read_only=True) # Info
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just name these after the severity level rather than commenting and documenting the conversion for Sx -> severity level?

Copy link
Author

Choose a reason for hiding this comment

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

The S0-S4 naming is maintained for potential backward compatibility and codebase consistency. The Sx pattern is used consistently throughout the UI's templates and database queries. That said I am happy to make the change to named labels to make it more clear for anyone integrating.

Copy link
Contributor

Choose a reason for hiding this comment

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

FWIW, the S0-4 naming is the absolute definition of legacy code. That was initially used in the very, very early versions of DefectDojo then transitions to the more typical "Critical, High, ..."

So, even though the code has S# all over it, the 'modern' version of that is the more human friendly labels.

Copy link
Author

Choose a reason for hiding this comment

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

I changed the API to return the human readable labels, also as part of refactoring the view to be able to share the same code between UI and API, I updated the Django templates to use the human readable labels as well.

curl -H "Authorization: Token REDACTED" \
     -H "Content-Type: application/json" \
     http://localhost:8080/api/v2/metrics/simple/

[
    {
        "product_type_id": 2,
        "product_type_name": "Production",
        "Total": 0,
        "critical": 0,
        "high": 0,
        "medium": 0,
        "low": 0,
        "info": 0,
        "Opened": 0,
        "Closed": 0
    },
    {
        "product_type_id": 1,
        "product_type_name": "Research and Development",
        "Total": 0,
        "critical": 0,
        "high": 0,
        "medium": 0,
        "low": 0,
        "info": 0,
        "Opened": 0,
        "Closed": 0
    }
]

@valentijnscholten valentijnscholten added this to the 2.48.0 milestone Jun 9, 2025
Copy link
Member

@valentijnscholten valentijnscholten left a comment

Choose a reason for hiding this comment

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

The (almost the) same simple metrics queries are also in dojo/metrics/views.py. Could you look at having them in once place?

@jamiesonio jamiesonio force-pushed the feature/metrics-api branch from 1737ad2 to 61e7ea1 Compare June 19, 2025 02:54
@github-actions github-actions bot added the ui label Jun 19, 2025
Copy link

dryrunsecurity bot commented Jun 19, 2025

DryRun Security

🔴 Risk threshold exceeded.

This pull request introduces several security considerations, including potential information disclosure through metrics serialization, IP logging during logout, and product type enumeration, with multiple sensitive files being edited across the project's API, utility, and template components. While none of the findings are blocking, they highlight areas where careful access control and error message handling could mitigate potential reconnaissance or information leakage risks.

🔴 Configured Codepaths Edit in dojo/templates/dojo/simple_metrics.html
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/urls.py
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/utils.py
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/api_v2/serializers.py
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/api_v2/views.py
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/metrics/views.py
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
🔴 Configured Codepaths Edit in dojo/templates/dojo/pt_counts.html
Vulnerability Configured Codepaths Edit
Description Sensitive edits detected for this file. Sensitive file paths and allowed authors can be configured in .dryrunsecurity.yaml.
Metrics Information Disclosure in dojo/api_v2/serializers.py
Vulnerability Metrics Information Disclosure
Description The new SimpleMetricsSerializer exposes detailed security metrics, including finding counts by severity and status. If not properly secured, this could provide attackers with insights into the application's security posture and internal metrics.

class Meta:
model = Notification_Webhooks
fields = "__all__"
class SimpleMetricsSerializer(serializers.Serializer):
"""Serializer for simple metrics data grouped by product type."""
product_type_id = serializers.IntegerField(read_only=True)
product_type_name = serializers.CharField(read_only=True)
Total = serializers.IntegerField(read_only=True)
# Severity labels
critical = serializers.IntegerField(read_only=True)
high = serializers.IntegerField(read_only=True)
medium = serializers.IntegerField(read_only=True)
low = serializers.IntegerField(read_only=True)
info = serializers.IntegerField(read_only=True)
Opened = serializers.IntegerField(read_only=True)
Closed = serializers.IntegerField(read_only=True)

Logout IP Logging in dojo/utils.py
Vulnerability Logout IP Logging
Description The patch introduces logging of IP addresses for both authenticated and anonymous logout attempts. While logging can be useful for auditing, logging IP addresses for anonymous users could potentially be used for tracking or reconnaissance if not carefully managed.

@receiver(user_logged_out)
def log_user_logout(sender, request, user, **kwargs):
if user:
logger.info("logout user: %s via ip: %s", user.username, request.META.get("REMOTE_ADDR"))
else:
logger.info("logout attempt for anonymous user via ip: %s", request.META.get("REMOTE_ADDR"))
@receiver(user_login_failed)

Product Type Enumeration in dojo/api_v2/views.py
Vulnerability Product Type Enumeration
Description The new metrics API endpoint allows filtering by product type ID, which could potentially enable an attacker to enumerate valid product type IDs through careful probing of the API responses. While the implementation includes authorization checks, the distinct error messages and response handling could leak information about the existence of product types.

return Answered_Survey.objects.all().order_by("id")
# Authorization: authenticated
class SimpleMetricsViewSet(
viewsets.ReadOnlyModelViewSet,
):
"""
Simple metrics API endpoint that provides finding counts by product type
broken down by severity and month status.
"""
serializer_class = serializers.SimpleMetricsSerializer
queryset = Product_Type.objects.none()
permission_classes = (IsAuthenticated,)
pagination_class = None
@extend_schema(
parameters=[
OpenApiParameter(
"date",
OpenApiTypes.DATE,
OpenApiParameter.QUERY,
required=False,
description="Date to generate metrics for (YYYY-MM-DD format). Defaults to current month.",
),
OpenApiParameter(
"product_type_id",
OpenApiTypes.INT,
OpenApiParameter.QUERY,
required=False,
description="Optional product type ID to filter metrics. If not provided, returns all accessible product types.",
),
],
responses={status.HTTP_200_OK: serializers.SimpleMetricsSerializer(many=True)},
)
def list(self, request):
"""Retrieve simple metrics data for the requested month grouped by product type."""
from dojo.metrics.views import get_simple_metrics_data
# Parse the date parameter, default to current month
now = timezone.now()
date_param = request.query_params.get("date")
product_type_id = request.query_params.get("product_type_id")
if date_param:
# Input validation
if len(date_param) > 20:
return Response(
{"error": "Invalid date parameter length."},
status=status.HTTP_400_BAD_REQUEST,
)
# Sanitize input - only allow alphanumeric characters and hyphens
import re
if not re.match(r"^[0-9\-]+$", date_param):
return Response(
{"error": "Invalid date format. Only numbers and hyphens allowed."},
status=status.HTTP_400_BAD_REQUEST,
)
try:
# Parse date string with validation
parsed_date = datetime.strptime(date_param, "%Y-%m-%d")
# Date range validation
min_date = datetime(2000, 1, 1)
max_date = datetime.now() + relativedelta(years=1)
if parsed_date < min_date or parsed_date > max_date:
return Response(
{"error": "Date must be between 2000-01-01 and one year from now."},
status=status.HTTP_400_BAD_REQUEST,
)
# Make it timezone aware
now = timezone.make_aware(parsed_date) if timezone.is_naive(parsed_date) else parsed_date
except ValueError:
return Response(
{"error": "Invalid date format. Use YYYY-MM-DD format."},
status=status.HTTP_400_BAD_REQUEST,
)
# Optional filtering by specific product type with validation
parsed_product_type_id = None
if product_type_id:
try:
parsed_product_type_id = int(product_type_id)
except ValueError:
return Response(
{"error": "Invalid product_type_id format."},
status=status.HTTP_400_BAD_REQUEST,
)
# Get metrics data
try:
metrics_data = get_simple_metrics_data(
now,
parsed_product_type_id,
)
except Exception as e:
logger.error(f"Error retrieving metrics: {e}")
return Response(
{"error": "Unable to retrieve metrics data."},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
# Check if product type was requested but not found
if parsed_product_type_id and not metrics_data:
return Response(
{"error": "Product type not found or access denied."},
status=status.HTTP_404_NOT_FOUND,
)
serializer = self.serializer_class(metrics_data, many=True)
return Response(serializer.data)
# Authorization: configuration
class AnnouncementViewSet(
DojoModelViewSet,

Test Credential Linting Disabled in ruff.toml
Vulnerability Test Credential Linting Disabled
Description Disabling the S106 linting rule for test files removes warnings about hardcoded passwords, which could lead to accidental exposure of credentials if test files are mishandled or the repository is compromised.

django-DefectDojo/ruff.toml

Lines 112 to 118 in 405b676

[lint.per-file-ignores]
"unittests/**" = [
"S105", # hardcoded passwords in tests are fine
"S106", # hardcoded password assigned to argument in tests are fine
"S108", # tmp paths mentioned in tests are fine
]

Verbose Error Message Disclosure in docs/content/en/api/metrics-endpoint.md
Vulnerability Verbose Error Message Disclosure
Description The API documentation reveals detailed error messages that provide insights into the backend's validation logic. While informative for developers, these messages could assist an attacker in understanding the application's input validation mechanisms and potential attack surfaces.

---
title: "Simple Metrics API Endpoint"
description: "API endpoint for retrieving finding metrics by product type with severity breakdown"
draft: false
weight: 3
---
## Simple Metrics API Endpoint
The Simple Metrics API endpoint provides finding counts by product type, broken down by severity levels and month status. This endpoint replicates the data from the UI's `/metrics/simple` page in JSON format, making it easier to integrate with other tools and dashboards.
### Endpoint Details
**URL:** `/api/v2/metrics/simple`
**Method:** `GET`
**Authentication:** Required (Token authentication)
**Authorization:** User must have `Product_Type_View` permission for the product types
### Query Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `date` | String (YYYY-MM-DD) | No | Date to filter metrics by month/year (defaults to current month) |
| `product_type_id` | Integer | No | Optional product type ID to filter metrics. If not provided, returns all accessible product types |
### Response Format
The endpoint returns an array of objects, each representing metrics for a product type:
```json
[
{
"product_type_id": 1,
"product_type_name": "Web Application",
"Total": 150,
"critical": 5,
"high": 25,
"medium": 75,
"low": 40,
"info": 5,
"Opened": 10,
"Closed": 8
},
{
"product_type_id": 2,
"product_type_name": "Mobile Application",
"Total": 89,
"critical": 2,
"high": 15,
"medium": 45,
"low": 25,
"info": 2,
"Opened": 7,
"Closed": 5
}
]
```
### Response Fields
| Field | Type | Description |
|-------|------|-------------|
| `product_type_id` | Integer | Unique identifier for the product type |
| `product_type_name` | String | Name of the product type |
| `Total` | Integer | Total number of findings for the product type in the specified month |
| `critical` | Integer | Number of Critical severity findings |
| `high` | Integer | Number of High severity findings |
| `medium` | Integer | Number of Medium severity findings |
| `low` | Integer | Number of Low severity findings |
| `info` | Integer | Number of Info severity findings |
| `Opened` | Integer | Number of findings opened in the specified month |
| `Closed` | Integer | Number of findings closed in the specified month |
### Example Usage
#### Get current month metrics
```bash
GET /api/v2/metrics/simple
```
#### Get metrics for January 2024
```bash
GET /api/v2/metrics/simple?date=2024-01-15
```
#### Get metrics for a specific product type
```bash
GET /api/v2/metrics/simple?product_type_id=1
```
#### Get metrics for a specific product type and date
```bash
GET /api/v2/metrics/simple?date=2024-05-01&product_type_id=2
```
### Error Responses
#### 400 Bad Request - Invalid date characters
```json
{
"error": "Invalid date format. Only numbers and hyphens allowed."
}
```
#### 400 Bad Request - Invalid date format
```json
{
"error": "Invalid date format. Use YYYY-MM-DD format."
}
```
#### 400 Bad Request - Date out of range
```json
{
"error": "Date must be between 2000-01-01 and one year from now."
}
```
#### 400 Bad Request - Invalid product_type_id format
```json
{
"error": "Invalid product_type_id format."
}
```
#### 404 Not Found - Product type not found or access denied
```json
{
"error": "Product type not found or access denied."
}
```
#### 403 Unauthorized - Missing or invalid authentication
```json
{
"detail": "Authentication credentials were not provided."
}
```
#### 403 Forbidden - Insufficient permissions
```json
{
"detail": "You do not have permission to perform this action."
}
```
### Notes
- **Authorization Model**: This endpoint uses the same authorization model as the UI's `/metrics/simple` page, ensuring consistent access control
- **Performance**: The endpoint is optimized with database aggregation instead of Python loops for better performance
- **Date Handling**: If no date is provided, the current month is used by default
- **Timezone**: All dates are handled in the server's configured timezone
- **Product Type Access**: Users will only see metrics for product types they have permission to view
- **Data Consistency**: The data returned by this API endpoint matches exactly what is displayed on the `/metrics/simple` UI page
- **Field Naming**: The API uses descriptive field names (`critical`, `high`, `medium`, `low`, `info` for severity levels and `Total`, `Opened`, `Closed` for counts) to maintain consistency and readability
- **URL Format**: The endpoint automatically redirects requests without trailing slash to include one (301 redirect)
- **Date Validation**: The API performs two levels of date validation: first checking for valid characters (only numbers and hyphens allowed), then validating the YYYY-MM-DD format
### Use Cases
This endpoint is useful for:
- **Dashboard Integration**: Integrating DefectDojo metrics into external dashboards and reporting tools
- **Automated Reporting**: Creating automated reports showing security metrics trends over time
- **CI/CD Integration**: Monitoring security metrics as part of continuous integration pipelines
- **Executive Reporting**: Generating high-level security metrics for management reporting
- **Data Analysis**: Performing custom analysis on security finding trends and patterns

We've notified @mtesauro.


All finding details can be found in the DryRun Security Dashboard.

@jamiesonio jamiesonio force-pushed the feature/metrics-api branch from 61e7ea1 to f10e455 Compare June 19, 2025 03:32
Comment on lines +101 to +142
with connection.cursor() as cursor:
cursor.execute("""
INSERT INTO dojo_finding (
title, description, severity, test_id, reporter_id, date,
active, verified, false_p, duplicate, out_of_scope,
created, last_reviewed, last_status_update,
mitigated, is_mitigated, risk_accepted, under_review,
under_defect_review, review_requested_by_id,
defect_review_requested_by_id, sonarqube_issue_id,
hash_code, line, file_path, component_name, component_version,
static_finding, dynamic_finding, created_from_issue_id,
status_id, group_id, sast_source_object, sast_sink_object,
sast_source_line, sast_source_file_path, nb_occurences,
publish_date, service, planned_remediation_date,
planned_remediation_version, effort_for_fixing,
impact, steps_to_reproduce, severity_justification,
references, mitigation, references_raw, mitigation_raw,
cvssv3, cvssv3_score, url, tags, scanner_confidence,
numerical_severity, param, payload, cwe, unique_id_from_tool,
vuln_id_from_tool, sast_source_function, sast_source_function_start,
sast_source_function_end, sast_sink_function, sast_sink_line,
sast_sink_file_path, sast_sink_function_start,
sast_sink_function_end, epss_score, epss_percentile,
cve_id, has_tags, sonarqube_project_key,
sonarqube_project_branch, sonarqube_project_pull_request
) VALUES (
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s,
NOW(), NULL, NOW(), NULL, %s, %s, %s, %s, NULL, NULL, NULL,
'', NULL, '', '', '', %s, %s, NULL, NULL, NULL, '',
'', NULL, '', 1, NOW(), '', NULL, '', '', '', '', '',
'', '', '', '', '', NULL, '', '',
CASE %s
WHEN 'Critical' THEN 0
WHEN 'High' THEN 1
WHEN 'Medium' THEN 2
WHEN 'Low' THEN 3
ELSE 4
END,
'', '', NULL, '', '', '', NULL, NULL, '', NULL, '',
NULL, NULL, NULL, NULL, NULL, %s, '', '', ''
)
""", [
Copy link
Member

Choose a reason for hiding this comment

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

Could you elaborate on why inserting the finding this way is needed?

Copy link
Author

Choose a reason for hiding this comment

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

I did this to bypass Django signals/Celery to ensure the test data was predictable/deterministic for testing the API call.

Copy link
Member

Choose a reason for hiding this comment

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

But why is that needed? I can't think of reason why a test would fail unless a finding is inserted via raw SQL. It also looks like this method is never called, or am I misreading the PR?

Copy link
Author

Choose a reason for hiding this comment

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

Thank you for digging into that. You are absolutely correct, it is unused. I suspect I was using this for initial testing before I discovered the test data fixture. I'll spend some time refactoring the unit tests here to make sure they align with the structure for the rest of the project.

@@ -165,52 +165,6 @@
"hash_code": "c89d25e445b088ba339908f68e15e3177b78d22f3039d1bfea51c4be251bf4e0",
"last_reviewed": null
}
},{
Copy link
Author

Choose a reason for hiding this comment

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

Removing the duplicate PK:8 as it was causing my unit tests to fail randomly

@jamiesonio jamiesonio force-pushed the feature/metrics-api branch from f10e455 to 405b676 Compare June 19, 2025 22:58
Copy link
Contributor

This pull request has conflicts, please resolve those before we can evaluate the pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants