Spaces:
Sleeping
Sleeping
Enhance URL scanning process by tracking attempted scans and handling permanent failures
Browse files
src/phising_detection/features/batch_url_scanner.py
CHANGED
|
@@ -85,35 +85,56 @@ def load_and_balance_feature_groups(
|
|
| 85 |
def get_already_scanned_urls(
|
| 86 |
project,
|
| 87 |
feature_group_name: str,
|
| 88 |
-
version: int
|
|
|
|
|
|
|
| 89 |
) -> set:
|
| 90 |
"""
|
| 91 |
-
Retrieve URLs that have already been scanned
|
| 92 |
|
| 93 |
Args:
|
| 94 |
project: Hopsworks project object
|
| 95 |
-
feature_group_name: Name of output feature group
|
| 96 |
version: Feature group version
|
|
|
|
|
|
|
| 97 |
|
| 98 |
Returns:
|
| 99 |
-
Set of URLs that have already been scanned (empty set if FG doesn't exist)
|
| 100 |
"""
|
|
|
|
|
|
|
|
|
|
| 101 |
try:
|
| 102 |
-
logger.info(f"Checking for
|
| 103 |
existing_df = hw.read_feature_group(project, feature_group_name, version)
|
| 104 |
|
| 105 |
if 'url' in existing_df.columns:
|
| 106 |
scanned_urls = set(existing_df['url'].dropna().unique())
|
| 107 |
-
logger.info(f"Found {len(scanned_urls)}
|
| 108 |
-
|
| 109 |
else:
|
| 110 |
logger.warning(f"Feature group exists but no 'url' column found")
|
| 111 |
-
return set()
|
| 112 |
|
| 113 |
except Exception as e:
|
| 114 |
logger.info(f"Output feature group not found or error reading it: {e}")
|
| 115 |
-
|
| 116 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
|
| 119 |
def filter_already_scanned(
|
|
@@ -151,12 +172,61 @@ def filter_already_scanned(
|
|
| 151 |
return filtered_df
|
| 152 |
|
| 153 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
def submit_url_batch(
|
| 155 |
client: URLScanClient,
|
| 156 |
urls: List[str],
|
| 157 |
visibility: str = "public",
|
| 158 |
delay_between_submissions: float = 1.0
|
| 159 |
-
) -> List[Dict[str, Any]]:
|
| 160 |
"""
|
| 161 |
Submit a batch of URLs for scanning (without waiting for results).
|
| 162 |
|
|
@@ -167,9 +237,12 @@ def submit_url_batch(
|
|
| 167 |
delay_between_submissions: Delay in seconds between submissions to respect rate limits
|
| 168 |
|
| 169 |
Returns:
|
| 170 |
-
|
|
|
|
|
|
|
| 171 |
"""
|
| 172 |
submissions = []
|
|
|
|
| 173 |
|
| 174 |
for i, url in enumerate(urls, 1):
|
| 175 |
logger.info(f"Submitting URL {i}/{len(urls)}: {url}")
|
|
@@ -182,8 +255,17 @@ def submit_url_batch(
|
|
| 182 |
logger.info(f"Successfully submitted: {url} (UUID: {submission.get('uuid')})")
|
| 183 |
|
| 184 |
except URLScanError as e:
|
| 185 |
-
|
| 186 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
continue
|
| 188 |
|
| 189 |
# Rate limiting: wait between submissions
|
|
@@ -191,7 +273,9 @@ def submit_url_batch(
|
|
| 191 |
time.sleep(delay_between_submissions)
|
| 192 |
|
| 193 |
logger.info(f"Submitted {len(submissions)}/{len(urls)} URLs successfully")
|
| 194 |
-
|
|
|
|
|
|
|
| 195 |
|
| 196 |
|
| 197 |
def retrieve_scan_results(
|
|
@@ -200,7 +284,7 @@ def retrieve_scan_results(
|
|
| 200 |
max_wait: int = 300,
|
| 201 |
poll_interval: int = 10,
|
| 202 |
initial_wait: int = 30
|
| 203 |
-
) -> List[Dict[str, Any]]:
|
| 204 |
"""
|
| 205 |
Retrieve results for submitted scans.
|
| 206 |
|
|
@@ -212,12 +296,15 @@ def retrieve_scan_results(
|
|
| 212 |
initial_wait: Time to wait before first poll attempt (seconds)
|
| 213 |
|
| 214 |
Returns:
|
| 215 |
-
|
|
|
|
|
|
|
| 216 |
"""
|
| 217 |
logger.info(f"Waiting {initial_wait} seconds for scans to complete...")
|
| 218 |
time.sleep(initial_wait)
|
| 219 |
|
| 220 |
results = []
|
|
|
|
| 221 |
pending_submissions = submissions.copy()
|
| 222 |
|
| 223 |
start_time = time.time()
|
|
@@ -237,11 +324,16 @@ def retrieve_scan_results(
|
|
| 237 |
logger.info(f"Retrieved result for {url} (UUID: {uuid})")
|
| 238 |
|
| 239 |
except URLScanError as e:
|
| 240 |
-
|
|
|
|
| 241 |
# Scan not ready yet, keep in pending list
|
| 242 |
still_pending.append(submission)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
else:
|
| 244 |
-
# Other error
|
| 245 |
logger.error(f"Failed to retrieve result for {url} (UUID: {uuid}): {e}")
|
| 246 |
|
| 247 |
pending_submissions = still_pending
|
|
@@ -250,13 +342,16 @@ def retrieve_scan_results(
|
|
| 250 |
logger.info(f"Still waiting for {len(pending_submissions)} scans. Waiting {poll_interval}s...")
|
| 251 |
time.sleep(poll_interval)
|
| 252 |
|
|
|
|
| 253 |
if pending_submissions:
|
| 254 |
-
logger.warning(f"Timeout: {len(pending_submissions)} scans did not complete in time")
|
| 255 |
for submission in pending_submissions:
|
| 256 |
logger.warning(f" - {submission.get('url')} (UUID: {submission.get('uuid')})")
|
| 257 |
|
| 258 |
logger.info(f"Successfully retrieved {len(results)}/{len(submissions)} scan results")
|
| 259 |
-
|
|
|
|
|
|
|
| 260 |
|
| 261 |
|
| 262 |
def process_and_upload_batch(
|
|
@@ -384,12 +479,14 @@ def main(
|
|
| 384 |
sample_size=sample_size
|
| 385 |
)
|
| 386 |
|
| 387 |
-
# Check for already scanned URLs
|
| 388 |
logger.info("Checking for already scanned URLs...")
|
| 389 |
scanned_urls = get_already_scanned_urls(
|
| 390 |
project=project,
|
| 391 |
feature_group_name=output_fg_name,
|
| 392 |
-
version=output_version
|
|
|
|
|
|
|
| 393 |
)
|
| 394 |
|
| 395 |
# Filter out already scanned URLs
|
|
@@ -430,7 +527,7 @@ def main(
|
|
| 430 |
|
| 431 |
# Phase 1: Submit all URLs for scanning
|
| 432 |
logger.info(f"Submitting {len(batch_urls)} URLs for scanning...")
|
| 433 |
-
submissions = submit_url_batch(
|
| 434 |
client=urlscan_client,
|
| 435 |
urls=batch_urls,
|
| 436 |
visibility="public",
|
|
@@ -440,7 +537,7 @@ def main(
|
|
| 440 |
# Phase 2: Retrieve scan results
|
| 441 |
if submissions:
|
| 442 |
logger.info(f"Retrieving results for {len(submissions)} submitted scans...")
|
| 443 |
-
scan_results = retrieve_scan_results(
|
| 444 |
client=urlscan_client,
|
| 445 |
submissions=submissions,
|
| 446 |
max_wait=300, # 5 minutes total wait time
|
|
@@ -449,6 +546,7 @@ def main(
|
|
| 449 |
)
|
| 450 |
else:
|
| 451 |
scan_results = []
|
|
|
|
| 452 |
logger.warning("No URLs were successfully submitted")
|
| 453 |
|
| 454 |
# Process and upload results
|
|
@@ -464,6 +562,45 @@ def main(
|
|
| 464 |
else:
|
| 465 |
logger.warning(f"No successful scans in batch {batch_num + 1}, skipping upload")
|
| 466 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
# Wait between batches to respect rate limits
|
| 468 |
if batch_num < total_batches - 1:
|
| 469 |
wait_time = 10
|
|
|
|
| 85 |
def get_already_scanned_urls(
|
| 86 |
project,
|
| 87 |
feature_group_name: str,
|
| 88 |
+
version: int,
|
| 89 |
+
attempted_fg_name: str = None,
|
| 90 |
+
attempted_fg_version: int = 1
|
| 91 |
) -> set:
|
| 92 |
"""
|
| 93 |
+
Retrieve URLs that have already been scanned or attempted from feature groups.
|
| 94 |
|
| 95 |
Args:
|
| 96 |
project: Hopsworks project object
|
| 97 |
+
feature_group_name: Name of output feature group (successful scans)
|
| 98 |
version: Feature group version
|
| 99 |
+
attempted_fg_name: Name of attempted scans tracking feature group (optional)
|
| 100 |
+
attempted_fg_version: Version of attempted scans feature group
|
| 101 |
|
| 102 |
Returns:
|
| 103 |
+
Set of URLs that have already been scanned or attempted (empty set if FG doesn't exist)
|
| 104 |
"""
|
| 105 |
+
all_attempted_urls = set()
|
| 106 |
+
|
| 107 |
+
# Check successful scans
|
| 108 |
try:
|
| 109 |
+
logger.info(f"Checking for successful scans in {feature_group_name} v{version}")
|
| 110 |
existing_df = hw.read_feature_group(project, feature_group_name, version)
|
| 111 |
|
| 112 |
if 'url' in existing_df.columns:
|
| 113 |
scanned_urls = set(existing_df['url'].dropna().unique())
|
| 114 |
+
logger.info(f"Found {len(scanned_urls)} successfully scanned URLs")
|
| 115 |
+
all_attempted_urls.update(scanned_urls)
|
| 116 |
else:
|
| 117 |
logger.warning(f"Feature group exists but no 'url' column found")
|
|
|
|
| 118 |
|
| 119 |
except Exception as e:
|
| 120 |
logger.info(f"Output feature group not found or error reading it: {e}")
|
| 121 |
+
|
| 122 |
+
# Check attempted scans (including failed ones)
|
| 123 |
+
if attempted_fg_name:
|
| 124 |
+
try:
|
| 125 |
+
logger.info(f"Checking for attempted scans in {attempted_fg_name} v{attempted_fg_version}")
|
| 126 |
+
attempted_df = hw.read_feature_group(project, attempted_fg_name, attempted_fg_version)
|
| 127 |
+
|
| 128 |
+
if 'url' in attempted_df.columns:
|
| 129 |
+
attempted_urls = set(attempted_df['url'].dropna().unique())
|
| 130 |
+
logger.info(f"Found {len(attempted_urls)} attempted URLs (including failures)")
|
| 131 |
+
all_attempted_urls.update(attempted_urls)
|
| 132 |
+
|
| 133 |
+
except Exception as e:
|
| 134 |
+
logger.info(f"Attempted scans feature group not found: {e}")
|
| 135 |
+
|
| 136 |
+
logger.info(f"Total URLs to skip (successful + attempted): {len(all_attempted_urls)}")
|
| 137 |
+
return all_attempted_urls
|
| 138 |
|
| 139 |
|
| 140 |
def filter_already_scanned(
|
|
|
|
| 172 |
return filtered_df
|
| 173 |
|
| 174 |
|
| 175 |
+
def record_attempted_scans(
|
| 176 |
+
project,
|
| 177 |
+
urls: List[str],
|
| 178 |
+
statuses: List[str],
|
| 179 |
+
uuids: List[str] = None,
|
| 180 |
+
feature_group_name: str = "attempted_scans",
|
| 181 |
+
version: int = 1
|
| 182 |
+
):
|
| 183 |
+
"""
|
| 184 |
+
Record attempted scans (both successful and failed) to prevent re-trying.
|
| 185 |
+
|
| 186 |
+
Args:
|
| 187 |
+
project: Hopsworks project object
|
| 188 |
+
urls: List of URLs that were attempted
|
| 189 |
+
statuses: List of status strings ('submitted', 'success', 'failed', 'timeout')
|
| 190 |
+
uuids: Optional list of scan UUIDs
|
| 191 |
+
feature_group_name: Name of tracking feature group
|
| 192 |
+
version: Feature group version
|
| 193 |
+
"""
|
| 194 |
+
if not urls:
|
| 195 |
+
return
|
| 196 |
+
|
| 197 |
+
import datetime
|
| 198 |
+
|
| 199 |
+
# Create DataFrame of attempted scans
|
| 200 |
+
attempted_df = pd.DataFrame({
|
| 201 |
+
'url': urls,
|
| 202 |
+
'status': statuses,
|
| 203 |
+
'timestamp': [datetime.datetime.now()] * len(urls)
|
| 204 |
+
})
|
| 205 |
+
|
| 206 |
+
logger.info(f"Recording {len(attempted_df)} attempted scans")
|
| 207 |
+
|
| 208 |
+
try:
|
| 209 |
+
hw.upload_dataframe_to_feature_group(
|
| 210 |
+
project=project,
|
| 211 |
+
df=attempted_df,
|
| 212 |
+
feature_group_name=feature_group_name,
|
| 213 |
+
version=version,
|
| 214 |
+
description="Tracking of all attempted URL scans (successful and failed)",
|
| 215 |
+
primary_key=["url"],
|
| 216 |
+
online_enabled=False,
|
| 217 |
+
write_options={"wait_for_job": False} # Don't wait, just record async
|
| 218 |
+
)
|
| 219 |
+
logger.info(f"Recorded attempted scans to {feature_group_name}")
|
| 220 |
+
except Exception as e:
|
| 221 |
+
logger.warning(f"Failed to record attempted scans: {e}")
|
| 222 |
+
|
| 223 |
+
|
| 224 |
def submit_url_batch(
|
| 225 |
client: URLScanClient,
|
| 226 |
urls: List[str],
|
| 227 |
visibility: str = "public",
|
| 228 |
delay_between_submissions: float = 1.0
|
| 229 |
+
) -> tuple[List[Dict[str, Any]], List[Dict[str, str]]]:
|
| 230 |
"""
|
| 231 |
Submit a batch of URLs for scanning (without waiting for results).
|
| 232 |
|
|
|
|
| 237 |
delay_between_submissions: Delay in seconds between submissions to respect rate limits
|
| 238 |
|
| 239 |
Returns:
|
| 240 |
+
Tuple of (submissions list, permanent_failures list)
|
| 241 |
+
- submissions: List of submission dicts with 'url', 'uuid', 'api'
|
| 242 |
+
- permanent_failures: List of {'url', 'error'} for non-retryable failures
|
| 243 |
"""
|
| 244 |
submissions = []
|
| 245 |
+
permanent_failures = []
|
| 246 |
|
| 247 |
for i, url in enumerate(urls, 1):
|
| 248 |
logger.info(f"Submitting URL {i}/{len(urls)}: {url}")
|
|
|
|
| 255 |
logger.info(f"Successfully submitted: {url} (UUID: {submission.get('uuid')})")
|
| 256 |
|
| 257 |
except URLScanError as e:
|
| 258 |
+
error_msg = str(e).lower()
|
| 259 |
+
# Check if this is a permanent failure or temporary (rate limit)
|
| 260 |
+
if "rate limit" in error_msg or "429" in error_msg:
|
| 261 |
+
logger.warning(f"Rate limit hit for {url} - will retry later")
|
| 262 |
+
# Don't add to permanent failures - this can be retried
|
| 263 |
+
elif "bad request" in error_msg or "invalid" in error_msg:
|
| 264 |
+
logger.error(f"Permanent failure for {url}: {e}")
|
| 265 |
+
permanent_failures.append({'url': url, 'error': str(e)})
|
| 266 |
+
else:
|
| 267 |
+
logger.error(f"Failed to submit {url}: {e}")
|
| 268 |
+
# Unknown error - don't record as permanent for safety
|
| 269 |
continue
|
| 270 |
|
| 271 |
# Rate limiting: wait between submissions
|
|
|
|
| 273 |
time.sleep(delay_between_submissions)
|
| 274 |
|
| 275 |
logger.info(f"Submitted {len(submissions)}/{len(urls)} URLs successfully")
|
| 276 |
+
if permanent_failures:
|
| 277 |
+
logger.info(f"Permanent failures: {len(permanent_failures)}")
|
| 278 |
+
return submissions, permanent_failures
|
| 279 |
|
| 280 |
|
| 281 |
def retrieve_scan_results(
|
|
|
|
| 284 |
max_wait: int = 300,
|
| 285 |
poll_interval: int = 10,
|
| 286 |
initial_wait: int = 30
|
| 287 |
+
) -> tuple[List[Dict[str, Any]], List[Dict[str, str]]]:
|
| 288 |
"""
|
| 289 |
Retrieve results for submitted scans.
|
| 290 |
|
|
|
|
| 296 |
initial_wait: Time to wait before first poll attempt (seconds)
|
| 297 |
|
| 298 |
Returns:
|
| 299 |
+
Tuple of (results list, permanent_failures list)
|
| 300 |
+
- results: List of scan results (successful retrievals only)
|
| 301 |
+
- permanent_failures: List of {'url', 'error'} for non-retryable failures (excludes timeouts)
|
| 302 |
"""
|
| 303 |
logger.info(f"Waiting {initial_wait} seconds for scans to complete...")
|
| 304 |
time.sleep(initial_wait)
|
| 305 |
|
| 306 |
results = []
|
| 307 |
+
permanent_failures = []
|
| 308 |
pending_submissions = submissions.copy()
|
| 309 |
|
| 310 |
start_time = time.time()
|
|
|
|
| 324 |
logger.info(f"Retrieved result for {url} (UUID: {uuid})")
|
| 325 |
|
| 326 |
except URLScanError as e:
|
| 327 |
+
error_msg = str(e).lower()
|
| 328 |
+
if "not found or not ready" in error_msg:
|
| 329 |
# Scan not ready yet, keep in pending list
|
| 330 |
still_pending.append(submission)
|
| 331 |
+
elif "dns" in error_msg or "domain" in error_msg or "unreachable" in error_msg:
|
| 332 |
+
# Permanent DNS/domain failures - won't work on retry
|
| 333 |
+
logger.error(f"Permanent failure for {url} (UUID: {uuid}): {e}")
|
| 334 |
+
permanent_failures.append({'url': url, 'error': str(e)})
|
| 335 |
else:
|
| 336 |
+
# Other error - log but don't record as permanent for safety
|
| 337 |
logger.error(f"Failed to retrieve result for {url} (UUID: {uuid}): {e}")
|
| 338 |
|
| 339 |
pending_submissions = still_pending
|
|
|
|
| 342 |
logger.info(f"Still waiting for {len(pending_submissions)} scans. Waiting {poll_interval}s...")
|
| 343 |
time.sleep(poll_interval)
|
| 344 |
|
| 345 |
+
# Timeouts are NOT permanent failures - scans might just be slow
|
| 346 |
if pending_submissions:
|
| 347 |
+
logger.warning(f"Timeout: {len(pending_submissions)} scans did not complete in time (will retry later)")
|
| 348 |
for submission in pending_submissions:
|
| 349 |
logger.warning(f" - {submission.get('url')} (UUID: {submission.get('uuid')})")
|
| 350 |
|
| 351 |
logger.info(f"Successfully retrieved {len(results)}/{len(submissions)} scan results")
|
| 352 |
+
if permanent_failures:
|
| 353 |
+
logger.info(f"Permanent failures: {len(permanent_failures)}")
|
| 354 |
+
return results, permanent_failures
|
| 355 |
|
| 356 |
|
| 357 |
def process_and_upload_batch(
|
|
|
|
| 479 |
sample_size=sample_size
|
| 480 |
)
|
| 481 |
|
| 482 |
+
# Check for already scanned URLs (including failed attempts)
|
| 483 |
logger.info("Checking for already scanned URLs...")
|
| 484 |
scanned_urls = get_already_scanned_urls(
|
| 485 |
project=project,
|
| 486 |
feature_group_name=output_fg_name,
|
| 487 |
+
version=output_version,
|
| 488 |
+
attempted_fg_name="attempted_scans", # Track failed scans too
|
| 489 |
+
attempted_fg_version=1
|
| 490 |
)
|
| 491 |
|
| 492 |
# Filter out already scanned URLs
|
|
|
|
| 527 |
|
| 528 |
# Phase 1: Submit all URLs for scanning
|
| 529 |
logger.info(f"Submitting {len(batch_urls)} URLs for scanning...")
|
| 530 |
+
submissions, submission_failures = submit_url_batch(
|
| 531 |
client=urlscan_client,
|
| 532 |
urls=batch_urls,
|
| 533 |
visibility="public",
|
|
|
|
| 537 |
# Phase 2: Retrieve scan results
|
| 538 |
if submissions:
|
| 539 |
logger.info(f"Retrieving results for {len(submissions)} submitted scans...")
|
| 540 |
+
scan_results, retrieval_failures = retrieve_scan_results(
|
| 541 |
client=urlscan_client,
|
| 542 |
submissions=submissions,
|
| 543 |
max_wait=300, # 5 minutes total wait time
|
|
|
|
| 546 |
)
|
| 547 |
else:
|
| 548 |
scan_results = []
|
| 549 |
+
retrieval_failures = []
|
| 550 |
logger.warning("No URLs were successfully submitted")
|
| 551 |
|
| 552 |
# Process and upload results
|
|
|
|
| 562 |
else:
|
| 563 |
logger.warning(f"No successful scans in batch {batch_num + 1}, skipping upload")
|
| 564 |
|
| 565 |
+
# Record ONLY successful scans and permanent failures (not timeouts or rate limits)
|
| 566 |
+
successful_urls = {result.get('original_url') or result.get('task', {}).get('url')
|
| 567 |
+
for result in scan_results}
|
| 568 |
+
|
| 569 |
+
attempted_urls = []
|
| 570 |
+
attempted_statuses = []
|
| 571 |
+
attempted_uuids = []
|
| 572 |
+
|
| 573 |
+
# Record successful scans
|
| 574 |
+
for result in scan_results:
|
| 575 |
+
url = result.get('original_url') or result.get('task', {}).get('url')
|
| 576 |
+
uuid = result.get('task', {}).get('uuid')
|
| 577 |
+
attempted_urls.append(url)
|
| 578 |
+
attempted_statuses.append('success')
|
| 579 |
+
attempted_uuids.append(uuid)
|
| 580 |
+
|
| 581 |
+
# Record permanent failures from submission (invalid URLs, etc.)
|
| 582 |
+
for failure in submission_failures:
|
| 583 |
+
attempted_urls.append(failure['url'])
|
| 584 |
+
attempted_statuses.append('failed_permanent')
|
| 585 |
+
attempted_uuids.append(None)
|
| 586 |
+
|
| 587 |
+
# Record permanent failures from retrieval (DNS errors, etc.)
|
| 588 |
+
for failure in retrieval_failures:
|
| 589 |
+
attempted_urls.append(failure['url'])
|
| 590 |
+
attempted_statuses.append('failed_permanent')
|
| 591 |
+
attempted_uuids.append(None)
|
| 592 |
+
|
| 593 |
+
# Only record if we have something to record
|
| 594 |
+
if attempted_urls:
|
| 595 |
+
record_attempted_scans(
|
| 596 |
+
project=project,
|
| 597 |
+
urls=attempted_urls,
|
| 598 |
+
statuses=attempted_statuses,
|
| 599 |
+
uuids=attempted_uuids,
|
| 600 |
+
feature_group_name="attempted_scans",
|
| 601 |
+
version=1
|
| 602 |
+
)
|
| 603 |
+
|
| 604 |
# Wait between batches to respect rate limits
|
| 605 |
if batch_num < total_batches - 1:
|
| 606 |
wait_time = 10
|