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
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
169 changes: 169 additions & 0 deletions docs/content/en/api/metrics-endpoint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
---
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
19 changes: 19 additions & 0 deletions dojo/api_v2/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3103,3 +3103,22 @@ class NotificationWebhooksSerializer(serializers.ModelSerializer):
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)
117 changes: 117 additions & 0 deletions dojo/api_v2/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3174,6 +3174,123 @@ def get_queryset(self):
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,
Expand Down
46 changes: 0 additions & 46 deletions dojo/fixtures/unit_limit_reqresp.json
Original file line number Diff line number Diff line change
Expand Up @@ -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

"pk": 8,
"model": "dojo.finding",
"fields": {
"last_reviewed_by": null,
"reviewers": [],
"static_finding": false,
"date": "2017-12-31",
"references": "",
"files": [],
"payload": null,
"under_defect_review": false,
"impact": "High",
"false_p": false,
"verified": false,
"severity": "High",
"title": "DUMMY FINDING WITH REQRESP",
"param": null,
"created": "2017-12-01T00:00:00Z",
"duplicate": false,
"mitigation": "MITIGATION",
"found_by": [
1
],
"numerical_severity": "S0",
"test": 5,
"out_of_scope": false,
"cwe": 1,
"file_path": "",
"duplicate_finding": null,
"description": "TEST finding",
"mitigated_by": null,
"reporter": 2,
"mitigated": null,
"active": false,
"line": 100,
"under_review": false,
"defect_review_requested_by": 2,
"review_requested_by": 2,
"thread_id": 1,
"url": "http://www.example.com",
"notes": [],
"dynamic_finding": false,
"hash_code": "c89d25e445b088ba339908f68e15e3177b78d22f3039d1bfea51c4be251bf4e0",
"last_reviewed": null
}
},{
"pk": 123,
"model": "dojo.burprawrequestresponse",
Expand Down
Loading