581 vulnerability scrolling (#582)
* Refactor vulnerability search logic to improve pagination handling and add scroll API for unlimited results export * Bump version to 0.1.18
taylor_socfortress committed
Jan 1, 2026 at 10:09 UTC
965c0bef606c13e126bd2e0a28e83de3268ca965
2 files changed
+449
-211
backend/app/agents/vulnerabilities/services/vulnerabilities.py
+448
-210
@@ -880,9 +880,7 @@ async def search_vulnerabilities_from_indexer(
880
881
# Override customer_code based on user permissions
882
if "*" not in accessible_customers:
883
- # User has limited access - filter by their accessible customers
883
if customer_code and customer_code not in accessible_customers:
885
- # User requested a customer they don't have access to
884
return VulnerabilitySearchResponse(
885
vulnerabilities=[],
886
total_count=0,
@@ -899,7 +897,6 @@ async def search_vulnerabilities_from_indexer(
897
message=f"Access denied to customer {customer_code}",
898
filters_applied={},
899
)
902
- # If no customer_code specified or user has access, we'll filter by accessible customers later
900
901
# Build filters applied dict for response
902
filters_applied = {}
@@ -914,14 +911,12 @@ async def search_vulnerabilities_from_indexer(
911
if package_name:
912
filters_applied["package_name"] = package_name
913
917
- # Create Elasticsearch client
914
es_client = None
915
try:
916
# Initialize Elasticsearch client
917
es_client = await create_wazuh_indexer_client_async("Wazuh-Indexer")
918
923
- # Always get all agents to build hostname to customer_code mapping
924
- # This ensures we can always provide customer_code in the response
919
+ # Get all agents for customer code mapping
920
all_agents_query = select(Agents)
921
all_agents_result = await db_session.execute(all_agents_query)
922
all_agents = all_agents_result.scalars().all()
@@ -940,7 +935,6 @@ async def search_vulnerabilities_from_indexer(
935
936
# Apply user access restrictions first
937
if "*" not in accessible_customers:
943
- # User has limited access - only show their customers' agents
938
query = query.filter(Agents.customer_code.in_(accessible_customers))
939
940
# Apply additional filters if specified
@@ -971,7 +965,6 @@ async def search_vulnerabilities_from_indexer(
965
)
966
967
# Build list of agent hostnames for Elasticsearch filtering
974
- # If user has restricted access, always filter by their accessible agents
968
if "*" not in accessible_customers or customer_code or agent_name:
969
for agent in agents:
970
if agent.hostname:
@@ -1016,146 +1009,65 @@ async def search_vulnerabilities_from_indexer(
1009
if package_name:
1010
es_query["bool"]["must"].append({"wildcard": {"package.name": f"*{package_name}*"}})
1011
1019
- # Calculate pagination
1020
- start_index = (page - 1) * page_size
1012
+ # First, get total count and severity aggregations
1013
+ count_response = await es_client.count(index=",".join(vuln_indices), body={"query": es_query})
1014
+ total_count = count_response["count"]
1015
+
1016
+ # Get severity aggregations
1017
+ agg_response = await es_client.search(
1018
+ index=",".join(vuln_indices),
1019
+ body={
1020
+ "query": es_query,
1021
+ "size": 0,
1022
+ "aggs": {"severity_counts": {"terms": {"field": "vulnerability.severity", "size": 10}}},
1023
+ },
1024
+ )
1025
1022
- # Create Elasticsearch client
1023
- es_client = None
1024
- try:
1025
- es_client = await create_wazuh_indexer_client_async("Wazuh-Indexer")
1026
-
1027
- # First, get total count and severity aggregations
1028
- count_response = await es_client.count(index=",".join(vuln_indices), body={"query": es_query})
1029
- total_count = count_response["count"]
1030
-
1031
- # Get severity aggregations
1032
- agg_response = await es_client.search(
1033
- index=",".join(vuln_indices),
1034
- body={
1035
- "query": es_query,
1036
- "size": 0, # We don't need documents, just aggregations
1037
- "aggs": {"severity_counts": {"terms": {"field": "vulnerability.severity", "size": 10}}},
1038
- },
1026
+ # Extract severity counts
1027
+ severity_counts = {"Critical": 0, "High": 0, "Medium": 0, "Low": 0}
1028
+ if "aggregations" in agg_response and "severity_counts" in agg_response["aggregations"]:
1029
+ for bucket in agg_response["aggregations"]["severity_counts"]["buckets"]:
1030
+ severity_value = bucket["key"]
1031
+ count = bucket["doc_count"]
1032
+ if severity_value in severity_counts:
1033
+ severity_counts[severity_value] = count
1034
+
1035
+ # Calculate pagination info
1036
+ total_pages = (total_count + page_size - 1) // page_size
1037
+ has_next = page < total_pages
1038
+ has_previous = page > 1
1039
+
1040
+ if total_count == 0:
1041
+ return VulnerabilitySearchResponse(
1042
+ vulnerabilities=[],
1043
+ total_count=0,
1044
+ critical_count=0,
1045
+ high_count=0,
1046
+ medium_count=0,
1047
+ low_count=0,
1048
+ page=page,
1049
+ page_size=page_size,
1050
+ total_pages=0,
1051
+ has_next=False,
1052
+ has_previous=False,
1053
+ success=True,
1054
+ message="No vulnerabilities found matching the specified criteria",
1055
+ filters_applied=filters_applied,
1056
)
1057
1041
- # Extract severity counts from aggregation response
1042
- severity_counts = {"Critical": 0, "High": 0, "Medium": 0, "Low": 0}
1043
- if "aggregations" in agg_response and "severity_counts" in agg_response["aggregations"]:
1044
- for bucket in agg_response["aggregations"]["severity_counts"]["buckets"]:
1045
- severity = bucket["key"]
1046
- count = bucket["doc_count"]
1047
- if severity in severity_counts:
1048
- severity_counts[severity] = count
1049
-
1050
- # Calculate pagination info
1051
- total_pages = (total_count + page_size - 1) // page_size
1052
- has_next = page < total_pages
1053
- has_previous = page > 1
1054
-
1055
- if total_count == 0:
1056
- return VulnerabilitySearchResponse(
1057
- vulnerabilities=[],
1058
- total_count=0,
1059
- critical_count=0,
1060
- high_count=0,
1061
- medium_count=0,
1062
- low_count=0,
1063
- page=page,
1064
- page_size=page_size,
1065
- total_pages=0,
1066
- has_next=False,
1067
- has_previous=False,
1068
- success=True,
1069
- message="No vulnerabilities found matching the specified criteria",
1070
- filters_applied=filters_applied,
1071
- )
1058
+ # Calculate start index
1059
+ start_index = (page - 1) * page_size
1060
1073
- # Get the actual results with pagination
1074
- search_response = await es_client.search(
1075
- index=",".join(vuln_indices),
1076
- body={
1077
- "query": es_query,
1078
- "sort": [{"vulnerability.detected_at": {"order": "desc"}}, {"vulnerability.severity": {"order": "asc"}}],
1079
- "from": start_index,
1080
- "size": page_size,
1081
- },
1061
+ # Check if pagination exceeds Elasticsearch's 10,000 result window
1062
+ if start_index >= 10000:
1063
+ logger.warning(
1064
+ f"Deep pagination requested (page {page}, start_index {start_index}). "
1065
+ f"Elasticsearch limits pagination to 10,000 results. "
1066
+ f"Please use more specific filters or export to CSV report.",
1067
)
1068
1084
- vulnerabilities = []
1085
- for hit in search_response["hits"]["hits"]:
1086
- try:
1087
- source = hit["_source"]
1088
- agent_data = source.get("agent", {})
1089
- agent_hostname = agent_data.get("name", "unknown")
1090
-
1091
- # Get customer code from our mapping
1092
- agent_customer_code = customer_agent_map.get(agent_hostname)
1093
-
1094
- # Process the vulnerability data
1095
- vuln_data = process_wazuh_document(hit)
1096
-
1097
- # Get EPSS score for the CVE (if requested)
1098
- epss_score, epss_percentile = None, None
1099
- if include_epss:
1100
- epss_score, epss_percentile = await get_epss_score_for_cve(vuln_data.cve_id)
1101
-
1102
- vulnerability_item = VulnerabilitySearchItem(
1103
- cve_id=vuln_data.cve_id,
1104
- severity=vuln_data.severity,
1105
- title=vuln_data.title,
1106
- agent_name=agent_hostname,
1107
- customer_code=agent_customer_code,
1108
- references=vuln_data.references,
1109
- detected_at=vuln_data.detected_at,
1110
- published_at=vuln_data.published_at,
1111
- base_score=vuln_data.base_score,
1112
- package_name=vuln_data.package_name,
1113
- package_version=vuln_data.package_version,
1114
- package_architecture=vuln_data.package_architecture,
1115
- epss_score=epss_score,
1116
- epss_percentile=epss_percentile,
1117
- )
1118
- vulnerabilities.append(vulnerability_item)
1119
-
1120
- except Exception as e:
1121
- logger.error(f"Error processing vulnerability document: {e}")
1122
- continue
1123
-
1124
- # Sort vulnerabilities by EPSS score (highest to lowest) if EPSS is included
1125
- if include_epss:
1126
- # Sort by EPSS score descending, treating None/null as 0
1127
- # Then by severity (Critical=0, High=1, Medium=2, Low=3) for tie-breaking
1128
- severity_order = {"Critical": 0, "High": 1, "Medium": 2, "Low": 3}
1129
-
1130
- def get_epss_sort_key(vuln):
1131
- # Convert EPSS score to float for sorting, handle string/None values
1132
- epss_score = vuln.epss_score
1133
- if epss_score is None:
1134
- epss_float = 0.0
1135
- else:
1136
- try:
1137
- epss_float = float(epss_score)
1138
- except (ValueError, TypeError):
1139
- epss_float = 0.0
1140
- return (
1141
- -epss_float, # Negative for descending order
1142
- severity_order.get(vuln.severity, 4), # Secondary sort by severity
1143
- vuln.cve_id, # Tertiary sort by CVE ID for consistency
1144
- )
1145
-
1146
- vulnerabilities.sort(key=get_epss_sort_key)
1147
- logger.info(f"Sorted {len(vulnerabilities)} vulnerabilities by EPSS score (highest to lowest)")
1148
-
1149
- message = f"Found {len(vulnerabilities)} vulnerabilities on page {page} of {total_pages}"
1150
- if filters_applied:
1151
- message += f" with filters: {filters_applied}"
1152
- if include_epss:
1153
- message += " (sorted by EPSS score, highest to lowest)"
1154
- else:
1155
- message += " (sorted by detection date and severity)"
1156
-
1069
return VulnerabilitySearchResponse(
1158
- vulnerabilities=vulnerabilities,
1070
+ vulnerabilities=[],
1071
total_count=total_count,
1072
critical_count=severity_counts["Critical"],
1073
high_count=severity_counts["High"],
@@ -1166,39 +1078,127 @@ async def search_vulnerabilities_from_indexer(
1078
total_pages=total_pages,
1079
has_next=has_next,
1080
has_previous=has_previous,
1169
- success=True,
1170
- message=message,
1081
+ success=False,
1082
+ message=(
1083
+ f"Deep pagination not supported beyond 10,000 results (requested page {page}, position {start_index}). "
1084
+ "Please use more specific filters to narrow down results or use the CSV export feature "
1085
+ "for accessing all vulnerabilities. Maximum supported page: 200 (with page_size=50)."
1086
+ ),
1087
filters_applied=filters_applied,
1088
)
1089
1174
- except Exception as e:
1175
- logger.error(f"Error searching vulnerabilities from indexer: {e}")
1176
- return VulnerabilitySearchResponse(
1177
- vulnerabilities=[],
1178
- total_count=0,
1179
- critical_count=0,
1180
- high_count=0,
1181
- medium_count=0,
1182
- low_count=0,
1183
- page=page,
1184
- page_size=page_size,
1185
- total_pages=0,
1186
- has_next=False,
1187
- has_previous=False,
1188
- success=False,
1189
- message=f"Failed to search vulnerabilities: {e}",
1190
- filters_applied=filters_applied if "filters_applied" in locals() else {},
1090
+ # Use standard pagination (within Elasticsearch limits)
1091
+ search_response = await es_client.search(
1092
+ index=",".join(vuln_indices),
1093
+ body={
1094
+ "query": es_query,
1095
+ "sort": [
1096
+ {"vulnerability.detected_at": {"order": "desc"}},
1097
+ {"vulnerability.severity": {"order": "asc"}},
1098
+ {"_id": {"order": "asc"}}, # Tiebreaker for consistent sorting
1099
+ ],
1100
+ "from": start_index,
1101
+ "size": page_size,
1102
+ },
1103
+ )
1104
+ hits = search_response["hits"]["hits"]
1105
+
1106
+ # Process the results
1107
+ vulnerabilities = []
1108
+ for hit in hits:
1109
+ try:
1110
+ source = hit["_source"]
1111
+ agent_data = source.get("agent", {})
1112
+ agent_hostname = agent_data.get("name", "unknown")
1113
+
1114
+ # Get customer code from our mapping
1115
+ agent_customer_code = customer_agent_map.get(agent_hostname)
1116
+
1117
+ # Process the vulnerability data
1118
+ vuln_data = process_wazuh_document(hit)
1119
+
1120
+ # Get EPSS score for the CVE (if requested)
1121
+ epss_score, epss_percentile = None, None
1122
+ if include_epss:
1123
+ epss_score, epss_percentile = await get_epss_score_for_cve(vuln_data.cve_id)
1124
+
1125
+ vulnerability_item = VulnerabilitySearchItem(
1126
+ cve_id=vuln_data.cve_id,
1127
+ severity=vuln_data.severity,
1128
+ title=vuln_data.title,
1129
+ agent_name=agent_hostname,
1130
+ customer_code=agent_customer_code,
1131
+ references=vuln_data.references,
1132
+ detected_at=vuln_data.detected_at,
1133
+ published_at=vuln_data.published_at,
1134
+ base_score=vuln_data.base_score,
1135
+ package_name=vuln_data.package_name,
1136
+ package_version=vuln_data.package_version,
1137
+ package_architecture=vuln_data.package_architecture,
1138
+ epss_score=epss_score,
1139
+ epss_percentile=epss_percentile,
1140
+ )
1141
+ vulnerabilities.append(vulnerability_item)
1142
+
1143
+ except Exception as e:
1144
+ logger.error(f"Error processing vulnerability document: {e}")
1145
+ continue
1146
+
1147
+ # Sort vulnerabilities by EPSS score if included
1148
+ if include_epss:
1149
+ severity_order = {"Critical": 0, "High": 1, "Medium": 2, "Low": 3}
1150
+
1151
+ def get_epss_sort_key(vuln):
1152
+ epss_score_val = vuln.epss_score
1153
+ if epss_score_val is None:
1154
+ epss_float = 0.0
1155
+ else:
1156
+ try:
1157
+ epss_float = float(epss_score_val)
1158
+ except (ValueError, TypeError):
1159
+ epss_float = 0.0
1160
+ return (
1161
+ -epss_float,
1162
+ severity_order.get(vuln.severity, 4),
1163
+ vuln.cve_id,
1164
+ )
1165
+
1166
+ vulnerabilities.sort(key=get_epss_sort_key)
1167
+ logger.info(f"Sorted {len(vulnerabilities)} vulnerabilities by EPSS score (highest to lowest)")
1168
+
1169
+ message = f"Found {len(vulnerabilities)} vulnerabilities on page {page} of {total_pages}"
1170
+ if filters_applied:
1171
+ message += f" with filters: {filters_applied}"
1172
+ if include_epss:
1173
+ message += " (sorted by EPSS score, highest to lowest)"
1174
+ else:
1175
+ message += " (sorted by detection date and severity)"
1176
+
1177
+ # Add helpful message when approaching the limit
1178
+ if start_index + page_size > 9000:
1179
+ message += (
1180
+ ". Note: Approaching pagination limit (10,000 results). Consider using filters or CSV export for complete data access."
1181
)
1192
- finally:
1193
- # Ensure the Elasticsearch client session is properly closed
1194
- if es_client:
1195
- try:
1196
- await es_client.close()
1197
- except Exception as close_error:
1198
- logger.warning(f"Error closing Elasticsearch client: {close_error}")
1182
+
1183
+ return VulnerabilitySearchResponse(
1184
+ vulnerabilities=vulnerabilities,
1185
+ total_count=total_count,
1186
+ critical_count=severity_counts["Critical"],
1187
+ high_count=severity_counts["High"],
1188
+ medium_count=severity_counts["Medium"],
1189
+ low_count=severity_counts["Low"],
1190
+ page=page,
1191
+ page_size=page_size,
1192
+ total_pages=total_pages,
1193
+ has_next=has_next,
1194
+ has_previous=has_previous,
1195
+ success=True,
1196
+ message=message,
1197
+ filters_applied=filters_applied,
1198
+ )
1199
1200
except Exception as e:
1201
- logger.error(f"Unexpected error in search_vulnerabilities_from_indexer: {e}")
1201
+ logger.error(f"Error searching vulnerabilities from indexer: {e}")
1202
return VulnerabilitySearchResponse(
1203
vulnerabilities=[],
1204
total_count=0,
@@ -1212,9 +1212,261 @@ async def search_vulnerabilities_from_indexer(
1212
has_next=False,
1213
has_previous=False,
1214
success=False,
1215
- message=f"Unexpected error occurred: {e}",
1215
+ message=f"Failed to search vulnerabilities: {e}",
1216
filters_applied=filters_applied if "filters_applied" in locals() else {},
1217
)
1218
+ finally:
1219
+ if es_client:
1220
+ try:
1221
+ await es_client.close()
1222
+ except Exception as close_error:
1223
+ logger.warning(f"Error closing Elasticsearch client: {close_error}")
1224
+
1225
+
1226
+async def fetch_all_vulnerabilities_for_export(
1227
+ db_session: AsyncSession,
1228
+ current_user: User,
1229
+ customer_code: str,
1230
+ agent_name: Optional[str] = None,
1231
+ severity: Optional[str] = None,
1232
+ cve_id: Optional[str] = None,
1233
+ package_name: Optional[str] = None,
1234
+ include_epss: bool = True,
1235
+) -> List[VulnerabilitySearchItem]:
1236
+ """
1237
+ Fetch ALL vulnerabilities for CSV export using scroll API (no pagination limits).
1238
+
1239
+ This function is specifically designed for report generation and can handle
1240
+ unlimited result sets by using Elasticsearch's scroll API.
1241
+
1242
+ Args:
1243
+ db_session: Database session for agent lookup
1244
+ current_user: Current authenticated user for customer access filtering
1245
+ customer_code: Customer code to filter by
1246
+ agent_name: Optional agent hostname filter
1247
+ severity: Optional severity filter
1248
+ cve_id: Optional CVE ID filter
1249
+ package_name: Optional package name filter
1250
+ include_epss: Whether to include EPSS scores
1251
+
1252
+ Returns:
1253
+ List of all matching vulnerabilities (no pagination)
1254
+ """
1255
+ logger.info(
1256
+ f"Fetching ALL vulnerabilities for export with filters: customer_code={customer_code}, "
1257
+ f"agent_name={agent_name}, severity={severity}, cve_id={cve_id}, "
1258
+ f"package_name={package_name}, include_epss={include_epss}",
1259
+ )
1260
+
1261
+ # Apply customer access filtering
1262
+ accessible_customers = await customer_access_handler.get_user_accessible_customers(current_user, db_session)
1263
+
1264
+ if "*" not in accessible_customers and customer_code not in accessible_customers:
1265
+ logger.warning(f"User {current_user.username} denied access to customer {customer_code}")
1266
+ return []
1267
+
1268
+ es_client = None
1269
+ scroll_id = None
1270
+
1271
+ try:
1272
+ # Initialize Elasticsearch client
1273
+ es_client = await create_wazuh_indexer_client_async("Wazuh-Indexer")
1274
+
1275
+ # Get all agents for customer code mapping
1276
+ all_agents_query = select(Agents)
1277
+ all_agents_result = await db_session.execute(all_agents_query)
1278
+ all_agents = all_agents_result.scalars().all()
1279
+
1280
+ # Build complete agent hostname to customer code mapping
1281
+ customer_agent_map = {}
1282
+ for agent in all_agents:
1283
+ if agent.hostname:
1284
+ customer_agent_map[agent.hostname] = agent.customer_code
1285
+
1286
+ # Get agent information for filtering
1287
+ agent_hostnames = []
1288
+ query = select(Agents).filter(Agents.customer_code == customer_code)
1289
+
1290
+ if agent_name:
1291
+ query = query.filter(Agents.hostname == agent_name)
1292
+
1293
+ result = await db_session.execute(query)
1294
+ agents = result.scalars().all()
1295
+
1296
+ if not agents:
1297
+ logger.warning(f"No agents found for customer {customer_code}")
1298
+ return []
1299
+
1300
+ for agent in agents:
1301
+ if agent.hostname:
1302
+ agent_hostnames.append(agent.hostname)
1303
+
1304
+ # Get vulnerability indices
1305
+ vuln_indices = await get_vulnerabilities_indices()
1306
+ if not vuln_indices:
1307
+ logger.warning("No vulnerability indices found")
1308
+ return []
1309
+
1310
+ # Build Elasticsearch query
1311
+ es_query = {"bool": {"must": []}}
1312
+
1313
+ # Add agent filter
1314
+ if agent_hostnames:
1315
+ es_query["bool"]["must"].append({"terms": {"agent.name": agent_hostnames}})
1316
+
1317
+ # Add severity filter
1318
+ if severity:
1319
+ es_query["bool"]["must"].append({"term": {"vulnerability.severity": severity}})
1320
+
1321
+ # Add CVE ID filter
1322
+ if cve_id:
1323
+ es_query["bool"]["must"].append({"term": {"vulnerability.id": cve_id}})
1324
+
1325
+ # Add package name filter
1326
+ if package_name:
1327
+ es_query["bool"]["must"].append({"wildcard": {"package.name": f"*{package_name}*"}})
1328
+
1329
+ # Use scroll API for unlimited results
1330
+ all_vulnerabilities = []
1331
+ scroll_size = 1000 # Process 1000 at a time
1332
+
1333
+ logger.info(f"Starting scroll search across {len(vuln_indices)} indices")
1334
+
1335
+ # Initial scroll request
1336
+ scroll_response = await es_client.search(
1337
+ index=",".join(vuln_indices),
1338
+ body={
1339
+ "query": es_query,
1340
+ "sort": [
1341
+ {"vulnerability.detected_at": {"order": "desc"}},
1342
+ {"vulnerability.severity": {"order": "asc"}},
1343
+ {"_id": {"order": "asc"}},
1344
+ ],
1345
+ },
1346
+ scroll="5m", # Keep scroll context alive for 5 minutes
1347
+ size=scroll_size,
1348
+ )
1349
+
1350
+ scroll_id = scroll_response["_scroll_id"]
1351
+ hits = scroll_response["hits"]["hits"]
1352
+
1353
+ logger.info(f"Initial scroll batch: {len(hits)} results")
1354
+
1355
+ # Process initial batch
1356
+ for hit in hits:
1357
+ try:
1358
+ source = hit["_source"]
1359
+ agent_data = source.get("agent", {})
1360
+ agent_hostname = agent_data.get("name", "unknown")
1361
+ agent_customer_code = customer_agent_map.get(agent_hostname)
1362
+
1363
+ vuln_data = process_wazuh_document(hit)
1364
+
1365
+ # Get EPSS score if requested
1366
+ epss_score, epss_percentile = None, None
1367
+ if include_epss:
1368
+ epss_score, epss_percentile = await get_epss_score_for_cve(vuln_data.cve_id)
1369
+
1370
+ vulnerability_item = VulnerabilitySearchItem(
1371
+ cve_id=vuln_data.cve_id,
1372
+ severity=vuln_data.severity,
1373
+ title=vuln_data.title,
1374
+ agent_name=agent_hostname,
1375
+ customer_code=agent_customer_code,
1376
+ references=vuln_data.references,
1377
+ detected_at=vuln_data.detected_at,
1378
+ published_at=vuln_data.published_at,
1379
+ base_score=vuln_data.base_score,
1380
+ package_name=vuln_data.package_name,
1381
+ package_version=vuln_data.package_version,
1382
+ package_architecture=vuln_data.package_architecture,
1383
+ epss_score=epss_score,
1384
+ epss_percentile=epss_percentile,
1385
+ )
1386
+ all_vulnerabilities.append(vulnerability_item)
1387
+
1388
+ except Exception as e:
1389
+ logger.error(f"Error processing vulnerability document: {e}")
1390
+ continue
1391
+
1392
+ # Continue scrolling through all results
1393
+ scroll_count = 1
1394
+ while len(hits) > 0:
1395
+ try:
1396
+ scroll_response = await es_client.scroll(scroll_id=scroll_id, scroll="5m")
1397
+ scroll_id = scroll_response["_scroll_id"]
1398
+ hits = scroll_response["hits"]["hits"]
1399
+
1400
+ if not hits:
1401
+ break
1402
+
1403
+ scroll_count += 1
1404
+ logger.info(f"Scroll batch {scroll_count}: {len(hits)} results (total so far: {len(all_vulnerabilities)})")
1405
+
1406
+ # Process batch
1407
+ for hit in hits:
1408
+ try:
1409
+ source = hit["_source"]
1410
+ agent_data = source.get("agent", {})
1411
+ agent_hostname = agent_data.get("name", "unknown")
1412
+ agent_customer_code = customer_agent_map.get(agent_hostname)
1413
+
1414
+ vuln_data = process_wazuh_document(hit)
1415
+
1416
+ # Get EPSS score if requested
1417
+ epss_score, epss_percentile = None, None
1418
+ if include_epss:
1419
+ epss_score, epss_percentile = await get_epss_score_for_cve(vuln_data.cve_id)
1420
+
1421
+ vulnerability_item = VulnerabilitySearchItem(
1422
+ cve_id=vuln_data.cve_id,
1423
+ severity=vuln_data.severity,
1424
+ title=vuln_data.title,
1425
+ agent_name=agent_hostname,
1426
+ customer_code=agent_customer_code,
1427
+ references=vuln_data.references,
1428
+ detected_at=vuln_data.detected_at,
1429
+ published_at=vuln_data.published_at,
1430
+ base_score=vuln_data.base_score,
1431
+ package_name=vuln_data.package_name,
1432
+ package_version=vuln_data.package_version,
1433
+ package_architecture=vuln_data.package_architecture,
1434
+ epss_score=epss_score,
1435
+ epss_percentile=epss_percentile,
1436
+ )
1437
+ all_vulnerabilities.append(vulnerability_item)
1438
+
1439
+ except Exception as e:
1440
+ logger.error(f"Error processing vulnerability document: {e}")
1441
+ continue
1442
+
1443
+ except Exception as scroll_error:
1444
+ logger.error(f"Error during scroll: {scroll_error}")
1445
+ break
1446
+
1447
+ logger.info(f"Successfully fetched {len(all_vulnerabilities)} total vulnerabilities using scroll API")
1448
+
1449
+ return all_vulnerabilities
1450
+
1451
+ except Exception as e:
1452
+ logger.error(f"Error fetching vulnerabilities for export: {e}")
1453
+ raise
1454
+
1455
+ finally:
1456
+ # Always clear the scroll context
1457
+ if scroll_id and es_client:
1458
+ try:
1459
+ await es_client.clear_scroll(scroll_id=scroll_id)
1460
+ logger.info("Cleared scroll context")
1461
+ except Exception as clear_error:
1462
+ logger.warning(f"Could not clear scroll context: {clear_error}")
1463
+
1464
+ # Close ES client
1465
+ if es_client:
1466
+ try:
1467
+ await es_client.close()
1468
+ except Exception as close_error:
1469
+ logger.warning(f"Error closing Elasticsearch client: {close_error}")
1470
1471
1472
async def generate_vulnerability_csv_report(
@@ -1279,50 +1531,36 @@ async def generate_vulnerability_csv_report(
1531
1532
logger.info(f"Generating vulnerability report for customer: {request.customer_code}")
1533
1282
- # Fetch ALL vulnerabilities (no pagination)
1283
- all_vulnerabilities = []
1284
- page = 1
1285
- page_size = 1000 # Large page size for efficiency
1286
-
1287
- while True:
1288
- search_result = await search_vulnerabilities_from_indexer(
1289
- db_session=db_session,
1290
- current_user=current_user,
1291
- customer_code=request.customer_code,
1292
- agent_name=request.agent_name,
1293
- severity=request.severity,
1294
- cve_id=request.cve_id,
1295
- package_name=request.package_name,
1296
- page=page,
1297
- page_size=page_size,
1298
- include_epss=request.include_epss,
1299
- )
1300
-
1301
- if not search_result.success:
1302
- # If we have a report_id, update it to failed status
1303
- if report_id:
1304
- stmt = select(VulnerabilityReport).filter(VulnerabilityReport.id == report_id)
1305
- result = await db_session.execute(stmt)
1306
- report = result.scalars().first()
1307
- if report:
1308
- report.status = "failed"
1309
- report.error_message = search_result.message
1310
- await db_session.commit()
1311
-
1312
- return VulnerabilityReportGenerateResponse(
1313
- success=False,
1314
- message="Failed to fetch vulnerability data",
1315
- error=search_result.message,
1316
- )
1317
-
1318
- all_vulnerabilities.extend(search_result.vulnerabilities)
1534
+ # Fetch ALL vulnerabilities using scroll API (no pagination limits)
1535
+ all_vulnerabilities = await fetch_all_vulnerabilities_for_export(
1536
+ db_session=db_session,
1537
+ current_user=current_user,
1538
+ customer_code=request.customer_code,
1539
+ agent_name=request.agent_name,
1540
+ severity=request.severity,
1541
+ cve_id=request.cve_id,
1542
+ package_name=request.package_name,
1543
+ include_epss=request.include_epss,
1544
+ )
1545
1320
- if not search_result.has_next:
1321
- break
1546
+ logger.info(f"Fetched {len(all_vulnerabilities)} vulnerabilities for report")
1547
1323
- page += 1
1548
+ if not all_vulnerabilities:
1549
+ # If we have a report_id, update it to failed status
1550
+ if report_id:
1551
+ stmt = select(VulnerabilityReport).filter(VulnerabilityReport.id == report_id)
1552
+ result = await db_session.execute(stmt)
1553
+ report = result.scalars().first()
1554
+ if report:
1555
+ report.status = "failed"
1556
+ report.error_message = "No vulnerabilities found matching criteria"
1557
+ await db_session.commit()
1558
1325
- logger.info(f"Fetched {len(all_vulnerabilities)} vulnerabilities for report")
1559
+ return VulnerabilityReportGenerateResponse(
1560
+ success=False,
1561
+ message="No vulnerabilities found matching the specified criteria",
1562
+ error="No data to export",
1563
+ )
1564
1565
# Generate CSV content
1566
csv_buffer = io.StringIO()
backend/app/version/services/version.py
+1
-1
@@ -7,7 +7,7 @@ from loguru import logger
7
from packaging.version import Version
8
9
# Current version - update this with each release
10
-CURRENT_VERSION = "0.1.17"
10
+CURRENT_VERSION = "0.1.18"
11
VERSION_CHECK_URL = "https://api.github.com/repos/socfortress/CoPilot/releases/latest"
12
13