Skip to content

Commit 1737ad2

Browse files
committed
Add Simple Metrics API endpoint
1 parent fcf9878 commit 1737ad2

File tree

9 files changed

+570
-5
lines changed

9 files changed

+570
-5
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
---
2+
title: "Simple Metrics API Endpoint"
3+
description: "API endpoint for retrieving finding metrics by product type with severity breakdown"
4+
draft: false
5+
weight: 3
6+
---
7+
8+
## Simple Metrics API Endpoint
9+
10+
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.
11+
12+
### Endpoint Details
13+
14+
**URL:** `/api/v2/metrics/simple`
15+
16+
**Method:** `GET`
17+
18+
**Authentication:** Required (Token authentication)
19+
20+
**Authorization:** User must have `Product_Type_View` permission for the product types
21+
22+
### Query Parameters
23+
24+
| Parameter | Type | Required | Description |
25+
|-----------|------|----------|-------------|
26+
| `date` | String (YYYY-MM-DD) | No | Date to filter metrics by month/year (defaults to current month) |
27+
| `product_type_id` | Integer | No | Optional product type ID to filter metrics. If not provided, returns all accessible product types |
28+
29+
### Response Format
30+
31+
The endpoint returns an array of objects, each representing metrics for a product type:
32+
33+
```json
34+
[
35+
{
36+
"product_type_id": 1,
37+
"product_type_name": "Web Application",
38+
"Total": 150,
39+
"S0": 5, // Critical
40+
"S1": 25, // High
41+
"S2": 75, // Medium
42+
"S3": 40, // Low
43+
"S4": 5, // Info
44+
"Opened": 10,
45+
"Closed": 8
46+
},
47+
{
48+
"product_type_id": 2,
49+
"product_type_name": "Mobile Application",
50+
"Total": 89,
51+
"S0": 2, // Critical
52+
"S1": 15, // High
53+
"S2": 45, // Medium
54+
"S3": 25, // Low
55+
"S4": 2, // Info
56+
"Opened": 7,
57+
"Closed": 5
58+
}
59+
]
60+
```
61+
62+
### Response Fields
63+
64+
| Field | Type | Description |
65+
|-------|------|-------------|
66+
| `product_type_id` | Integer | Unique identifier for the product type |
67+
| `product_type_name` | String | Name of the product type |
68+
| `Total` | Integer | Total number of findings for the product type in the specified month |
69+
| `S0` | Integer | Number of Critical severity findings |
70+
| `S1` | Integer | Number of High severity findings |
71+
| `S2` | Integer | Number of Medium severity findings |
72+
| `S3` | Integer | Number of Low severity findings |
73+
| `S4` | Integer | Number of Info severity findings |
74+
| `Opened` | Integer | Number of findings opened in the specified month |
75+
| `Closed` | Integer | Number of findings closed in the specified month |
76+
77+
### Example Usage
78+
79+
#### Get current month metrics
80+
```bash
81+
GET /api/v2/metrics/simple
82+
```
83+
84+
#### Get metrics for January 2024
85+
```bash
86+
GET /api/v2/metrics/simple?date=2024-01-15
87+
```
88+
89+
#### Get metrics for a specific product type
90+
```bash
91+
GET /api/v2/metrics/simple?product_type_id=1
92+
```
93+
94+
#### Get metrics for a specific product type and date
95+
```bash
96+
GET /api/v2/metrics/simple?date=2024-05-01&product_type_id=2
97+
```
98+
99+
### Error Responses
100+
101+
#### 400 Bad Request - Invalid date characters
102+
```json
103+
{
104+
"error": "Invalid date format. Only numbers and hyphens allowed."
105+
}
106+
```
107+
108+
#### 400 Bad Request - Invalid date format
109+
```json
110+
{
111+
"error": "Invalid date format. Use YYYY-MM-DD format."
112+
}
113+
```
114+
115+
#### 400 Bad Request - Date out of range
116+
```json
117+
{
118+
"error": "Date must be between 2000-01-01 and one year from now."
119+
}
120+
```
121+
122+
#### 400 Bad Request - Invalid product_type_id format
123+
```json
124+
{
125+
"error": "Invalid product_type_id format."
126+
}
127+
```
128+
129+
#### 404 Not Found - Product type not found or access denied
130+
```json
131+
{
132+
"error": "Product type not found or access denied."
133+
}
134+
```
135+
136+
#### 403 Unauthorized - Missing or invalid authentication
137+
```json
138+
{
139+
"detail": "Authentication credentials were not provided."
140+
}
141+
```
142+
143+
#### 403 Forbidden - Insufficient permissions
144+
```json
145+
{
146+
"detail": "You do not have permission to perform this action."
147+
}
148+
```
149+
150+
### Notes
151+
152+
- **Authorization Model**: This endpoint uses the same authorization model as the UI's `/metrics/simple` page, ensuring consistent access control
153+
- **Performance**: The endpoint is optimized with database aggregation instead of Python loops for better performance
154+
- **Date Handling**: If no date is provided, the current month is used by default
155+
- **Timezone**: All dates are handled in the server's configured timezone
156+
- **Product Type Access**: Users will only see metrics for product types they have permission to view
157+
- **Data Consistency**: The data returned by this API endpoint matches exactly what is displayed on the `/metrics/simple` UI page
158+
- **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
159+
- **URL Format**: The endpoint automatically redirects requests without trailing slash to include one (301 redirect)
160+
- **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
161+
162+
### Use Cases
163+
164+
This endpoint is useful for:
165+
- **Dashboard Integration**: Integrating DefectDojo metrics into external dashboards and reporting tools
166+
- **Automated Reporting**: Creating automated reports showing security metrics trends over time
167+
- **CI/CD Integration**: Monitoring security metrics as part of continuous integration pipelines
168+
- **Executive Reporting**: Generating high-level security metrics for management reporting
169+
- **Data Analysis**: Performing custom analysis on security finding trends and patterns

dojo/api_v2/serializers.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3089,3 +3089,19 @@ class NotificationWebhooksSerializer(serializers.ModelSerializer):
30893089
class Meta:
30903090
model = Notification_Webhooks
30913091
fields = "__all__"
3092+
3093+
3094+
class SimpleMetricsSerializer(serializers.Serializer):
3095+
3096+
"""Serializer for simple metrics data grouped by product type."""
3097+
3098+
product_type_id = serializers.IntegerField(read_only=True)
3099+
product_type_name = serializers.CharField(read_only=True)
3100+
Total = serializers.IntegerField(read_only=True)
3101+
S0 = serializers.IntegerField(read_only=True) # Critical
3102+
S1 = serializers.IntegerField(read_only=True) # High
3103+
S2 = serializers.IntegerField(read_only=True) # Medium
3104+
S3 = serializers.IntegerField(read_only=True) # Low
3105+
S4 = serializers.IntegerField(read_only=True) # Info
3106+
Opened = serializers.IntegerField(read_only=True)
3107+
Closed = serializers.IntegerField(read_only=True)

dojo/api_v2/views.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from django.contrib.auth.models import Permission
1212
from django.core.exceptions import ValidationError
1313
from django.db import IntegrityError
14+
from django.db.models import Count, Q
1415
from django.http import FileResponse, Http404, HttpResponse
1516
from django.shortcuts import get_object_or_404
1617
from django.utils import timezone
@@ -3180,6 +3181,178 @@ def get_queryset(self):
31803181
return Answered_Survey.objects.all().order_by("id")
31813182

31823183

3184+
# Authorization: object-based (consistent with UI)
3185+
class SimpleMetricsViewSet(
3186+
viewsets.ReadOnlyModelViewSet,
3187+
):
3188+
3189+
"""
3190+
Simple metrics API endpoint that provides finding counts by product type
3191+
broken down by severity and month status.
3192+
3193+
This endpoint replicates the logic from the UI's /metrics/simple endpoint
3194+
and uses the same authorization model for consistency.
3195+
"""
3196+
3197+
serializer_class = serializers.SimpleMetricsSerializer
3198+
queryset = Product_Type.objects.none() # Required for consistent auth behavior
3199+
permission_classes = (IsAuthenticated,) # Match pattern used by RoleViewSet
3200+
pagination_class = None
3201+
3202+
@extend_schema(
3203+
parameters=[
3204+
OpenApiParameter(
3205+
"date",
3206+
OpenApiTypes.DATE,
3207+
OpenApiParameter.QUERY,
3208+
required=False,
3209+
description="Date to generate metrics for (YYYY-MM-DD format). Defaults to current month.",
3210+
),
3211+
OpenApiParameter(
3212+
"product_type_id",
3213+
OpenApiTypes.INT,
3214+
OpenApiParameter.QUERY,
3215+
required=False,
3216+
description="Optional product type ID to filter metrics. If not provided, returns all accessible product types.",
3217+
),
3218+
],
3219+
responses={status.HTTP_200_OK: serializers.SimpleMetricsSerializer(many=True)},
3220+
)
3221+
def list(self, request):
3222+
"""
3223+
Retrieve simple metrics data for the requested month grouped by product type.
3224+
3225+
This endpoint replicates the logic from the UI's /metrics/simple endpoint
3226+
and uses the same authorization model for consistency.
3227+
3228+
Performance optimized with database aggregation instead of Python loops.
3229+
"""
3230+
# Parse the date parameter, default to current month (same as UI)
3231+
now = timezone.now()
3232+
date_param = request.query_params.get("date")
3233+
product_type_id = request.query_params.get("product_type_id")
3234+
3235+
if date_param:
3236+
# Enhanced input validation while maintaining consistency with UI behavior
3237+
if len(date_param) > 20:
3238+
return Response(
3239+
{"error": "Invalid date parameter length."},
3240+
status=status.HTTP_400_BAD_REQUEST,
3241+
)
3242+
3243+
# Sanitize input - only allow alphanumeric characters and hyphens
3244+
import re
3245+
if not re.match(r"^[0-9\-]+$", date_param):
3246+
return Response(
3247+
{"error": "Invalid date format. Only numbers and hyphens allowed."},
3248+
status=status.HTTP_400_BAD_REQUEST,
3249+
)
3250+
3251+
try:
3252+
# Parse date string with validation
3253+
parsed_date = datetime.strptime(date_param, "%Y-%m-%d")
3254+
3255+
# Reasonable date range validation
3256+
min_date = datetime(2000, 1, 1)
3257+
max_date = datetime.now() + relativedelta(years=1)
3258+
3259+
if parsed_date < min_date or parsed_date > max_date:
3260+
return Response(
3261+
{"error": "Date must be between 2000-01-01 and one year from now."},
3262+
status=status.HTTP_400_BAD_REQUEST,
3263+
)
3264+
3265+
# Make it timezone aware
3266+
now = timezone.make_aware(parsed_date) if timezone.is_naive(parsed_date) else parsed_date
3267+
3268+
except ValueError:
3269+
return Response(
3270+
{"error": "Invalid date format. Use YYYY-MM-DD format."},
3271+
status=status.HTTP_400_BAD_REQUEST,
3272+
)
3273+
3274+
# Get authorized product types (same as UI implementation)
3275+
product_types = get_authorized_product_types(Permissions.Product_Type_View)
3276+
3277+
# Optional filtering by specific product type
3278+
if product_type_id:
3279+
try:
3280+
product_type_id = int(product_type_id)
3281+
product_types = product_types.filter(id=product_type_id)
3282+
if not product_types.exists():
3283+
return Response(
3284+
{"error": "Product type not found or access denied."},
3285+
status=status.HTTP_404_NOT_FOUND,
3286+
)
3287+
except ValueError:
3288+
return Response(
3289+
{"error": "Invalid product_type_id format."},
3290+
status=status.HTTP_400_BAD_REQUEST,
3291+
)
3292+
3293+
# Build base filter conditions (same logic as UI)
3294+
base_filter = Q(
3295+
false_p=False,
3296+
duplicate=False,
3297+
out_of_scope=False,
3298+
date__month=now.month,
3299+
date__year=now.year,
3300+
)
3301+
3302+
# Apply verification status filtering if enabled (same as UI)
3303+
if (get_system_setting("enforce_verified_status", True) or
3304+
get_system_setting("enforce_verified_status_metrics", True)):
3305+
base_filter &= Q(verified=True)
3306+
3307+
# Optimize with single aggregated query per product type
3308+
metrics_data = []
3309+
3310+
for pt in product_types:
3311+
# Single aggregated query replacing the Python loop
3312+
metrics = Finding.objects.filter(
3313+
test__engagement__product__prod_type=pt,
3314+
).filter(base_filter).aggregate(
3315+
# Total count
3316+
total=Count("id"),
3317+
3318+
# Count by severity using conditional aggregation
3319+
critical=Count("id", filter=Q(severity="Critical")),
3320+
high=Count("id", filter=Q(severity="High")),
3321+
medium=Count("id", filter=Q(severity="Medium")),
3322+
low=Count("id", filter=Q(severity="Low")),
3323+
info=Count("id", filter=~Q(severity__in=["Critical", "High", "Medium", "Low"])),
3324+
3325+
# Count opened in current month
3326+
opened=Count("id", filter=Q(date__year=now.year, date__month=now.month)),
3327+
3328+
# Count closed in current month
3329+
closed=Count("id", filter=Q(
3330+
mitigated__isnull=False,
3331+
mitigated__year=now.year,
3332+
mitigated__month=now.month,
3333+
)),
3334+
)
3335+
3336+
# Build the findings summary (same structure as UI)
3337+
findings_broken_out = {
3338+
"product_type_id": pt.id,
3339+
"product_type_name": pt.name, # Always show real name like UI
3340+
"Total": metrics["total"] or 0,
3341+
"S0": metrics["critical"] or 0, # Critical
3342+
"S1": metrics["high"] or 0, # High
3343+
"S2": metrics["medium"] or 0, # Medium
3344+
"S3": metrics["low"] or 0, # Low
3345+
"S4": metrics["info"] or 0, # Info
3346+
"Opened": metrics["opened"] or 0,
3347+
"Closed": metrics["closed"] or 0,
3348+
}
3349+
3350+
metrics_data.append(findings_broken_out)
3351+
3352+
serializer = self.serializer_class(metrics_data, many=True)
3353+
return Response(serializer.data)
3354+
3355+
31833356
# Authorization: configuration
31843357
class AnnouncementViewSet(
31853358
DojoModelViewSet,

dojo/urls.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
ReImportScanView,
5959
RiskAcceptanceViewSet,
6060
RoleViewSet,
61+
SimpleMetricsViewSet,
6162
SLAConfigurationViewset,
6263
SonarqubeIssueTransitionViewSet,
6364
SonarqubeIssueViewSet,
@@ -143,6 +144,7 @@
143144
v2_api.register(r"languages", LanguageViewSet, basename="languages")
144145
v2_api.register(r"language_types", LanguageTypeViewSet, basename="language_type")
145146
v2_api.register(r"metadata", DojoMetaViewSet, basename="metadata")
147+
v2_api.register(r"metrics/simple", SimpleMetricsViewSet, basename="simple_metrics")
146148
v2_api.register(r"network_locations", NetworkLocationsViewset, basename="network_locations")
147149
v2_api.register(r"notes", NotesViewSet, basename="notes")
148150
v2_api.register(r"note_type", NoteTypeViewSet, basename="note_type")

0 commit comments

Comments
 (0)