Skip to content

Commit 405b676

Browse files
committed
Add Simple Metrics API endpoint
1 parent ad93859 commit 405b676

File tree

13 files changed

+1167
-170
lines changed

13 files changed

+1167
-170
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+
"critical": 5,
40+
"high": 25,
41+
"medium": 75,
42+
"low": 40,
43+
"info": 5,
44+
"Opened": 10,
45+
"Closed": 8
46+
},
47+
{
48+
"product_type_id": 2,
49+
"product_type_name": "Mobile Application",
50+
"Total": 89,
51+
"critical": 2,
52+
"high": 15,
53+
"medium": 45,
54+
"low": 25,
55+
"info": 2,
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+
| `critical` | Integer | Number of Critical severity findings |
70+
| `high` | Integer | Number of High severity findings |
71+
| `medium` | Integer | Number of Medium severity findings |
72+
| `low` | Integer | Number of Low severity findings |
73+
| `info` | 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 descriptive field names (`critical`, `high`, `medium`, `low`, `info` for severity levels and `Total`, `Opened`, `Closed` for counts) to maintain consistency and readability
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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3103,3 +3103,22 @@ class NotificationWebhooksSerializer(serializers.ModelSerializer):
31033103
class Meta:
31043104
model = Notification_Webhooks
31053105
fields = "__all__"
3106+
3107+
3108+
class SimpleMetricsSerializer(serializers.Serializer):
3109+
3110+
"""Serializer for simple metrics data grouped by product type."""
3111+
3112+
product_type_id = serializers.IntegerField(read_only=True)
3113+
product_type_name = serializers.CharField(read_only=True)
3114+
Total = serializers.IntegerField(read_only=True)
3115+
3116+
# Severity labels
3117+
critical = serializers.IntegerField(read_only=True)
3118+
high = serializers.IntegerField(read_only=True)
3119+
medium = serializers.IntegerField(read_only=True)
3120+
low = serializers.IntegerField(read_only=True)
3121+
info = serializers.IntegerField(read_only=True)
3122+
3123+
Opened = serializers.IntegerField(read_only=True)
3124+
Closed = serializers.IntegerField(read_only=True)

dojo/api_v2/views.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3174,6 +3174,123 @@ def get_queryset(self):
31743174
return Answered_Survey.objects.all().order_by("id")
31753175

31763176

3177+
# Authorization: authenticated
3178+
class SimpleMetricsViewSet(
3179+
viewsets.ReadOnlyModelViewSet,
3180+
):
3181+
3182+
"""
3183+
Simple metrics API endpoint that provides finding counts by product type
3184+
broken down by severity and month status.
3185+
"""
3186+
3187+
serializer_class = serializers.SimpleMetricsSerializer
3188+
queryset = Product_Type.objects.none()
3189+
permission_classes = (IsAuthenticated,)
3190+
pagination_class = None
3191+
3192+
@extend_schema(
3193+
parameters=[
3194+
OpenApiParameter(
3195+
"date",
3196+
OpenApiTypes.DATE,
3197+
OpenApiParameter.QUERY,
3198+
required=False,
3199+
description="Date to generate metrics for (YYYY-MM-DD format). Defaults to current month.",
3200+
),
3201+
OpenApiParameter(
3202+
"product_type_id",
3203+
OpenApiTypes.INT,
3204+
OpenApiParameter.QUERY,
3205+
required=False,
3206+
description="Optional product type ID to filter metrics. If not provided, returns all accessible product types.",
3207+
),
3208+
],
3209+
responses={status.HTTP_200_OK: serializers.SimpleMetricsSerializer(many=True)},
3210+
)
3211+
def list(self, request):
3212+
"""Retrieve simple metrics data for the requested month grouped by product type."""
3213+
from dojo.metrics.views import get_simple_metrics_data
3214+
3215+
# Parse the date parameter, default to current month
3216+
now = timezone.now()
3217+
date_param = request.query_params.get("date")
3218+
product_type_id = request.query_params.get("product_type_id")
3219+
3220+
if date_param:
3221+
# Input validation
3222+
if len(date_param) > 20:
3223+
return Response(
3224+
{"error": "Invalid date parameter length."},
3225+
status=status.HTTP_400_BAD_REQUEST,
3226+
)
3227+
3228+
# Sanitize input - only allow alphanumeric characters and hyphens
3229+
import re
3230+
if not re.match(r"^[0-9\-]+$", date_param):
3231+
return Response(
3232+
{"error": "Invalid date format. Only numbers and hyphens allowed."},
3233+
status=status.HTTP_400_BAD_REQUEST,
3234+
)
3235+
3236+
try:
3237+
# Parse date string with validation
3238+
parsed_date = datetime.strptime(date_param, "%Y-%m-%d")
3239+
3240+
# Date range validation
3241+
min_date = datetime(2000, 1, 1)
3242+
max_date = datetime.now() + relativedelta(years=1)
3243+
3244+
if parsed_date < min_date or parsed_date > max_date:
3245+
return Response(
3246+
{"error": "Date must be between 2000-01-01 and one year from now."},
3247+
status=status.HTTP_400_BAD_REQUEST,
3248+
)
3249+
3250+
# Make it timezone aware
3251+
now = timezone.make_aware(parsed_date) if timezone.is_naive(parsed_date) else parsed_date
3252+
3253+
except ValueError:
3254+
return Response(
3255+
{"error": "Invalid date format. Use YYYY-MM-DD format."},
3256+
status=status.HTTP_400_BAD_REQUEST,
3257+
)
3258+
3259+
# Optional filtering by specific product type with validation
3260+
parsed_product_type_id = None
3261+
if product_type_id:
3262+
try:
3263+
parsed_product_type_id = int(product_type_id)
3264+
except ValueError:
3265+
return Response(
3266+
{"error": "Invalid product_type_id format."},
3267+
status=status.HTTP_400_BAD_REQUEST,
3268+
)
3269+
3270+
# Get metrics data
3271+
try:
3272+
metrics_data = get_simple_metrics_data(
3273+
now,
3274+
parsed_product_type_id,
3275+
)
3276+
except Exception as e:
3277+
logger.error(f"Error retrieving metrics: {e}")
3278+
return Response(
3279+
{"error": "Unable to retrieve metrics data."},
3280+
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
3281+
)
3282+
3283+
# Check if product type was requested but not found
3284+
if parsed_product_type_id and not metrics_data:
3285+
return Response(
3286+
{"error": "Product type not found or access denied."},
3287+
status=status.HTTP_404_NOT_FOUND,
3288+
)
3289+
3290+
serializer = self.serializer_class(metrics_data, many=True)
3291+
return Response(serializer.data)
3292+
3293+
31773294
# Authorization: configuration
31783295
class AnnouncementViewSet(
31793296
DojoModelViewSet,

dojo/fixtures/unit_limit_reqresp.json

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -165,52 +165,6 @@
165165
"hash_code": "c89d25e445b088ba339908f68e15e3177b78d22f3039d1bfea51c4be251bf4e0",
166166
"last_reviewed": null
167167
}
168-
},{
169-
"pk": 8,
170-
"model": "dojo.finding",
171-
"fields": {
172-
"last_reviewed_by": null,
173-
"reviewers": [],
174-
"static_finding": false,
175-
"date": "2017-12-31",
176-
"references": "",
177-
"files": [],
178-
"payload": null,
179-
"under_defect_review": false,
180-
"impact": "High",
181-
"false_p": false,
182-
"verified": false,
183-
"severity": "High",
184-
"title": "DUMMY FINDING WITH REQRESP",
185-
"param": null,
186-
"created": "2017-12-01T00:00:00Z",
187-
"duplicate": false,
188-
"mitigation": "MITIGATION",
189-
"found_by": [
190-
1
191-
],
192-
"numerical_severity": "S0",
193-
"test": 5,
194-
"out_of_scope": false,
195-
"cwe": 1,
196-
"file_path": "",
197-
"duplicate_finding": null,
198-
"description": "TEST finding",
199-
"mitigated_by": null,
200-
"reporter": 2,
201-
"mitigated": null,
202-
"active": false,
203-
"line": 100,
204-
"under_review": false,
205-
"defect_review_requested_by": 2,
206-
"review_requested_by": 2,
207-
"thread_id": 1,
208-
"url": "http://www.example.com",
209-
"notes": [],
210-
"dynamic_finding": false,
211-
"hash_code": "c89d25e445b088ba339908f68e15e3177b78d22f3039d1bfea51c4be251bf4e0",
212-
"last_reviewed": null
213-
}
214168
},{
215169
"pk": 123,
216170
"model": "dojo.burprawrequestresponse",

0 commit comments

Comments
 (0)