Full Code of yyhuni/xingrin for AI

main 2a0e09c4eea6 cached
828 files
11.1 MB
3.0M tokens
3062 symbols
1 requests
Copy disabled (too large) Download .txt
Showing preview only (12,174K chars total). Download the full file to get everything.
Repository: yyhuni/xingrin
Branch: main
Commit: 2a0e09c4eea6
Files: 828
Total size: 11.1 MB

Directory structure:
gitextract__pp8fuf4/

├── .dockerignore
├── .github/
│   └── workflows/
│       └── docker-build.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── VERSION
├── backend/
│   ├── .gitignore
│   ├── apps/
│   │   ├── __init__.py
│   │   ├── asset/
│   │   │   ├── __init__.py
│   │   │   ├── apps.py
│   │   │   ├── dtos/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── asset/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── directory_dto.py
│   │   │   │   │   ├── endpoint_dto.py
│   │   │   │   │   ├── host_port_mapping_dto.py
│   │   │   │   │   ├── ip_address_dto.py
│   │   │   │   │   ├── port_dto.py
│   │   │   │   │   ├── subdomain_dto.py
│   │   │   │   │   ├── vulnerability_dto.py
│   │   │   │   │   └── website_dto.py
│   │   │   │   └── snapshot/
│   │   │   │       ├── __init__.py
│   │   │   │       ├── directory_snapshot_dto.py
│   │   │   │       ├── endpoint_snapshot_dto.py
│   │   │   │       ├── host_port_mapping_snapshot_dto.py
│   │   │   │       ├── subdomain_snapshot_dto.py
│   │   │   │       ├── vulnerability_snapshot_dto.py
│   │   │   │       └── website_snapshot_dto.py
│   │   │   ├── migrations/
│   │   │   │   ├── 0001_initial.py
│   │   │   │   ├── 0002_create_search_views.py
│   │   │   │   ├── 0003_add_screenshot_models.py
│   │   │   │   ├── 0004_add_status_code_to_screenshot.py
│   │   │   │   └── __init__.py
│   │   │   ├── models/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── asset_models.py
│   │   │   │   ├── screenshot_models.py
│   │   │   │   ├── snapshot_models.py
│   │   │   │   └── statistics_models.py
│   │   │   ├── repositories/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── asset/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── directory_repository.py
│   │   │   │   │   ├── endpoint_repository.py
│   │   │   │   │   ├── host_port_mapping_repository.py
│   │   │   │   │   ├── subdomain_repository.py
│   │   │   │   │   └── website_repository.py
│   │   │   │   ├── snapshot/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── directory_snapshot_repository.py
│   │   │   │   │   ├── endpoint_snapshot_repository.py
│   │   │   │   │   ├── host_port_mapping_snapshot_repository.py
│   │   │   │   │   ├── subdomain_snapshot_repository.py
│   │   │   │   │   ├── vulnerability_snapshot_repository.py
│   │   │   │   │   └── website_snapshot_repository.py
│   │   │   │   └── statistics_repository.py
│   │   │   ├── serializers.py
│   │   │   ├── services/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── asset/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── directory_service.py
│   │   │   │   │   ├── endpoint_service.py
│   │   │   │   │   ├── host_port_mapping_service.py
│   │   │   │   │   ├── subdomain_service.py
│   │   │   │   │   ├── vulnerability_service.py
│   │   │   │   │   └── website_service.py
│   │   │   │   ├── playwright_screenshot_service.py
│   │   │   │   ├── screenshot_service.py
│   │   │   │   ├── search_service.py
│   │   │   │   ├── snapshot/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── directory_snapshots_service.py
│   │   │   │   │   ├── endpoint_snapshots_service.py
│   │   │   │   │   ├── host_port_mapping_snapshots_service.py
│   │   │   │   │   ├── subdomain_snapshots_service.py
│   │   │   │   │   ├── vulnerability_snapshots_service.py
│   │   │   │   │   └── website_snapshots_service.py
│   │   │   │   └── statistics_service.py
│   │   │   ├── urls.py
│   │   │   └── views/
│   │   │       ├── __init__.py
│   │   │       ├── asset_views.py
│   │   │       └── search_views.py
│   │   ├── common/
│   │   │   ├── __init__.py
│   │   │   ├── apps.py
│   │   │   ├── authentication.py
│   │   │   ├── container_bootstrap.py
│   │   │   ├── decorators/
│   │   │   │   ├── __init__.py
│   │   │   │   └── db_connection.py
│   │   │   ├── definitions.py
│   │   │   ├── error_codes.py
│   │   │   ├── exception_handlers.py
│   │   │   ├── management/
│   │   │   │   └── commands/
│   │   │   │       ├── db_health_check.py
│   │   │   │       ├── db_monitor.py
│   │   │   │       └── init_admin.py
│   │   │   ├── migrations/
│   │   │   │   ├── 0001_initial.py
│   │   │   │   └── __init__.py
│   │   │   ├── models/
│   │   │   │   ├── __init__.py
│   │   │   │   └── blacklist.py
│   │   │   ├── normalizer.py
│   │   │   ├── pagination.py
│   │   │   ├── permissions.py
│   │   │   ├── prefect_django_setup.py
│   │   │   ├── response_helpers.py
│   │   │   ├── serializers/
│   │   │   │   ├── __init__.py
│   │   │   │   └── blacklist_serializers.py
│   │   │   ├── services/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── blacklist_service.py
│   │   │   │   └── system_log_service.py
│   │   │   ├── signals.py
│   │   │   ├── urls.py
│   │   │   ├── utils/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── blacklist_filter.py
│   │   │   │   ├── csv_utils.py
│   │   │   │   ├── dedup.py
│   │   │   │   ├── filter_utils.py
│   │   │   │   └── hash.py
│   │   │   ├── validators.py
│   │   │   ├── views/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── auth_views.py
│   │   │   │   ├── blacklist_views.py
│   │   │   │   ├── health_views.py
│   │   │   │   ├── system_log_views.py
│   │   │   │   └── version_views.py
│   │   │   └── websocket_auth.py
│   │   ├── engine/
│   │   │   ├── __init__.py
│   │   │   ├── apps.py
│   │   │   ├── consumers/
│   │   │   │   ├── __init__.py
│   │   │   │   └── worker_deploy_consumer.py
│   │   │   ├── management/
│   │   │   │   ├── __init__.py
│   │   │   │   └── commands/
│   │   │   │       ├── __init__.py
│   │   │   │       ├── init_default_engine.py
│   │   │   │       ├── init_fingerprints.py
│   │   │   │       ├── init_nuclei_templates.py
│   │   │   │       └── init_wordlists.py
│   │   │   ├── migrations/
│   │   │   │   ├── 0001_initial.py
│   │   │   │   └── __init__.py
│   │   │   ├── models/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── engine.py
│   │   │   │   └── fingerprints.py
│   │   │   ├── repositories/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── django_engine_repository.py
│   │   │   │   ├── django_wordlist_repository.py
│   │   │   │   ├── django_worker_repository.py
│   │   │   │   └── nuclei_repo_repository.py
│   │   │   ├── routing.py
│   │   │   ├── scheduler.py
│   │   │   ├── serializers/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── engine_serializer.py
│   │   │   │   ├── fingerprints/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── arl.py
│   │   │   │   │   ├── ehole.py
│   │   │   │   │   ├── fingerprinthub.py
│   │   │   │   │   ├── fingers.py
│   │   │   │   │   ├── goby.py
│   │   │   │   │   └── wappalyzer.py
│   │   │   │   ├── nuclei_template_repo_serializer.py
│   │   │   │   ├── wordlist_serializer.py
│   │   │   │   └── worker_serializer.py
│   │   │   ├── services/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── deploy_service.py
│   │   │   │   ├── engine_service.py
│   │   │   │   ├── fingerprints/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── arl_service.py
│   │   │   │   │   ├── base.py
│   │   │   │   │   ├── ehole.py
│   │   │   │   │   ├── fingerprinthub_service.py
│   │   │   │   │   ├── fingers_service.py
│   │   │   │   │   ├── goby.py
│   │   │   │   │   └── wappalyzer.py
│   │   │   │   ├── nuclei_template_repo_service.py
│   │   │   │   ├── task_distributor.py
│   │   │   │   ├── wordlist_service.py
│   │   │   │   ├── worker_load_service.py
│   │   │   │   └── worker_service.py
│   │   │   ├── urls.py
│   │   │   └── views/
│   │   │       ├── __init__.py
│   │   │       ├── engine_views.py
│   │   │       ├── fingerprints/
│   │   │       │   ├── __init__.py
│   │   │       │   ├── arl.py
│   │   │       │   ├── base.py
│   │   │       │   ├── ehole.py
│   │   │       │   ├── fingerprinthub.py
│   │   │       │   ├── fingers.py
│   │   │       │   ├── goby.py
│   │   │       │   └── wappalyzer.py
│   │   │       ├── nuclei_template_repo_views.py
│   │   │       ├── wordlist_views.py
│   │   │       └── worker_views.py
│   │   ├── scan/
│   │   │   ├── __init__.py
│   │   │   ├── apps.py
│   │   │   ├── configs/
│   │   │   │   ├── command_templates.py
│   │   │   │   └── engine_config_example.yaml
│   │   │   ├── flows/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── directory_scan_flow.py
│   │   │   │   ├── fingerprint_detect_flow.py
│   │   │   │   ├── initiate_scan_flow.py
│   │   │   │   ├── port_scan_flow.py
│   │   │   │   ├── screenshot_flow.py
│   │   │   │   ├── site_scan_flow.py
│   │   │   │   ├── subdomain_discovery_flow.py
│   │   │   │   ├── url_fetch/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── domain_name_url_fetch_flow.py
│   │   │   │   │   ├── main_flow.py
│   │   │   │   │   ├── sites_url_fetch_flow.py
│   │   │   │   │   └── utils.py
│   │   │   │   └── vuln_scan/
│   │   │   │       ├── __init__.py
│   │   │   │       ├── endpoints_vuln_scan_flow.py
│   │   │   │       ├── main_flow.py
│   │   │   │       └── utils.py
│   │   │   ├── handlers/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── initiate_scan_flow_handlers.py
│   │   │   │   └── scan_flow_handlers.py
│   │   │   ├── management/
│   │   │   │   └── __init__.py
│   │   │   ├── migrations/
│   │   │   │   ├── 0001_initial.py
│   │   │   │   ├── 0002_add_cached_screenshots_count.py
│   │   │   │   ├── 0003_add_wecom_fields.py
│   │   │   │   └── __init__.py
│   │   │   ├── models/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── scan_log_model.py
│   │   │   │   ├── scan_models.py
│   │   │   │   ├── scheduled_scan_model.py
│   │   │   │   └── subfinder_provider_settings_model.py
│   │   │   ├── notifications/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── consumers.py
│   │   │   │   ├── models.py
│   │   │   │   ├── receivers.py
│   │   │   │   ├── repositories.py
│   │   │   │   ├── routing.py
│   │   │   │   ├── serializers.py
│   │   │   │   ├── services.py
│   │   │   │   ├── types.py
│   │   │   │   ├── urls.py
│   │   │   │   └── views.py
│   │   │   ├── orchestrators/
│   │   │   │   ├── __init__.py
│   │   │   │   └── flow_orchestrator.py
│   │   │   ├── providers/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── base.py
│   │   │   │   ├── database_provider.py
│   │   │   │   ├── list_provider.py
│   │   │   │   ├── pipeline_provider.py
│   │   │   │   ├── snapshot_provider.py
│   │   │   │   └── tests/
│   │   │   │       ├── __init__.py
│   │   │   │       ├── test_common_properties.py
│   │   │   │       ├── test_database_provider.py
│   │   │   │       ├── test_list_provider.py
│   │   │   │       ├── test_pipeline_provider.py
│   │   │   │       └── test_snapshot_provider.py
│   │   │   ├── repositories/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── django_scan_repository.py
│   │   │   │   └── scheduled_scan_repository.py
│   │   │   ├── scripts/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── run_cleanup.py
│   │   │   │   ├── run_delete_scans.py
│   │   │   │   ├── run_initiate_scan.py
│   │   │   │   └── run_scheduled_scan.py
│   │   │   ├── serializers/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── mixins.py
│   │   │   │   ├── scan_log_serializers.py
│   │   │   │   ├── scan_serializers.py
│   │   │   │   ├── scheduled_scan_serializers.py
│   │   │   │   └── subfinder_provider_settings_serializers.py
│   │   │   ├── services/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── quick_scan_service.py
│   │   │   │   ├── scan_control_service.py
│   │   │   │   ├── scan_creation_service.py
│   │   │   │   ├── scan_service.py
│   │   │   │   ├── scan_state_service.py
│   │   │   │   ├── scan_stats_service.py
│   │   │   │   ├── scheduled_scan_service.py
│   │   │   │   ├── subfinder_provider_config_service.py
│   │   │   │   └── target_export_service.py
│   │   │   ├── tasks/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── directory_scan/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── export_sites_task.py
│   │   │   │   │   └── run_and_stream_save_directories_task.py
│   │   │   │   ├── fingerprint_detect/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── export_urls_task.py
│   │   │   │   │   └── run_xingfinger_task.py
│   │   │   │   ├── port_scan/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── export_hosts_task.py
│   │   │   │   │   ├── run_and_stream_save_ports_task.py
│   │   │   │   │   └── types.py
│   │   │   │   ├── screenshot/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   └── capture_screenshots_task.py
│   │   │   │   ├── site_scan/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── export_site_urls_task.py
│   │   │   │   │   └── run_and_stream_save_websites_task.py
│   │   │   │   ├── subdomain_discovery/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── merge_and_validate_task.py
│   │   │   │   │   ├── run_subdomain_discovery_task.py
│   │   │   │   │   └── save_domains_task.py
│   │   │   │   ├── tests/
│   │   │   │   │   └── test_task_backward_compatibility.py
│   │   │   │   ├── url_fetch/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── clean_urls_task.py
│   │   │   │   │   ├── export_sites_task.py
│   │   │   │   │   ├── merge_and_deduplicate_urls_task.py
│   │   │   │   │   ├── run_and_stream_save_urls_task.py
│   │   │   │   │   ├── run_url_fetcher_task.py
│   │   │   │   │   └── save_urls_task.py
│   │   │   │   └── vuln_scan/
│   │   │   │       ├── __init__.py
│   │   │   │       ├── export_endpoints_task.py
│   │   │   │       ├── run_and_stream_save_dalfox_vulns_task.py
│   │   │   │       ├── run_and_stream_save_nuclei_vulns_task.py
│   │   │   │       └── run_vuln_tool_task.py
│   │   │   ├── urls.py
│   │   │   ├── utils/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── command_builder.py
│   │   │   │   ├── command_executor.py
│   │   │   │   ├── config_merger.py
│   │   │   │   ├── config_parser.py
│   │   │   │   ├── directory_cleanup.py
│   │   │   │   ├── fingerprint_helpers.py
│   │   │   │   ├── nuclei_helpers.py
│   │   │   │   ├── performance.py
│   │   │   │   ├── system_load.py
│   │   │   │   ├── user_logger.py
│   │   │   │   ├── wordlist_helpers.py
│   │   │   │   └── workspace_utils.py
│   │   │   └── views/
│   │   │       ├── __init__.py
│   │   │       ├── scan_log_views.py
│   │   │       ├── scan_views.py
│   │   │       ├── scheduled_scan_views.py
│   │   │       └── subfinder_provider_settings_views.py
│   │   └── targets/
│   │       ├── __init__.py
│   │       ├── apps.py
│   │       ├── migrations/
│   │       │   ├── 0001_initial.py
│   │       │   └── __init__.py
│   │       ├── models.py
│   │       ├── repositories/
│   │       │   ├── __init__.py
│   │       │   ├── django_organization_repository.py
│   │       │   └── django_target_repository.py
│   │       ├── scripts/
│   │       │   ├── __init__.py
│   │       │   ├── run_delete_organizations.py
│   │       │   └── run_delete_targets.py
│   │       ├── serializers.py
│   │       ├── services/
│   │       │   ├── __init__.py
│   │       │   ├── organization_service.py
│   │       │   └── target_service.py
│   │       ├── urls.py
│   │       └── views.py
│   ├── config/
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── logging_config.py
│   │   ├── settings.py
│   │   └── urls.py
│   ├── fingerprints/
│   │   ├── ARL.yaml
│   │   ├── ehole.json
│   │   ├── fingerprinthub_web.json
│   │   ├── fingers_http.json
│   │   ├── goby.json
│   │   └── wappalyzer.json
│   ├── manage.py
│   ├── pyproject.toml
│   ├── requirements.txt
│   ├── resources/
│   │   └── resolvers.txt
│   ├── scripts/
│   │   ├── generate_test_data_sql.py
│   │   ├── performance/
│   │   │   ├── monitor_pg_performance.sh
│   │   │   ├── pg_stats_after_test.sql
│   │   │   ├── pg_stats_before_test.sql
│   │   │   └── start_performance_test.sh
│   │   └── worker-deploy/
│   │       ├── agent.sh
│   │       ├── bootstrap.sh
│   │       ├── install.sh
│   │       ├── start-agent.sh
│   │       └── uninstall.sh
│   └── wordlist/
│       ├── dir_default.txt
│       └── subdomains-top1million-110000.txt
├── docker/
│   ├── agent/
│   │   └── Dockerfile
│   ├── docker-compose.dev.yml
│   ├── docker-compose.yml
│   ├── frontend/
│   │   └── Dockerfile
│   ├── nginx/
│   │   ├── Dockerfile
│   │   ├── nginx.conf
│   │   └── ssl/
│   │       └── README.md
│   ├── postgres/
│   │   ├── Dockerfile
│   │   └── init-user-db.sh
│   ├── restart.sh
│   ├── scripts/
│   │   ├── common.sh
│   │   ├── init-data.sh
│   │   ├── install-pg-ivm.sh
│   │   ├── setup-swap.sh
│   │   ├── setup-system-monitor.sh
│   │   └── test-pg-ivm.sh
│   ├── server/
│   │   ├── Dockerfile
│   │   └── start.sh
│   ├── start.sh
│   ├── stop.sh
│   └── worker/
│       └── Dockerfile
├── docker-push.sh
├── docs/
│   ├── README.md
│   ├── nuclei-template-architecture.md
│   ├── plans/
│   │   ├── 2026-03-08-oss-readiness-design.md
│   │   └── 2026-03-08-oss-readiness.md
│   ├── quick-start.md
│   ├── scan-flow-architecture.md
│   ├── version-management.md
│   └── wordlist-architecture.md
├── frontend/
│   ├── .gitignore
│   ├── app/
│   │   ├── [locale]/
│   │   │   ├── dashboard/
│   │   │   │   ├── data.json
│   │   │   │   └── page.tsx
│   │   │   ├── layout.tsx
│   │   │   ├── login/
│   │   │   │   ├── layout.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── organization/
│   │   │   │   ├── [id]/
│   │   │   │   │   └── page.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── page.tsx
│   │   │   ├── scan/
│   │   │   │   ├── engine/
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── history/
│   │   │   │   │   ├── [id]/
│   │   │   │   │   │   ├── directories/
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   ├── endpoints/
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   ├── ip-addresses/
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   ├── layout.tsx
│   │   │   │   │   │   ├── overview/
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   ├── page.tsx
│   │   │   │   │   │   ├── screenshots/
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   ├── subdomain/
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   ├── vulnerabilities/
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   └── websites/
│   │   │   │   │   │       └── page.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   └── scheduled/
│   │   │   │       └── page.tsx
│   │   │   ├── search/
│   │   │   │   └── page.tsx
│   │   │   ├── settings/
│   │   │   │   ├── api-keys/
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── blacklist/
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── notifications/
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── system-logs/
│   │   │   │   │   └── page.tsx
│   │   │   │   └── workers/
│   │   │   │       └── page.tsx
│   │   │   ├── target/
│   │   │   │   ├── [id]/
│   │   │   │   │   ├── details/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── directories/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── endpoints/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── ip-addresses/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── layout.tsx
│   │   │   │   │   ├── overview/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── page.tsx
│   │   │   │   │   ├── screenshots/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── settings/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── subdomain/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── vulnerabilities/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   └── websites/
│   │   │   │   │       └── page.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── tools/
│   │   │   │   ├── config/
│   │   │   │   │   ├── custom/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── opensource/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── fingerprints/
│   │   │   │   │   ├── arl/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── ehole/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── fingerprinthub/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── fingers/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── goby/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── layout.tsx
│   │   │   │   │   ├── page.tsx
│   │   │   │   │   └── wappalyzer/
│   │   │   │   │       └── page.tsx
│   │   │   │   ├── nuclei/
│   │   │   │   │   ├── [repoId]/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   └── wordlists/
│   │   │   │       └── page.tsx
│   │   │   └── vulnerabilities/
│   │   │       └── page.tsx
│   │   ├── globals.css
│   │   └── layout.tsx
│   ├── components/
│   │   ├── about-dialog.tsx
│   │   ├── app-sidebar.tsx
│   │   ├── auth/
│   │   │   ├── auth-guard.tsx
│   │   │   ├── auth-layout.tsx
│   │   │   ├── change-password-dialog.tsx
│   │   │   └── index.ts
│   │   ├── color-theme-switcher.tsx
│   │   ├── common/
│   │   │   ├── bulk-add-urls-dialog.tsx
│   │   │   └── smart-filter-input.tsx
│   │   ├── dashboard/
│   │   │   ├── asset-distribution-chart.tsx
│   │   │   ├── asset-trend-chart.tsx
│   │   │   ├── dashboard-activity-tabs.tsx
│   │   │   ├── dashboard-data-table.tsx
│   │   │   ├── dashboard-scan-history.tsx
│   │   │   ├── dashboard-scheduled-scans.tsx
│   │   │   ├── dashboard-stat-cards.tsx
│   │   │   ├── recent-vulnerabilities.tsx
│   │   │   └── vuln-severity-chart.tsx
│   │   ├── directories/
│   │   │   ├── directories-columns.tsx
│   │   │   ├── directories-data-table.tsx
│   │   │   └── directories-view.tsx
│   │   ├── disk/
│   │   │   └── disk-stat-cards.tsx
│   │   ├── endpoints/
│   │   │   ├── endpoints-columns.tsx
│   │   │   ├── endpoints-data-table.tsx
│   │   │   ├── endpoints-detail-view.tsx
│   │   │   └── index.ts
│   │   ├── fingerprints/
│   │   │   ├── arl-fingerprint-columns.tsx
│   │   │   ├── arl-fingerprint-data-table.tsx
│   │   │   ├── arl-fingerprint-dialog.tsx
│   │   │   ├── arl-fingerprint-view.tsx
│   │   │   ├── ehole-fingerprint-columns.tsx
│   │   │   ├── ehole-fingerprint-data-table.tsx
│   │   │   ├── ehole-fingerprint-dialog.tsx
│   │   │   ├── ehole-fingerprint-view.tsx
│   │   │   ├── fingerprinthub-fingerprint-columns.tsx
│   │   │   ├── fingerprinthub-fingerprint-data-table.tsx
│   │   │   ├── fingerprinthub-fingerprint-dialog.tsx
│   │   │   ├── fingerprinthub-fingerprint-view.tsx
│   │   │   ├── fingers-fingerprint-columns.tsx
│   │   │   ├── fingers-fingerprint-data-table.tsx
│   │   │   ├── fingers-fingerprint-dialog.tsx
│   │   │   ├── fingers-fingerprint-view.tsx
│   │   │   ├── goby-fingerprint-columns.tsx
│   │   │   ├── goby-fingerprint-data-table.tsx
│   │   │   ├── goby-fingerprint-dialog.tsx
│   │   │   ├── goby-fingerprint-view.tsx
│   │   │   ├── import-fingerprint-dialog.tsx
│   │   │   ├── index.ts
│   │   │   ├── wappalyzer-fingerprint-columns.tsx
│   │   │   ├── wappalyzer-fingerprint-data-table.tsx
│   │   │   ├── wappalyzer-fingerprint-dialog.tsx
│   │   │   └── wappalyzer-fingerprint-view.tsx
│   │   ├── ip-addresses/
│   │   │   ├── index.ts
│   │   │   ├── ip-addresses-columns.tsx
│   │   │   ├── ip-addresses-data-table.tsx
│   │   │   └── ip-addresses-view.tsx
│   │   ├── language-switcher.tsx
│   │   ├── loading-spinner.tsx
│   │   ├── nav-secondary.tsx
│   │   ├── nav-system.tsx
│   │   ├── nav-user.tsx
│   │   ├── notifications/
│   │   │   ├── index.ts
│   │   │   └── notification-drawer.tsx
│   │   ├── organization/
│   │   │   ├── add-organization-dialog.tsx
│   │   │   ├── edit-organization-dialog.tsx
│   │   │   ├── index.ts
│   │   │   ├── organization-columns.tsx
│   │   │   ├── organization-data-table.tsx
│   │   │   ├── organization-detail-view.tsx
│   │   │   ├── organization-list.tsx
│   │   │   └── targets/
│   │   │       ├── add-target-dialog.tsx
│   │   │       ├── index.ts
│   │   │       ├── link-target-dialog.tsx
│   │   │       ├── targets-columns.tsx
│   │   │       ├── targets-data-table.tsx
│   │   │       └── targets-detail-view.tsx
│   │   ├── providers/
│   │   │   ├── index.ts
│   │   │   ├── query-provider.tsx
│   │   │   ├── theme-provider.tsx
│   │   │   └── ui-i18n-provider.tsx
│   │   ├── route-prefetch.tsx
│   │   ├── route-progress.tsx
│   │   ├── scan/
│   │   │   ├── engine/
│   │   │   │   ├── engine-columns.tsx
│   │   │   │   ├── engine-create-dialog.tsx
│   │   │   │   ├── engine-data-table.tsx
│   │   │   │   ├── engine-edit-dialog.tsx
│   │   │   │   └── index.ts
│   │   │   ├── engine-preset-selector.tsx
│   │   │   ├── history/
│   │   │   │   ├── index.ts
│   │   │   │   ├── scan-history-columns.tsx
│   │   │   │   ├── scan-history-data-table.tsx
│   │   │   │   ├── scan-history-list.tsx
│   │   │   │   ├── scan-history-stat-cards.tsx
│   │   │   │   └── scan-overview.tsx
│   │   │   ├── initiate-scan-dialog.tsx
│   │   │   ├── quick-scan-dialog.tsx
│   │   │   ├── scan-config-editor.tsx
│   │   │   ├── scan-log-list.tsx
│   │   │   ├── scan-progress-dialog.tsx
│   │   │   └── scheduled/
│   │   │       ├── create-scheduled-scan-dialog.tsx
│   │   │       ├── edit-scheduled-scan-dialog.tsx
│   │   │       ├── index.ts
│   │   │       ├── scheduled-scan-columns.tsx
│   │   │       └── scheduled-scan-data-table.tsx
│   │   ├── screenshots/
│   │   │   └── screenshots-gallery.tsx
│   │   ├── search/
│   │   │   ├── index.ts
│   │   │   ├── search-page.tsx
│   │   │   ├── search-pagination.tsx
│   │   │   ├── search-result-card.tsx
│   │   │   └── search-results-table.tsx
│   │   ├── settings/
│   │   │   ├── system-logs/
│   │   │   │   ├── ansi-log-viewer.tsx
│   │   │   │   ├── index.ts
│   │   │   │   ├── log-toolbar.tsx
│   │   │   │   └── system-logs-view.tsx
│   │   │   └── workers/
│   │   │       ├── deploy-terminal-dialog.tsx
│   │   │       ├── index.ts
│   │   │       ├── worker-dialog.tsx
│   │   │       └── worker-list.tsx
│   │   ├── site-header.tsx
│   │   ├── subdomains/
│   │   │   ├── bulk-add-subdomains-dialog.tsx
│   │   │   ├── index.ts
│   │   │   ├── subdomains-columns.tsx
│   │   │   ├── subdomains-data-table.tsx
│   │   │   └── subdomains-detail-view.tsx
│   │   ├── target/
│   │   │   ├── add-target-dialog.tsx
│   │   │   ├── all-targets-columns.tsx
│   │   │   ├── all-targets-detail-view.tsx
│   │   │   ├── index.ts
│   │   │   ├── target-overview.tsx
│   │   │   ├── target-settings.tsx
│   │   │   └── targets-data-table.tsx
│   │   ├── theme-toggle.tsx
│   │   ├── tools/
│   │   │   ├── commands/
│   │   │   │   ├── commands-columns.tsx
│   │   │   │   ├── commands-data-table.tsx
│   │   │   │   └── index.ts
│   │   │   ├── config/
│   │   │   │   ├── add-custom-tool-dialog.tsx
│   │   │   │   ├── add-tool-dialog.tsx
│   │   │   │   ├── custom-tools-list.tsx
│   │   │   │   ├── index.ts
│   │   │   │   ├── opensource-tools-list.tsx
│   │   │   │   └── tool-card.tsx
│   │   │   ├── index.ts
│   │   │   ├── wordlist-edit-dialog.tsx
│   │   │   └── wordlist-upload-dialog.tsx
│   │   ├── ui/
│   │   │   ├── alert-dialog.tsx
│   │   │   ├── alert.tsx
│   │   │   ├── avatar.tsx
│   │   │   ├── badge.tsx
│   │   │   ├── button.tsx
│   │   │   ├── calendar.tsx
│   │   │   ├── card-grid-skeleton.tsx
│   │   │   ├── card.tsx
│   │   │   ├── chart.tsx
│   │   │   ├── checkbox.tsx
│   │   │   ├── collapsible.tsx
│   │   │   ├── command.tsx
│   │   │   ├── confirm-dialog.tsx
│   │   │   ├── copyable-popover-content.tsx
│   │   │   ├── data-table/
│   │   │   │   ├── column-header.tsx
│   │   │   │   ├── column-resizer.tsx
│   │   │   │   ├── expandable-cell.tsx
│   │   │   │   ├── index.ts
│   │   │   │   ├── pagination.tsx
│   │   │   │   ├── toolbar.tsx
│   │   │   │   └── unified-data-table.tsx
│   │   │   ├── data-table-skeleton.tsx
│   │   │   ├── datetime-picker.tsx
│   │   │   ├── dialog.tsx
│   │   │   ├── drawer.tsx
│   │   │   ├── dropdown-menu.tsx
│   │   │   ├── dropzone.tsx
│   │   │   ├── field.tsx
│   │   │   ├── form.tsx
│   │   │   ├── hover-card.tsx
│   │   │   ├── input.tsx
│   │   │   ├── label.tsx
│   │   │   ├── master-detail-skeleton.tsx
│   │   │   ├── popover.tsx
│   │   │   ├── progress.tsx
│   │   │   ├── radio-group.tsx
│   │   │   ├── scroll-area.tsx
│   │   │   ├── select.tsx
│   │   │   ├── separator.tsx
│   │   │   ├── shadcn-io/
│   │   │   │   ├── banner/
│   │   │   │   │   └── index.tsx
│   │   │   │   └── status/
│   │   │   │       └── index.tsx
│   │   │   ├── sheet.tsx
│   │   │   ├── sidebar.tsx
│   │   │   ├── skeleton.tsx
│   │   │   ├── sonner.tsx
│   │   │   ├── spinner.tsx
│   │   │   ├── switch.tsx
│   │   │   ├── table.tsx
│   │   │   ├── tabs.tsx
│   │   │   ├── textarea.tsx
│   │   │   ├── toggle-group.tsx
│   │   │   ├── toggle.tsx
│   │   │   ├── tooltip.tsx
│   │   │   └── yaml-editor.tsx
│   │   ├── vulnerabilities/
│   │   │   ├── index.ts
│   │   │   ├── vulnerabilities-columns.tsx
│   │   │   ├── vulnerabilities-data-table.tsx
│   │   │   ├── vulnerabilities-detail-view.tsx
│   │   │   └── vulnerability-detail-dialog.tsx
│   │   └── websites/
│   │       ├── websites-columns.tsx
│   │       ├── websites-data-table.tsx
│   │       └── websites-view.tsx
│   ├── components.json
│   ├── eslint.config.mjs
│   ├── hooks/
│   │   ├── use-api-key-settings.ts
│   │   ├── use-auth.ts
│   │   ├── use-color-theme.ts
│   │   ├── use-commands.ts
│   │   ├── use-dashboard.ts
│   │   ├── use-directories.ts
│   │   ├── use-disk.ts
│   │   ├── use-endpoints.ts
│   │   ├── use-engines.ts
│   │   ├── use-fingerprints.ts
│   │   ├── use-global-blacklist.ts
│   │   ├── use-ip-addresses.ts
│   │   ├── use-mobile.ts
│   │   ├── use-notification-settings.ts
│   │   ├── use-notification-sse.ts
│   │   ├── use-notifications.ts
│   │   ├── use-nuclei-git-settings.ts
│   │   ├── use-nuclei-repos.ts
│   │   ├── use-nuclei-templates.ts
│   │   ├── use-organizations.ts
│   │   ├── use-route-prefetch.ts
│   │   ├── use-scan-logs.ts
│   │   ├── use-scans.ts
│   │   ├── use-scheduled-scans.ts
│   │   ├── use-screenshots.ts
│   │   ├── use-search.ts
│   │   ├── use-step.ts
│   │   ├── use-subdomains.ts
│   │   ├── use-system-logs.ts
│   │   ├── use-targets.ts
│   │   ├── use-tools.ts
│   │   ├── use-version.ts
│   │   ├── use-vulnerabilities.ts
│   │   ├── use-websites.ts
│   │   ├── use-wordlists.ts
│   │   └── use-workers.ts
│   ├── i18n/
│   │   ├── config.ts
│   │   ├── navigation.ts
│   │   └── request.ts
│   ├── lib/
│   │   ├── api-client.ts
│   │   ├── date-utils.ts
│   │   ├── domain-validator.ts
│   │   ├── endpoint-validator.ts
│   │   ├── engine-config.ts
│   │   ├── env.ts
│   │   ├── error-code-map.ts
│   │   ├── error-handler.ts
│   │   ├── i18n-format.ts
│   │   ├── response-parser.ts
│   │   ├── subdomain-validator.ts
│   │   ├── table-utils.ts
│   │   ├── target-validator.ts
│   │   ├── toast-helpers.ts
│   │   ├── url-validator.ts
│   │   └── utils.ts
│   ├── messages/
│   │   ├── en.json
│   │   └── zh.json
│   ├── middleware.ts
│   ├── mock/
│   │   ├── config.ts
│   │   ├── data/
│   │   │   ├── auth.ts
│   │   │   ├── blacklist.ts
│   │   │   ├── dashboard.ts
│   │   │   ├── directories.ts
│   │   │   ├── endpoints.ts
│   │   │   ├── engines.ts
│   │   │   ├── fingerprints.ts
│   │   │   ├── ip-addresses.ts
│   │   │   ├── notification-settings.ts
│   │   │   ├── notifications.ts
│   │   │   ├── nuclei-templates.ts
│   │   │   ├── organizations.ts
│   │   │   ├── scans.ts
│   │   │   ├── scheduled-scans.ts
│   │   │   ├── search.ts
│   │   │   ├── subdomains.ts
│   │   │   ├── system-logs.ts
│   │   │   ├── targets.ts
│   │   │   ├── tools.ts
│   │   │   ├── vulnerabilities.ts
│   │   │   ├── websites.ts
│   │   │   ├── wordlists.ts
│   │   │   └── workers.ts
│   │   └── index.ts
│   ├── next.config.ts
│   ├── package.json
│   ├── postcss.config.mjs
│   ├── public/
│   │   ├── animations/
│   │   │   └── Security000-Purple.json
│   │   └── mockServiceWorker.js
│   ├── services/
│   │   ├── api-key-settings.service.ts
│   │   ├── auth.service.ts
│   │   ├── command.service.ts
│   │   ├── dashboard.service.ts
│   │   ├── directory.service.ts
│   │   ├── disk.service.ts
│   │   ├── endpoint.service.ts
│   │   ├── engine.service.ts
│   │   ├── fingerprint.service.ts
│   │   ├── global-blacklist.service.ts
│   │   ├── ip-address.service.ts
│   │   ├── notification-settings.service.ts
│   │   ├── notification.service.ts
│   │   ├── nuclei-git.service.ts
│   │   ├── nuclei-repo.api.ts
│   │   ├── nuclei.service.ts
│   │   ├── organization.service.ts
│   │   ├── scan.service.ts
│   │   ├── scheduled-scan.service.ts
│   │   ├── screenshot.service.ts
│   │   ├── search.service.ts
│   │   ├── subdomain.service.ts
│   │   ├── system-log.service.ts
│   │   ├── target.service.ts
│   │   ├── tool.service.ts
│   │   ├── version.service.ts
│   │   ├── vulnerability.service.ts
│   │   ├── website.service.ts
│   │   ├── wordlist.service.ts
│   │   └── worker.service.ts
│   ├── styles/
│   │   └── themes/
│   │       ├── bubblegum.css
│   │       ├── clean-slate.css
│   │       ├── cosmic-night.css
│   │       ├── cyberpunk-1.css
│   │       ├── eva-01.css
│   │       ├── index.css
│   │       ├── quantum-rose.css
│   │       ├── vercel-dark.css
│   │       ├── vercel.css
│   │       └── violet-bloom.css
│   ├── tsconfig.json
│   ├── types/
│   │   ├── api-key-settings.types.ts
│   │   ├── api-response.types.ts
│   │   ├── auth.types.ts
│   │   ├── command.types.ts
│   │   ├── common.types.ts
│   │   ├── dashboard.types.ts
│   │   ├── data-table.types.ts
│   │   ├── directory.types.ts
│   │   ├── disk.types.ts
│   │   ├── endpoint.types.ts
│   │   ├── engine.types.ts
│   │   ├── fingerprint.types.ts
│   │   ├── ip-address.types.ts
│   │   ├── notification-settings.types.ts
│   │   ├── notification.types.ts
│   │   ├── nuclei-git.types.ts
│   │   ├── nuclei.types.ts
│   │   ├── organization.types.ts
│   │   ├── psl.d.ts
│   │   ├── scan.types.ts
│   │   ├── scheduled-scan.types.ts
│   │   ├── search.types.ts
│   │   ├── subdomain.types.ts
│   │   ├── system-log.types.ts
│   │   ├── target.types.ts
│   │   ├── tool.types.ts
│   │   ├── version.types.ts
│   │   ├── vulnerability.types.ts
│   │   ├── website.types.ts
│   │   ├── wordlist.types.ts
│   │   └── worker.types.ts
│   └── vercel.json
├── install.sh
├── restart.sh
├── start.sh
├── stop.sh
├── uninstall.sh
└── update.sh

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
# Node modules(前端本地开发产物,Docker 构建时会重新安装)
frontend/node_modules
frontend/.next

# Python 虚拟环境
.venv
__pycache__
*.pyc

# 日志和临时文件
*.log
.DS_Store

# Git
.git
.gitignore

# IDE
.idea
.vscode

# Docker 相关(避免嵌套)
docker/.env


================================================
FILE: .github/workflows/docker-build.yml
================================================
name: Build and Push Docker Images

on:
  push:
    tags:
      - 'v*'  # 只在推送 v 开头的 tag 时触发(如 v1.0.0)
  workflow_dispatch:  # 手动触发

# 并发控制:同一分支只保留最新的构建,取消之前正在运行的
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:
  REGISTRY: docker.io
  IMAGE_PREFIX: yyhuni

permissions:
  contents: write

jobs:
  # AMD64 构建(原生 x64 runner)
  build-amd64:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        include:
          - image: xingrin-server
            dockerfile: docker/server/Dockerfile
            context: .
          - image: xingrin-frontend
            dockerfile: docker/frontend/Dockerfile
            context: .
          - image: xingrin-worker
            dockerfile: docker/worker/Dockerfile
            context: .
          - image: xingrin-nginx
            dockerfile: docker/nginx/Dockerfile
            context: .
          - image: xingrin-agent
            dockerfile: docker/agent/Dockerfile
            context: .
          - image: xingrin-postgres
            dockerfile: docker/postgres/Dockerfile
            context: docker/postgres

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Free disk space
        run: |
          sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
          sudo docker image prune -af

      - name: Generate SSL certificates for nginx build
        if: matrix.image == 'xingrin-nginx'
        run: |
          mkdir -p docker/nginx/ssl
          openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
            -keyout docker/nginx/ssl/privkey.pem \
            -out docker/nginx/ssl/fullchain.pem \
            -subj "/CN=localhost"

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Get version
        id: version
        run: |
          if [[ $GITHUB_REF == refs/tags/* ]]; then
            echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
          else
            echo "VERSION=dev-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
          fi

      - name: Build and push AMD64
        uses: docker/build-push-action@v5
        with:
          context: ${{ matrix.context }}
          file: ${{ matrix.dockerfile }}
          platforms: linux/amd64
          push: true
          tags: ${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:${{ steps.version.outputs.VERSION }}-amd64
          build-args: IMAGE_TAG=${{ steps.version.outputs.VERSION }}
          cache-from: type=registry,ref=${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:cache-amd64
          cache-to: type=registry,ref=${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:cache-amd64,mode=max
          provenance: false
          sbom: false

  # ARM64 构建(原生 ARM64 runner)
  build-arm64:
    runs-on: ubuntu-22.04-arm
    strategy:
      matrix:
        include:
          - image: xingrin-server
            dockerfile: docker/server/Dockerfile
            context: .
          - image: xingrin-frontend
            dockerfile: docker/frontend/Dockerfile
            context: .
          - image: xingrin-worker
            dockerfile: docker/worker/Dockerfile
            context: .
          - image: xingrin-nginx
            dockerfile: docker/nginx/Dockerfile
            context: .
          - image: xingrin-agent
            dockerfile: docker/agent/Dockerfile
            context: .
          - image: xingrin-postgres
            dockerfile: docker/postgres/Dockerfile
            context: docker/postgres

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Generate SSL certificates for nginx build
        if: matrix.image == 'xingrin-nginx'
        run: |
          mkdir -p docker/nginx/ssl
          openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
            -keyout docker/nginx/ssl/privkey.pem \
            -out docker/nginx/ssl/fullchain.pem \
            -subj "/CN=localhost"

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Get version
        id: version
        run: |
          if [[ $GITHUB_REF == refs/tags/* ]]; then
            echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
          else
            echo "VERSION=dev-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
          fi

      - name: Build and push ARM64
        uses: docker/build-push-action@v5
        with:
          context: ${{ matrix.context }}
          file: ${{ matrix.dockerfile }}
          platforms: linux/arm64
          push: true
          tags: ${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:${{ steps.version.outputs.VERSION }}-arm64
          build-args: IMAGE_TAG=${{ steps.version.outputs.VERSION }}
          cache-from: type=registry,ref=${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:cache-arm64
          cache-to: type=registry,ref=${{ env.IMAGE_PREFIX }}/${{ matrix.image }}:cache-arm64,mode=max
          provenance: false
          sbom: false

  # 合并多架构 manifest
  merge-manifests:
    runs-on: ubuntu-latest
    needs: [build-amd64, build-arm64]
    strategy:
      matrix:
        image:
          - xingrin-server
          - xingrin-frontend
          - xingrin-worker
          - xingrin-nginx
          - xingrin-agent
          - xingrin-postgres
    steps:
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Get version
        id: version
        run: |
          if [[ $GITHUB_REF == refs/tags/* ]]; then
            echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
            echo "IS_RELEASE=true" >> $GITHUB_OUTPUT
          else
            echo "VERSION=dev-$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
            echo "IS_RELEASE=false" >> $GITHUB_OUTPUT
          fi

      - name: Create and push multi-arch manifest
        run: |
          VERSION=${{ steps.version.outputs.VERSION }}
          IMAGE=${{ env.IMAGE_PREFIX }}/${{ matrix.image }}
          
          docker manifest create ${IMAGE}:${VERSION} \
            ${IMAGE}:${VERSION}-amd64 \
            ${IMAGE}:${VERSION}-arm64
          docker manifest push ${IMAGE}:${VERSION}
          
          if [[ "${{ steps.version.outputs.IS_RELEASE }}" == "true" ]]; then
            docker manifest create ${IMAGE}:latest \
              ${IMAGE}:${VERSION}-amd64 \
              ${IMAGE}:${VERSION}-arm64
            docker manifest push ${IMAGE}:latest
          fi

  # 更新 VERSION 文件
  update-version:
    runs-on: ubuntu-latest
    needs: merge-manifests
    if: startsWith(github.ref, 'refs/tags/v')
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # 获取完整历史,用于判断 tag 所在分支
          token: ${{ secrets.GITHUB_TOKEN }}

      - name: Determine source branch and version
        id: branch
        run: |
          VERSION="${GITHUB_REF#refs/tags/}"
          
          # 查找包含此 tag 的分支
          BRANCHES=$(git branch -r --contains ${{ github.ref_name }})
          echo "Branches containing tag: $BRANCHES"
          
          # 判断 tag 来自哪个分支
          if echo "$BRANCHES" | grep -q "origin/main"; then
            TARGET_BRANCH="main"
            UPDATE_LATEST="true"
          elif echo "$BRANCHES" | grep -q "origin/dev"; then
            TARGET_BRANCH="dev"
            UPDATE_LATEST="false"
          else
            echo "Warning: Tag not found in main or dev branch, defaulting to main"
            TARGET_BRANCH="main"
            UPDATE_LATEST="false"
          fi
          
          echo "BRANCH=$TARGET_BRANCH" >> $GITHUB_OUTPUT
          echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
          echo "UPDATE_LATEST=$UPDATE_LATEST" >> $GITHUB_OUTPUT
          echo "Will update VERSION on branch: $TARGET_BRANCH"

      - name: Checkout target branch
        run: |
          git checkout ${{ steps.branch.outputs.BRANCH }}

      - name: Update VERSION file
        run: |
          VERSION="${{ steps.branch.outputs.VERSION }}"
          echo "$VERSION" > VERSION
          echo "Updated VERSION to $VERSION on branch ${{ steps.branch.outputs.BRANCH }}"

      - name: Commit and push
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add VERSION
          git diff --staged --quiet || git commit -m "chore: bump version to ${{ steps.branch.outputs.VERSION }}"
          git push origin ${{ steps.branch.outputs.BRANCH }}


================================================
FILE: .gitignore
================================================
# ============================
# 操作系统相关文件
# ============================
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

# ============================
# 前端 (Next.js/Node.js) 相关
# ============================
# 依赖目录
front-back/node_modules/
front-back/.pnpm-store/

# Next.js 构建产物
front-back/.next/
front-back/out/
front-back/dist/

# 环境变量文件
front-back/.env
front-back/.env.local
front-back/.env.development.local
front-back/.env.test.local
front-back/.env.production.local

# 运行时和缓存
front-back/.turbo/
front-back/.swc/
front-back/.eslintcache
front-back/.tsbuildinfo

# ============================
# 后端 (Python/Django) 相关
# ============================
# Python 虚拟环境
.venv/
venv/
env/
ENV/

# Python 编译文件
*.pyc
*.pyo
*.pyd
__pycache__/
*.py[cod]
*$py.class

# Django 相关
backend/db.sqlite3
backend/db.sqlite3-journal
backend/media/
backend/staticfiles/
backend/.env
backend/.env.local

# Python 测试和覆盖率
.pytest_cache/
.coverage
htmlcov/
*.cover
.hypothesis/

# ============================
# 后端 (Go) 相关
# ============================
# 编译产物
backend/bin/
backend/dist/
backend/*.exe
backend/*.exe~
backend/*.dll
backend/*.so
backend/*.dylib

# 测试相关
backend/*.test
backend/*.out
backend/*.prof

# Go workspace 文件
backend/go.work
backend/go.work.sum

# Go 依赖管理
backend/vendor/

# ============================
# IDE 和编辑器相关
# ============================
.vscode/
.idea/
.cursor/
.claude/
.kiro/
.playwright-mcp/
*.swp
*.swo
*~

# ============================
# Docker 相关
# ============================
docker/.env
docker/.env.local

# SSL 证书和私钥(不应提交)
docker/nginx/ssl/*.pem
docker/nginx/ssl/*.key
docker/nginx/ssl/*.crt

# ============================
# 日志文件和扫描结果
# ============================
*.log
logs/
results/

# 开发脚本运行时文件(进程 ID 和启动日志)
backend/scripts/dev/.pids/

# ============================
# 临时文件
# ============================
tmp/
temp/
.cache/

HGETALL
KEYS
vuln_scan/input_endpoints.txt
open-in-v0

================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

我们作为成员、贡献者和维护者,承诺让社区中的每个人都能在没有骚扰的环境中参与项目,不论年龄、体型、可见或不可见残障、族裔、性别特征、性别认同和表达、经验水平、教育程度、社会经济地位、国籍、外貌、种族、宗教或性取向。

我们承诺以开放、友善、多元和尊重的方式参与社区建设。

## Our Standards

积极行为示例:

- 以尊重和建设性的方式给出反馈
- 对不同观点和经验保持开放态度
- 真诚接受合理批评并持续改进
- 以社区整体利益为先

不可接受的行为包括:

- 人身攻击、侮辱、歧视或骚扰言论
- 恶意挑衅、持续性争吵或破坏讨论秩序
- 发布他人私人信息
- 其他在专业环境中不适当的行为

## Enforcement Responsibilities

维护者有责任公平、及时地澄清和执行社区行为标准,并可对任何不当行为采取适当且公正的纠正措施。

## Scope

本行为准则适用于所有项目空间,也适用于代表项目参与公共场合的行为,包括使用官方邮箱、社交账号或在活动中作为项目代表发言。

## Enforcement

如果你遇到骚扰、辱骂或其他不可接受的行为,请联系维护者:`poem@admin.com`。

所有投诉都会被审查并得到适当回应。

## Attribution

本行为准则参考 Contributor Covenant,并根据本项目需要进行了简化整理。


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to XingRin

感谢你愿意为 XingRin 做出贡献。

## 贡献方式

- 提交 Bug 报告
- 提出功能建议或产品想法
- 改进文档、安装流程和示例
- 提交代码修复或新功能 Pull Request

## 开始之前

在开始较大改动前,建议先创建 Issue 说明背景、目标和实现方向,避免重复工作。

对于小修复、文档修正和明显 Bug,也欢迎直接提交 Pull Request。

## Pull Request 建议

- 保持改动聚焦,避免把无关修改混在一个 PR 中
- 尽量补充必要的说明、截图或复现步骤
- 如果涉及安全相关行为,请明确测试范围和授权前提
- 与现有代码风格、命名和目录结构保持一致

## Issue 建议

提交 Issue 时,建议附上:

- 使用场景
- 复现步骤
- 实际结果与预期结果
- 日志、截图或错误信息
- 运行环境信息

## 许可证约定

提交 Pull Request 即表示你确认:

- 你有权提交这些代码、文档或资源
- 你的贡献将以本仓库当前使用的 [MIT License](./LICENSE) 发布

## 社区协作

参与社区前,请先阅读 [Code of Conduct](./CODE_OF_CONDUCT.md)。

感谢你的时间、想法和贡献。


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2025-2026 yyhuni

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
<h1 align="center">XingRin - 星环</h1>

<p align="center">
  <b>开源攻击面管理平台 (ASM) | 面向授权安全测试与防御研究的资产发现与安全自动化系统</b>
</p>

<p align="center">
  <b>持续维护中</b>:由于现在架构,代码对于ai开发来说,演化进度难度很高,代码原生分布式能力弱,为了适应项目的长期发展,正在进行新一代架构重构,后端采用 Go 语言为核心,其强大的分布式能力是目前最优解,当前版本暂停维护,Issue、建议和 PR 都欢迎提交,旧项目的所有建议,都会逐步迁移到新版本。预计5-6月份上线新仓库,欢迎入群等待。
</p>


<p align="center">
  <a href="https://github.com/yyhuni/xingrin/stargazers"><img src="https://img.shields.io/github/stars/yyhuni/xingrin?style=flat-square&logo=github" alt="GitHub stars"></a>
  <a href="https://github.com/yyhuni/xingrin/network/members"><img src="https://img.shields.io/github/forks/yyhuni/xingrin?style=flat-square&logo=github" alt="GitHub forks"></a>
  <a href="https://github.com/yyhuni/xingrin/issues"><img src="https://img.shields.io/github/issues/yyhuni/xingrin?style=flat-square&logo=github" alt="GitHub issues"></a>
  <a href="https://github.com/yyhuni/xingrin/blob/main/LICENSE"><img src="https://img.shields.io/github/license/yyhuni/xingrin?style=flat-square" alt="License"></a>
</p>

<p align="center">
  <a href="#功能特性">功能特性</a> •
  <a href="#全局资产搜索">资产搜索</a> •
  <a href="#快速开始">快速开始</a> •
  <a href="#文档">文档</a> •
  <a href="#反馈与贡献">反馈与贡献</a>
</p>

<p align="center">
  <sub>关键词: Open Source | ASM | 攻击面管理 | 授权安全测试 | 防御研究 | 资产发现 | 资产搜索 | 安全自动化 | 风险检测 | 子域名枚举 | EASM</sub>
</p>

---
## 在线 Demo

 **[https://xingrin.vercel.app/](https://xingrin.vercel.app/)**

> 仅用于 UI 展示,未接入后端数据库

> XingRin 聚焦 **自有资产管理、授权安全测试与防御研究** 场景,帮助安全团队对互联网暴露面进行持续发现、监控与分析。

---

<p align="center">
  <b>现代化 UI</b>
</p>

<p align="center">
  <img src="docs/screenshots/light.png" alt="Light Mode" width="24%">
  <img src="docs/screenshots/bubblegum.png" alt="Bubblegum" width="24%">
  <img src="docs/screenshots/cosmic-night.png" alt="Cosmic Night" width="24%">
  <img src="docs/screenshots/quantum-rose.png" alt="Quantum Rose" width="24%">
</p>

## 文档

- [技术文档](./docs/README.md) - 技术文档导航(持续完善中)
- [快速开始](./docs/quick-start.md) - 一键安装和部署指南
- [版本管理](./docs/version-management.md) - Git Tag 驱动的自动化版本管理系统
- [安全检测模板架构](./docs/nuclei-template-architecture.md) - 检测模板仓库的存储与同步
- [字典文件架构](./docs/wordlist-architecture.md) - 字典文件的存储与同步
- [扫描流程架构](./docs/scan-flow-architecture.md) - 完整扫描流程与工具编排
- [贡献指南](./CONTRIBUTING.md) - Issue、Pull Request 与贡献约定
- [安全策略](./SECURITY.md) - 漏洞报告与安全响应流程
- [行为准则](./CODE_OF_CONDUCT.md) - 社区协作行为规范


---

## 功能特性

### 扫描能力

| 功能 | 状态 | 工具 | 说明 |
|------|------|------|------|
| 子域名扫描 | 已完成 | Subfinder, Amass, PureDNS | 被动收集 + 主动枚举,聚合 50+ 数据源 |
| 端口扫描 | 已完成 | Naabu | 自定义端口范围 |
| 站点发现 | 已完成 | HTTPX | HTTP 探测,自动获取标题、状态码、技术栈 |
| 指纹识别 | 已完成 | XingFinger | 2.7W+ 指纹规则,多源指纹库 |
| URL 收集 | 已完成 | Waymore, Katana | 历史数据 + 主动爬取 |
| 目录扫描 | 已完成 | FFUF | 高效探测,智能字典 |
| 安全检测 | 已完成 | Nuclei, Dalfox | 9000+ 检测模板,覆盖常见 Web 风险与配置问题 |
| 站点截图 | 已完成 | Playwright | WebP 高压缩存储 |

### 平台能力

| 功能 | 状态 | 说明 |
|------|------|------|
| 目标管理 | 已完成 | 多层级组织,支持域名/IP 目标 |
| 资产快照 | 已完成 | 扫描结果对比,追踪资产变化 |
| 黑名单过滤 | 已完成 | 全局 + Target 级,支持通配符/CIDR |
| 定时任务 | 已完成 | Cron 表达式,自动化周期扫描 |
| 分布式扫描 | 已完成 | 多 Worker 节点,负载感知调度 |
| 全局搜索 | 已完成 | 表达式语法,多字段组合查询 |
| 通知推送 | 已完成 | 企业微信、Telegram、Discord |
| API 密钥管理 | 已完成 | 可视化配置各数据源 API Key |

### 扫描流程架构

完整的扫描流程包括:子域名发现、端口扫描、站点发现、指纹识别、URL 收集、目录扫描、安全检测等阶段

```mermaid
flowchart LR
    START["开始扫描"]
    
    subgraph STAGE1["阶段 1: 资产发现"]
        direction TB
        SUB["子域名发现<br/>subfinder, amass, puredns"]
        PORT["端口扫描<br/>naabu"]
        SITE["站点识别<br/>httpx"]
        FINGER["指纹识别<br/>xingfinger"]
        SUB --> PORT --> SITE --> FINGER
    end
    
    subgraph STAGE2["阶段 2: 深度分析"]
        direction TB
        URL["URL 收集<br/>waymore, katana"]
        DIR["目录扫描<br/>ffuf"]
        SCREENSHOT["站点截图<br/>playwright"]
    end
    
    subgraph STAGE3["阶段 3: 安全检测"]
        VULN["安全检测<br/>nuclei, dalfox"]
    end
    
    FINISH["扫描完成"]
    
    START --> STAGE1
    FINGER --> STAGE2
    STAGE2 --> STAGE3
    STAGE3 --> FINISH
    
    style START fill:#34495e,stroke:#2c3e50,stroke-width:2px,color:#fff
    style FINISH fill:#27ae60,stroke:#229954,stroke-width:2px,color:#fff
    style STAGE1 fill:#3498db,stroke:#2980b9,stroke-width:2px,color:#fff
    style STAGE2 fill:#9b59b6,stroke:#8e44ad,stroke-width:2px,color:#fff
    style STAGE3 fill:#e67e22,stroke:#d35400,stroke-width:2px,color:#fff
    style SUB fill:#5dade2,stroke:#3498db,stroke-width:1px,color:#fff
    style PORT fill:#5dade2,stroke:#3498db,stroke-width:1px,color:#fff
    style SITE fill:#5dade2,stroke:#3498db,stroke-width:1px,color:#fff
    style FINGER fill:#5dade2,stroke:#3498db,stroke-width:1px,color:#fff
    style URL fill:#bb8fce,stroke:#9b59b6,stroke-width:1px,color:#fff
    style DIR fill:#bb8fce,stroke:#9b59b6,stroke-width:1px,color:#fff
    style SCREENSHOT fill:#bb8fce,stroke:#9b59b6,stroke-width:1px,color:#fff
    style VULN fill:#f0b27a,stroke:#e67e22,stroke-width:1px,color:#fff
```

详细说明请查看 [扫描流程架构文档](./docs/scan-flow-architecture.md)

### 分布式架构
- **多节点扫描** - 支持部署多个 Worker 节点,横向扩展扫描能力
- **本地节点** - 零配置,安装即自动注册本地 Docker Worker
- **远程节点** - SSH 一键部署远程 VPS 作为扫描节点
- **负载感知调度** - 实时感知节点负载,自动分发任务到最优节点
- **节点监控** - 实时心跳检测,CPU/内存/磁盘状态监控
- **断线重连** - 节点离线自动检测,恢复后自动重新接入

```mermaid
flowchart TB
    subgraph MASTER["主服务器 (Master Server)"]
        direction TB
        
        REDIS["Redis 负载缓存"]
        
        subgraph SCHEDULER["任务调度器 (Task Distributor)"]
            direction TB
            SUBMIT["接收扫描任务"]
            SELECT["负载感知选择"]
            DISPATCH["智能分发"]
            
            SUBMIT --> SELECT
            SELECT --> DISPATCH
        end
        
        REDIS -.负载数据.-> SELECT
    end
    
    subgraph WORKERS["Worker 节点集群"]
        direction TB
        
        W1["Worker 1 (本地)<br/>CPU: 45% | MEM: 60%"]
        W2["Worker 2 (远程)<br/>CPU: 30% | MEM: 40%"]
        W3["Worker N (远程)<br/>CPU: 90% | MEM: 85%"]
    end
    
    DISPATCH -->|任务分发| W1
    DISPATCH -->|任务分发| W2
    DISPATCH -->|高负载跳过| W3
    
    W1 -.心跳上报.-> REDIS
    W2 -.心跳上报.-> REDIS
    W3 -.心跳上报.-> REDIS
```

### 全局资产搜索
- **多类型搜索** - 支持 Website 和 Endpoint 两种资产类型
- **表达式语法** - 支持 `=`(模糊)、`==`(精确)、`!=`(不等于)操作符
- **逻辑组合** - 支持 `&&` (AND) 和 `||` (OR) 逻辑组合
- **多字段查询** - 支持 host、url、title、tech、status、body、header 字段
- **CSV 导出** - 流式导出全部搜索结果,无数量限制

#### 搜索语法示例

```bash
# 基础搜索
host="api"                    # host 包含 "api"
status=="200"                 # 状态码精确等于 200
tech="nginx"                  # 技术栈包含 nginx

# 组合搜索
host="api" && status=="200"   # host 包含 api 且状态码为 200
tech="vue" || tech="react"    # 技术栈包含 vue 或 react

# 复杂查询
host="admin" && tech="php" && status=="200"
url="/api/v1" && status!="404"
```

### 可视化界面
- **数据统计** - 资产/漏洞统计仪表盘
- **实时通知** - WebSocket 消息推送
- **通知推送** - 实时企业微信,tg,discard消息推送服务

---

## 快速开始

### 环境要求

- **操作系统**: Ubuntu 20.04+ / Debian 11+ 
- **系统架构**: AMD64 (x86_64) / ARM64 (aarch64)
- **硬件**: 2核 4G 内存起步,20GB+ 磁盘空间

### 一键安装

```bash
# 克隆项目
git clone https://github.com/yyhuni/xingrin.git
cd xingrin

# 安装并启动(生产模式)
sudo ./install.sh

# 中国大陆用户推荐使用镜像加速(第三方加速服务可能会失效,不保证长期可用)
sudo ./install.sh --mirror
```

> **--mirror 参数说明**
> - 自动配置 Docker 镜像加速(国内镜像源)
> - 加速 Git 仓库克隆(Nuclei 模板等)

### 访问服务

- **Web 界面**: `https://ip:8083` 
- **默认账号**: admin / admin(首次登录后请修改密码)

### 常用命令

```bash
# 启动服务
sudo ./start.sh

# 停止服务
sudo ./stop.sh

# 重启服务
sudo ./restart.sh

# 卸载
sudo ./uninstall.sh
```

## 反馈与贡献

- **发现 Bug、想法或改进建议**:欢迎提交 [Issue](https://github.com/yyhuni/xingrin/issues)
- **想直接参与开发**:欢迎发起 Pull Request,提交前建议先阅读 [贡献指南](./CONTRIBUTING.md)
- **安全问题反馈**:请优先阅读 [安全策略](./SECURITY.md),避免公开披露未修复漏洞

## 联系
- 微信公众号: **塔罗安全学苑**
- 微信群去公众号底下的菜单,有个交流群,点击就可以看到了,链接过期可以私信我拉你

<img src="docs/wechat-qrcode.png" alt="微信公众号" width="200">

### 关注公众号免费领取指纹库

| 指纹库 | 数量 |
|--------|------|
| ehole.json | 21,977 |
| ARL.yaml | 9,264 |
| goby.json | 7,086 |
| FingerprintHub.json | 3,147 |

> 关注公众号回复「指纹」即可获取

## 赞助支持

如果这个项目对你有帮助,谢谢请我能喝杯蜜雪冰城,你的star和赞助是我免费更新的动力

<p>
  <img src="docs/wx_pay.jpg" alt="微信支付" width="200">
  <img src="docs/zfb_pay.jpg" alt="支付宝" width="200">
</p>



## 安全与合规说明

**重要:请在使用前仔细阅读**

1. 本项目面向**授权安全测试**、**防御研究**与**自有资产攻击面管理**场景
2. 使用者必须确保已获得目标系统、数据和环境的**明确合法授权**
3. 请勿将本项目用于任何未经授权的扫描、入侵、破坏或其他违法行为
4. 使用者应自行确认其行为符合所在地区法律法规、合同义务和组织政策
5. 项目维护者**不对滥用行为负责**

使用本工具即表示您同意:
- 仅在合法授权范围内使用
- 遵守所在地区的法律法规
- 承担因滥用产生的一切后果

## Star History

如果这个项目对你有帮助,请给一个 Star 支持一下!

[![Star History Chart](https://api.star-history.com/svg?repos=yyhuni/xingrin&type=Date)](https://star-history.com/#yyhuni/xingrin&Date)

## 许可证

本项目采用 [MIT License](LICENSE)。

MIT 许可证允许商业和非商业使用、修改、分发与私有部署,但要求保留原始版权和许可证声明。

请注意:开源许可证授予的是代码使用、修改和分发权限,并不构成对任何未经授权安全测试或违法行为的许可。使用者仍需自行确保其用途合法、合规且已获得明确授权。


================================================
FILE: SECURITY.md
================================================
# Security Policy

## Supported Versions

安全修复优先面向以下范围:

| Version | Supported |
| --- | --- |
| `main` 最新提交 | ✅ |
| 最新稳定发布 | ✅ |
| 历史旧版本 | ⚠️ 仅在风险较高且易于回补时评估 |

## Reporting a Vulnerability

如果你发现了潜在安全漏洞,请不要直接公开提交未修复细节。

建议的报告方式:

1. 通过 GitHub 私密安全报告功能提交(如果仓库已开启)
2. 或发送邮件至 `poem@admin.com`

提交时建议包含:

- 漏洞类型与影响范围
- 复现步骤或最小 PoC
- 受影响版本、环境与配置
- 可能的修复建议(如果有)

## Response Process

- 我会尽快确认收到报告
- 我会评估影响范围、可复现性和修复优先级
- 修复完成后,会在合适时间通过提交记录、Release 或公告说明

感谢你以负责任的方式帮助改进 XingRin 的安全性。


================================================
FILE: VERSION
================================================
v1.5.8


================================================
FILE: backend/.gitignore
================================================
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
*.egg-info/
dist/
build/
.hypothesis/  # Hypothesis 属性测试缓存

# 虚拟环境
venv/
env/
ENV/

# Django
*.log
db.sqlite3
db.sqlite3-journal
/media
/staticfiles

# 运行时文件(Flower、PID)
/var/*
!/var/.gitkeep
flower.db
pids/
script/dev/.pids/

# 扫描结果和日志(后端数据)
/results/*
!/results/.gitkeep
/logs/*
!/logs/.gitkeep

# 环境变量(敏感信息)
.env
.env.development
.env.production
.env.staging
# 只提交模板文件:.env.*.example

# IDE
.vscode/
.idea/
*.swp
*.swo
*~

# macOS
.DS_Store


================================================
FILE: backend/apps/__init__.py
================================================


================================================
FILE: backend/apps/asset/__init__.py
================================================


================================================
FILE: backend/apps/asset/apps.py
================================================
from django.apps import AppConfig


class AssetConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'apps.asset'


================================================
FILE: backend/apps/asset/dtos/__init__.py
================================================
"""Asset DTOs - 数据传输对象"""

# 资产模块 DTOs
from .asset import (
    SubdomainDTO,
    WebSiteDTO,
    IPAddressDTO,
    DirectoryDTO,
    PortDTO,
    EndpointDTO,
)

# 快照模块 DTOs
from .snapshot import (
    SubdomainSnapshotDTO,
)

__all__ = [
    # 资产模块
    'SubdomainDTO',
    'WebSiteDTO',
    'IPAddressDTO',
    'DirectoryDTO',
    'PortDTO',
    'EndpointDTO',
    # 快照模块
    'SubdomainSnapshotDTO',
]


================================================
FILE: backend/apps/asset/dtos/asset/__init__.py
================================================
"""Asset DTOs - 数据传输对象"""

from .subdomain_dto import SubdomainDTO
from .ip_address_dto import IPAddressDTO
from .port_dto import PortDTO
from .website_dto import WebSiteDTO
from .directory_dto import DirectoryDTO
from .host_port_mapping_dto import HostPortMappingDTO
from .endpoint_dto import EndpointDTO
from .vulnerability_dto import VulnerabilityDTO

__all__ = [
    'SubdomainDTO',
    'IPAddressDTO',
    'PortDTO',
    'WebSiteDTO',
    'DirectoryDTO',
    'HostPortMappingDTO',
    'EndpointDTO',
    'VulnerabilityDTO',
]


================================================
FILE: backend/apps/asset/dtos/asset/directory_dto.py
================================================
"""Directory DTO"""

from dataclasses import dataclass
from typing import Optional


@dataclass
class DirectoryDTO:
    """目录数据传输对象"""
    target_id: int
    url: str
    status: Optional[int] = None
    content_length: Optional[int] = None
    words: Optional[int] = None
    lines: Optional[int] = None
    content_type: str = ''
    duration: Optional[int] = None


================================================
FILE: backend/apps/asset/dtos/asset/endpoint_dto.py
================================================
"""Endpoint DTO"""

from dataclasses import dataclass
from typing import Optional, List


@dataclass
class EndpointDTO:
    """端点 DTO - 资产表数据传输对象"""
    target_id: int
    url: str
    host: Optional[str] = None
    title: Optional[str] = None
    status_code: Optional[int] = None
    content_length: Optional[int] = None
    webserver: Optional[str] = None
    response_body: Optional[str] = None
    content_type: Optional[str] = None
    tech: Optional[List[str]] = None
    vhost: Optional[bool] = None
    location: Optional[str] = None
    matched_gf_patterns: Optional[List[str]] = None
    response_headers: Optional[str] = None
    
    def __post_init__(self):
        if self.tech is None:
            self.tech = []
        if self.matched_gf_patterns is None:
            self.matched_gf_patterns = []


================================================
FILE: backend/apps/asset/dtos/asset/host_port_mapping_dto.py
================================================
"""HostPortMapping DTO"""

from dataclasses import dataclass


@dataclass
class HostPortMappingDTO:
    """主机端口映射 DTO(资产表)"""
    target_id: int
    host: str
    ip: str
    port: int


================================================
FILE: backend/apps/asset/dtos/asset/ip_address_dto.py
================================================
"""IPAddress DTO"""

from dataclasses import dataclass


@dataclass
class IPAddressDTO:
    """
    IP地址数据传输对象
    
    只包含 IP 自身的信息,不包含关联关系。
    关联关系通过 SubdomainIPAssociationDTO 管理。
    """
    ip: str
    protocol_version: str = ''
    is_private: bool = False
    reverse_pointer: str = ''


================================================
FILE: backend/apps/asset/dtos/asset/port_dto.py
================================================
"""Port DTO"""

from dataclasses import dataclass


@dataclass
class PortDTO:
    """端口数据传输对象"""
    ip_address_id: int
    number: int
    service_name: str = ''
    target_id: int = None
    scan_id: int = None


================================================
FILE: backend/apps/asset/dtos/asset/subdomain_dto.py
================================================
"""Subdomain DTO"""

from dataclasses import dataclass


@dataclass
class SubdomainDTO:
    """
    子域名 DTO(纯资产表)
    
    用于传递子域名资产数据,只包含资产本身的信息。
    扫描相关信息存储在快照表中。
    """
    name: str
    target_id: int  # 必填:子域名必须属于某个目标


================================================
FILE: backend/apps/asset/dtos/asset/vulnerability_dto.py
================================================
"""Vulnerability DTO"""

from dataclasses import dataclass, field
from typing import Optional, Dict, Any
from decimal import Decimal


@dataclass
class VulnerabilityDTO:
    """漏洞数据传输对象(资产表用)"""
    target_id: int
    url: str
    vuln_type: str
    severity: str
    source: str = ""
    cvss_score: Optional[Decimal] = None
    description: str = ""
    raw_output: Dict[str, Any] = field(default_factory=dict)


================================================
FILE: backend/apps/asset/dtos/asset/website_dto.py
================================================
"""WebSite DTO"""

from dataclasses import dataclass
from typing import List, Optional


@dataclass
class WebSiteDTO:
    """网站数据传输对象"""
    target_id: int
    url: str
    host: str = ''
    title: str = ''
    status_code: Optional[int] = None
    content_length: Optional[int] = None
    location: str = ''
    webserver: str = ''
    content_type: str = ''
    tech: List[str] = None
    response_body: str = ''
    vhost: Optional[bool] = None
    created_at: str = None
    response_headers: str = ''
    
    def __post_init__(self):
        if self.tech is None:
            self.tech = []


================================================
FILE: backend/apps/asset/dtos/snapshot/__init__.py
================================================
"""Snapshot DTOs"""

from .subdomain_snapshot_dto import SubdomainSnapshotDTO
from .host_port_mapping_snapshot_dto import HostPortMappingSnapshotDTO
from .website_snapshot_dto import WebsiteSnapshotDTO
from .directory_snapshot_dto import DirectorySnapshotDTO
from .endpoint_snapshot_dto import EndpointSnapshotDTO
from .vulnerability_snapshot_dto import VulnerabilitySnapshotDTO

__all__ = [
    'SubdomainSnapshotDTO',
    'HostPortMappingSnapshotDTO',
    'WebsiteSnapshotDTO',
    'DirectorySnapshotDTO',
    'EndpointSnapshotDTO',
    'VulnerabilitySnapshotDTO',
]


================================================
FILE: backend/apps/asset/dtos/snapshot/directory_snapshot_dto.py
================================================
"""Directory Snapshot DTO"""

from dataclasses import dataclass
from typing import Optional
from apps.asset.dtos.asset import DirectoryDTO


@dataclass
class DirectorySnapshotDTO:
    """
    目录快照数据传输对象
    
    用于保存扫描过程中发现的目录信息到快照表
    
    注意:target_id 只用于传递数据和转换为资产 DTO,不会保存到快照表中。
    快照只属于 scan。
    """
    scan_id: int
    target_id: int  # 仅用于传递数据,不保存到数据库
    url: str
    status: Optional[int] = None
    content_length: Optional[int] = None
    words: Optional[int] = None
    lines: Optional[int] = None
    content_type: str = ''
    duration: Optional[int] = None
    
    def to_asset_dto(self) -> DirectoryDTO:
        """
        转换为资产 DTO(用于同步到资产表)
        
        注意:去除 scan_id 字段,因为资产表不需要
        
        Returns:
            DirectoryDTO: 资产表 DTO
        """
        return DirectoryDTO(
            target_id=self.target_id,
            url=self.url,
            status=self.status,
            content_length=self.content_length,
            words=self.words,
            lines=self.lines,
            content_type=self.content_type,
            duration=self.duration
        )


================================================
FILE: backend/apps/asset/dtos/snapshot/endpoint_snapshot_dto.py
================================================
"""EndpointSnapshot DTO"""

from dataclasses import dataclass
from typing import List, Optional


@dataclass
class EndpointSnapshotDTO:
    """
    端点快照 DTO
    
    注意:target_id 只用于传递数据和转换为资产 DTO,不会保存到快照表中。
    快照只属于 scan。
    """
    scan_id: int
    target_id: int  # 必填,用于同步到资产表
    url: str
    host: str = ''  # 主机名(域名或IP地址)
    title: str = ''
    status_code: Optional[int] = None
    content_length: Optional[int] = None
    location: str = ''
    webserver: str = ''
    content_type: str = ''
    tech: List[str] = None
    response_body: str = ''
    vhost: Optional[bool] = None
    matched_gf_patterns: List[str] = None
    response_headers: str = ''
    
    def __post_init__(self):
        if self.tech is None:
            self.tech = []
        if self.matched_gf_patterns is None:
            self.matched_gf_patterns = []
    
    def to_asset_dto(self):
        """
        转换为资产 DTO(用于同步到资产表)
        
        Returns:
            EndpointDTO: 资产表 DTO(移除 scan_id)
        """
        from apps.asset.dtos.asset import EndpointDTO
        
        return EndpointDTO(
            target_id=self.target_id,
            url=self.url,
            host=self.host,
            title=self.title,
            status_code=self.status_code,
            content_length=self.content_length,
            webserver=self.webserver,
            response_body=self.response_body,
            content_type=self.content_type,
            tech=self.tech if self.tech else [],
            vhost=self.vhost,
            location=self.location,
            matched_gf_patterns=self.matched_gf_patterns if self.matched_gf_patterns else [],
            response_headers=self.response_headers,
        )


================================================
FILE: backend/apps/asset/dtos/snapshot/host_port_mapping_snapshot_dto.py
================================================
"""HostPortMappingSnapshot DTO"""

from dataclasses import dataclass
from typing import Optional


@dataclass
class HostPortMappingSnapshotDTO:
    """主机端口映射快照 DTO"""
    scan_id: int
    host: str
    ip: str
    port: int
    target_id: Optional[int] = None  # 冗余字段,用于同步到资产表
    
    def to_asset_dto(self):
        """
        转换为资产 DTO(用于同步到资产表)
        
        Returns:
            HostPortMappingDTO: 资产表 DTO(移除 scan_id)
        """
        from apps.asset.dtos.asset import HostPortMappingDTO
        
        if self.target_id is None:
            raise ValueError("target_id 不能为 None,无法同步到资产表")
        
        return HostPortMappingDTO(
            target_id=self.target_id,
            host=self.host,
            ip=self.ip,
            port=self.port
        )


================================================
FILE: backend/apps/asset/dtos/snapshot/subdomain_snapshot_dto.py
================================================
"""SubdomainSnapshot DTO"""

from dataclasses import dataclass
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from apps.asset.dtos import SubdomainDTO


@dataclass
class SubdomainSnapshotDTO:
    """
    子域名快照 DTO
    
    用于传递快照数据,包含完整的业务上下文信息。
    快照表记录每次扫描的历史数据。
    """
    name: str
    scan_id: int  # 必填:快照必须关联扫描任务
    target_id: int  # 必填:目标ID(用于转换为资产 DTO)
    
    def to_asset_dto(self) -> 'SubdomainDTO':
        """
        转换为资产 DTO(用于保存到资产表)
        
        Returns:
            SubdomainDTO: 资产 DTO(不包含 scan_id)
        
        Note:
            资产表只存储核心数据,扫描上下文(scan_id)不保存到资产表。
            target_id 已经包含在 DTO 中,无需额外传参。
        """
        from apps.asset.dtos import SubdomainDTO
        return SubdomainDTO(name=self.name, target_id=self.target_id)


================================================
FILE: backend/apps/asset/dtos/snapshot/vulnerability_snapshot_dto.py
================================================
"""VulnerabilitySnapshot DTO"""

from dataclasses import dataclass, field
from typing import Optional, Dict, Any
from decimal import Decimal


@dataclass
class VulnerabilitySnapshotDTO:
    """漏洞快照 DTO

    对应 VulnerabilitySnapshot 模型,用于在 Service/Task 之间传递漏洞结果数据。

    设计与其他快照 DTO 一致:
    - scan_id: 只属于快照表
    - target_id: 只用于转换为资产 DTO,不直接存入快照表
    """

    scan_id: int
    target_id: int  # 仅用于传递数据和生成资产 DTO,不保存到快照表
    url: str
    vuln_type: str
    severity: str
    source: str = ""
    cvss_score: Optional[Decimal] = None
    description: str = ""
    raw_output: Dict[str, Any] = field(default_factory=dict)

    def to_asset_dto(self):
        """转换为漏洞资产 DTO(用于同步到 Vulnerability 表)。"""
        from apps.asset.dtos.asset import VulnerabilityDTO

        return VulnerabilityDTO(
            target_id=self.target_id,
            url=self.url,
            vuln_type=self.vuln_type,
            severity=self.severity,
            source=self.source,
            cvss_score=self.cvss_score,
            description=self.description,
            raw_output=self.raw_output,
        )


================================================
FILE: backend/apps/asset/dtos/snapshot/website_snapshot_dto.py
================================================
"""WebsiteSnapshot DTO"""

from dataclasses import dataclass
from typing import List, Optional


@dataclass
class WebsiteSnapshotDTO:
    """
    网站快照 DTO
    
    注意:target_id 只用于传递数据和转换为资产 DTO,不会保存到快照表中。
    快照只属于 scan,target 信息通过 scan.target 获取。
    """
    scan_id: int
    target_id: int  # 必填,用于同步到资产表
    url: str
    host: str
    title: str = ''
    status_code: Optional[int] = None  # 统一命名:status -> status_code
    content_length: Optional[int] = None
    location: str = ''
    webserver: str = ''  # 统一命名:web_server -> webserver
    content_type: str = ''
    tech: List[str] = None
    response_body: str = ''
    vhost: Optional[bool] = None
    response_headers: str = ''
    
    def __post_init__(self):
        if self.tech is None:
            self.tech = []
    
    def to_asset_dto(self):
        """
        转换为资产 DTO(用于同步到资产表)
        
        Returns:
            WebSiteDTO: 资产表 DTO(移除 scan_id)
        """
        from apps.asset.dtos.asset import WebSiteDTO
        
        return WebSiteDTO(
            target_id=self.target_id,
            url=self.url,
            host=self.host,
            title=self.title,
            status_code=self.status_code,
            content_length=self.content_length,
            location=self.location,
            webserver=self.webserver,
            content_type=self.content_type,
            tech=self.tech if self.tech else [],
            response_body=self.response_body,
            vhost=self.vhost,
            response_headers=self.response_headers,
        )


================================================
FILE: backend/apps/asset/migrations/0001_initial.py
================================================
# Generated by Django 5.2.7 on 2026-01-06 00:55

import django.contrib.postgres.fields
import django.contrib.postgres.indexes
import django.core.validators
import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = [
        ('scan', '0001_initial'),
        ('targets', '0001_initial'),
    ]

    operations = [
        migrations.CreateModel(
            name='AssetStatistics',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('total_targets', models.IntegerField(default=0, help_text='目标总数')),
                ('total_subdomains', models.IntegerField(default=0, help_text='子域名总数')),
                ('total_ips', models.IntegerField(default=0, help_text='IP地址总数')),
                ('total_endpoints', models.IntegerField(default=0, help_text='端点总数')),
                ('total_websites', models.IntegerField(default=0, help_text='网站总数')),
                ('total_vulns', models.IntegerField(default=0, help_text='漏洞总数')),
                ('total_assets', models.IntegerField(default=0, help_text='总资产数(子域名+IP+端点+网站)')),
                ('prev_targets', models.IntegerField(default=0, help_text='上次目标总数')),
                ('prev_subdomains', models.IntegerField(default=0, help_text='上次子域名总数')),
                ('prev_ips', models.IntegerField(default=0, help_text='上次IP地址总数')),
                ('prev_endpoints', models.IntegerField(default=0, help_text='上次端点总数')),
                ('prev_websites', models.IntegerField(default=0, help_text='上次网站总数')),
                ('prev_vulns', models.IntegerField(default=0, help_text='上次漏洞总数')),
                ('prev_assets', models.IntegerField(default=0, help_text='上次总资产数')),
                ('updated_at', models.DateTimeField(auto_now=True, help_text='最后更新时间')),
            ],
            options={
                'verbose_name': '资产统计',
                'verbose_name_plural': '资产统计',
                'db_table': 'asset_statistics',
            },
        ),
        migrations.CreateModel(
            name='StatisticsHistory',
            fields=[
                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('date', models.DateField(help_text='统计日期', unique=True)),
                ('total_targets', models.IntegerField(default=0, help_text='目标总数')),
                ('total_subdomains', models.IntegerField(default=0, help_text='子域名总数')),
                ('total_ips', models.IntegerField(default=0, help_text='IP地址总数')),
                ('total_endpoints', models.IntegerField(default=0, help_text='端点总数')),
                ('total_websites', models.IntegerField(default=0, help_text='网站总数')),
                ('total_vulns', models.IntegerField(default=0, help_text='漏洞总数')),
                ('total_assets', models.IntegerField(default=0, help_text='总资产数')),
                ('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
                ('updated_at', models.DateTimeField(auto_now=True, help_text='更新时间')),
            ],
            options={
                'verbose_name': '统计历史',
                'verbose_name_plural': '统计历史',
                'db_table': 'statistics_history',
                'ordering': ['-date'],
                'indexes': [models.Index(fields=['date'], name='statistics__date_1d29cd_idx')],
            },
        ),
        migrations.CreateModel(
            name='Directory',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('url', models.CharField(help_text='完整请求 URL', max_length=2000)),
                ('status', models.IntegerField(blank=True, help_text='HTTP 响应状态码', null=True)),
                ('content_length', models.BigIntegerField(blank=True, help_text='响应体字节大小(Content-Length 或实际长度)', null=True)),
                ('words', models.IntegerField(blank=True, help_text='响应体中单词数量(按空格分割)', null=True)),
                ('lines', models.IntegerField(blank=True, help_text='响应体行数(按换行符分割)', null=True)),
                ('content_type', models.CharField(blank=True, default='', help_text='响应头 Content-Type 值', max_length=200)),
                ('duration', models.BigIntegerField(blank=True, help_text='请求耗时(单位:纳秒)', null=True)),
                ('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
                ('target', models.ForeignKey(help_text='所属的扫描目标', on_delete=django.db.models.deletion.CASCADE, related_name='directories', to='targets.target')),
            ],
            options={
                'verbose_name': '目录',
                'verbose_name_plural': '目录',
                'db_table': 'directory',
                'ordering': ['-created_at'],
                'indexes': [models.Index(fields=['-created_at'], name='directory_created_2cef03_idx'), models.Index(fields=['target'], name='directory_target__e310c8_idx'), models.Index(fields=['url'], name='directory_url_ba40cd_idx'), models.Index(fields=['status'], name='directory_status_40bbe6_idx'), django.contrib.postgres.indexes.GinIndex(fields=['url'], name='directory_url_trgm_idx', opclasses=['gin_trgm_ops'])],
                'constraints': [models.UniqueConstraint(fields=('target', 'url'), name='unique_directory_url_target')],
            },
        ),
        migrations.CreateModel(
            name='DirectorySnapshot',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('url', models.CharField(help_text='目录URL', max_length=2000)),
                ('status', models.IntegerField(blank=True, help_text='HTTP状态码', null=True)),
                ('content_length', models.BigIntegerField(blank=True, help_text='内容长度', null=True)),
                ('words', models.IntegerField(blank=True, help_text='响应体中单词数量(按空格分割)', null=True)),
                ('lines', models.IntegerField(blank=True, help_text='响应体行数(按换行符分割)', null=True)),
                ('content_type', models.CharField(blank=True, default='', help_text='响应头 Content-Type 值', max_length=200)),
                ('duration', models.BigIntegerField(blank=True, help_text='请求耗时(单位:纳秒)', null=True)),
                ('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
                ('scan', models.ForeignKey(help_text='所属的扫描任务', on_delete=django.db.models.deletion.CASCADE, related_name='directory_snapshots', to='scan.scan')),
            ],
            options={
                'verbose_name': '目录快照',
                'verbose_name_plural': '目录快照',
                'db_table': 'directory_snapshot',
                'ordering': ['-created_at'],
                'indexes': [models.Index(fields=['scan'], name='directory_s_scan_id_c45900_idx'), models.Index(fields=['url'], name='directory_s_url_b4b72b_idx'), models.Index(fields=['status'], name='directory_s_status_e9f57e_idx'), models.Index(fields=['content_type'], name='directory_s_content_45e864_idx'), models.Index(fields=['-created_at'], name='directory_s_created_eb9d27_idx'), django.contrib.postgres.indexes.GinIndex(fields=['url'], name='dir_snap_url_trgm', opclasses=['gin_trgm_ops'])],
                'constraints': [models.UniqueConstraint(fields=('scan', 'url'), name='unique_directory_per_scan_snapshot')],
            },
        ),
        migrations.CreateModel(
            name='Endpoint',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('url', models.TextField(help_text='最终访问的完整URL')),
                ('host', models.CharField(blank=True, default='', help_text='主机名(域名或IP地址)', max_length=253)),
                ('location', models.TextField(blank=True, default='', help_text='重定向地址(HTTP 3xx 响应头 Location)')),
                ('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
                ('title', models.TextField(blank=True, default='', help_text='网页标题(HTML <title> 标签内容)')),
                ('webserver', models.TextField(blank=True, default='', help_text='服务器类型(HTTP 响应头 Server 值)')),
                ('response_body', models.TextField(blank=True, default='', help_text='HTTP响应体')),
                ('content_type', models.TextField(blank=True, default='', help_text='响应类型(HTTP Content-Type 响应头)')),
                ('tech', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, default=list, help_text='技术栈(服务器/框架/语言等)', size=None)),
                ('status_code', models.IntegerField(blank=True, help_text='HTTP状态码', null=True)),
                ('content_length', models.IntegerField(blank=True, help_text='响应体大小(单位字节)', null=True)),
                ('vhost', models.BooleanField(blank=True, help_text='是否支持虚拟主机', null=True)),
                ('matched_gf_patterns', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, default=list, help_text='匹配的GF模式列表,用于识别敏感端点(如api, debug, config等)', size=None)),
                ('response_headers', models.TextField(blank=True, default='', help_text='原始HTTP响应头')),
                ('target', models.ForeignKey(help_text='所属的扫描目标(主关联字段,表示所属关系,不能为空)', on_delete=django.db.models.deletion.CASCADE, related_name='endpoints', to='targets.target')),
            ],
            options={
                'verbose_name': '端点',
                'verbose_name_plural': '端点',
                'db_table': 'endpoint',
                'ordering': ['-created_at'],
                'indexes': [models.Index(fields=['-created_at'], name='endpoint_created_44fe9c_idx'), models.Index(fields=['target'], name='endpoint_target__7f9065_idx'), models.Index(fields=['url'], name='endpoint_url_30f66e_idx'), models.Index(fields=['host'], name='endpoint_host_5b4cc8_idx'), models.Index(fields=['status_code'], name='endpoint_status__5d4fdd_idx'), models.Index(fields=['title'], name='endpoint_title_29e26c_idx'), django.contrib.postgres.indexes.GinIndex(fields=['tech'], name='endpoint_tech_2bfa7c_gin'), django.contrib.postgres.indexes.GinIndex(fields=['response_headers'], name='endpoint_resp_headers_trgm_idx', opclasses=['gin_trgm_ops']), django.contrib.postgres.indexes.GinIndex(fields=['url'], name='endpoint_url_trgm_idx', opclasses=['gin_trgm_ops']), django.contrib.postgres.indexes.GinIndex(fields=['title'], name='endpoint_title_trgm_idx', opclasses=['gin_trgm_ops'])],
                'constraints': [models.UniqueConstraint(fields=('url', 'target'), name='unique_endpoint_url_target')],
            },
        ),
        migrations.CreateModel(
            name='EndpointSnapshot',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('url', models.TextField(help_text='端点URL')),
                ('host', models.CharField(blank=True, default='', help_text='主机名(域名或IP地址)', max_length=253)),
                ('title', models.TextField(blank=True, default='', help_text='页面标题')),
                ('status_code', models.IntegerField(blank=True, help_text='HTTP状态码', null=True)),
                ('content_length', models.IntegerField(blank=True, help_text='内容长度', null=True)),
                ('location', models.TextField(blank=True, default='', help_text='重定向位置')),
                ('webserver', models.TextField(blank=True, default='', help_text='Web服务器')),
                ('content_type', models.TextField(blank=True, default='', help_text='内容类型')),
                ('tech', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, default=list, help_text='技术栈', size=None)),
                ('response_body', models.TextField(blank=True, default='', help_text='HTTP响应体')),
                ('vhost', models.BooleanField(blank=True, help_text='虚拟主机标志', null=True)),
                ('matched_gf_patterns', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, default=list, help_text='匹配的GF模式列表', size=None)),
                ('response_headers', models.TextField(blank=True, default='', help_text='原始HTTP响应头')),
                ('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
                ('scan', models.ForeignKey(help_text='所属的扫描任务', on_delete=django.db.models.deletion.CASCADE, related_name='endpoint_snapshots', to='scan.scan')),
            ],
            options={
                'verbose_name': '端点快照',
                'verbose_name_plural': '端点快照',
                'db_table': 'endpoint_snapshot',
                'ordering': ['-created_at'],
                'indexes': [models.Index(fields=['scan'], name='endpoint_sn_scan_id_6ac9a7_idx'), models.Index(fields=['url'], name='endpoint_sn_url_205160_idx'), models.Index(fields=['host'], name='endpoint_sn_host_577bfd_idx'), models.Index(fields=['title'], name='endpoint_sn_title_516a05_idx'), models.Index(fields=['status_code'], name='endpoint_sn_status__83efb0_idx'), models.Index(fields=['webserver'], name='endpoint_sn_webserv_66be83_idx'), models.Index(fields=['-created_at'], name='endpoint_sn_created_21fb5b_idx'), django.contrib.postgres.indexes.GinIndex(fields=['tech'], name='endpoint_sn_tech_0d0752_gin'), django.contrib.postgres.indexes.GinIndex(fields=['response_headers'], name='ep_snap_resp_hdr_trgm', opclasses=['gin_trgm_ops']), django.contrib.postgres.indexes.GinIndex(fields=['url'], name='ep_snap_url_trgm', opclasses=['gin_trgm_ops']), django.contrib.postgres.indexes.GinIndex(fields=['title'], name='ep_snap_title_trgm', opclasses=['gin_trgm_ops'])],
                'constraints': [models.UniqueConstraint(fields=('scan', 'url'), name='unique_endpoint_per_scan_snapshot')],
            },
        ),
        migrations.CreateModel(
            name='HostPortMapping',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('host', models.CharField(help_text='主机名(域名或IP)', max_length=1000)),
                ('ip', models.GenericIPAddressField(help_text='IP地址')),
                ('port', models.IntegerField(help_text='端口号(1-65535)', validators=[django.core.validators.MinValueValidator(1, message='端口号必须大于等于1'), django.core.validators.MaxValueValidator(65535, message='端口号必须小于等于65535')])),
                ('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
                ('target', models.ForeignKey(help_text='所属的扫描目标', on_delete=django.db.models.deletion.CASCADE, related_name='host_port_mappings', to='targets.target')),
            ],
            options={
                'verbose_name': '主机端口映射',
                'verbose_name_plural': '主机端口映射',
                'db_table': 'host_port_mapping',
                'ordering': ['-created_at'],
                'indexes': [models.Index(fields=['target'], name='host_port_m_target__943e9b_idx'), models.Index(fields=['host'], name='host_port_m_host_f78363_idx'), models.Index(fields=['ip'], name='host_port_m_ip_2e6f02_idx'), models.Index(fields=['port'], name='host_port_m_port_9fb9ff_idx'), models.Index(fields=['host', 'ip'], name='host_port_m_host_3ce245_idx'), models.Index(fields=['-created_at'], name='host_port_m_created_11cd22_idx')],
                'constraints': [models.UniqueConstraint(fields=('target', 'host', 'ip', 'port'), name='unique_target_host_ip_port')],
            },
        ),
        migrations.CreateModel(
            name='HostPortMappingSnapshot',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('host', models.CharField(help_text='主机名(域名或IP)', max_length=1000)),
                ('ip', models.GenericIPAddressField(help_text='IP地址')),
                ('port', models.IntegerField(help_text='端口号(1-65535)', validators=[django.core.validators.MinValueValidator(1, message='端口号必须大于等于1'), django.core.validators.MaxValueValidator(65535, message='端口号必须小于等于65535')])),
                ('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
                ('scan', models.ForeignKey(help_text='所属的扫描任务(主关联)', on_delete=django.db.models.deletion.CASCADE, related_name='host_port_mapping_snapshots', to='scan.scan')),
            ],
            options={
                'verbose_name': '主机端口映射快照',
                'verbose_name_plural': '主机端口映射快照',
                'db_table': 'host_port_mapping_snapshot',
                'ordering': ['-created_at'],
                'indexes': [models.Index(fields=['scan'], name='host_port_m_scan_id_50ba0b_idx'), models.Index(fields=['host'], name='host_port_m_host_e99054_idx'), models.Index(fields=['ip'], name='host_port_m_ip_54818c_idx'), models.Index(fields=['port'], name='host_port_m_port_ed7b48_idx'), models.Index(fields=['host', 'ip'], name='host_port_m_host_8a463a_idx'), models.Index(fields=['scan', 'host'], name='host_port_m_scan_id_426fdb_idx'), models.Index(fields=['-created_at'], name='host_port_m_created_fb28b8_idx')],
                'constraints': [models.UniqueConstraint(fields=('scan', 'host', 'ip', 'port'), name='unique_scan_host_ip_port_snapshot')],
            },
        ),
        migrations.CreateModel(
            name='Subdomain',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('name', models.CharField(help_text='子域名名称', max_length=1000)),
                ('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
                ('target', models.ForeignKey(help_text='所属的扫描目标(主关联字段,表示所属关系,不能为空)', on_delete=django.db.models.deletion.CASCADE, related_name='subdomains', to='targets.target')),
            ],
            options={
                'verbose_name': '子域名',
                'verbose_name_plural': '子域名',
                'db_table': 'subdomain',
                'ordering': ['-created_at'],
                'indexes': [models.Index(fields=['-created_at'], name='subdomain_created_e187a8_idx'), models.Index(fields=['name', 'target'], name='subdomain_name_60e1d0_idx'), models.Index(fields=['target'], name='subdomain_target__e409f0_idx'), models.Index(fields=['name'], name='subdomain_name_d40ba7_idx'), django.contrib.postgres.indexes.GinIndex(fields=['name'], name='subdomain_name_trgm_idx', opclasses=['gin_trgm_ops'])],
                'constraints': [models.UniqueConstraint(fields=('name', 'target'), name='unique_subdomain_name_target')],
            },
        ),
        migrations.CreateModel(
            name='SubdomainSnapshot',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('name', models.CharField(help_text='子域名名称', max_length=1000)),
                ('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
                ('scan', models.ForeignKey(help_text='所属的扫描任务', on_delete=django.db.models.deletion.CASCADE, related_name='subdomain_snapshots', to='scan.scan')),
            ],
            options={
                'verbose_name': '子域名快照',
                'verbose_name_plural': '子域名快照',
                'db_table': 'subdomain_snapshot',
                'ordering': ['-created_at'],
                'indexes': [models.Index(fields=['scan'], name='subdomain_s_scan_id_68c253_idx'), models.Index(fields=['name'], name='subdomain_s_name_2da42b_idx'), models.Index(fields=['-created_at'], name='subdomain_s_created_d2b48e_idx'), django.contrib.postgres.indexes.GinIndex(fields=['name'], name='subdomain_snap_name_trgm', opclasses=['gin_trgm_ops'])],
                'constraints': [models.UniqueConstraint(fields=('scan', 'name'), name='unique_subdomain_per_scan_snapshot')],
            },
        ),
        migrations.CreateModel(
            name='Vulnerability',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('url', models.CharField(help_text='漏洞所在的URL', max_length=2000)),
                ('vuln_type', models.CharField(help_text='漏洞类型(如 xss, sqli)', max_length=100)),
                ('severity', models.CharField(choices=[('unknown', '未知'), ('info', '信息'), ('low', '低'), ('medium', '中'), ('high', '高'), ('critical', '危急')], default='unknown', help_text='严重性(未知/信息/低/中/高/危急)', max_length=20)),
                ('source', models.CharField(blank=True, default='', help_text='来源工具(如 dalfox, nuclei, crlfuzz)', max_length=50)),
                ('cvss_score', models.DecimalField(blank=True, decimal_places=1, help_text='CVSS 评分(0.0-10.0)', max_digits=3, null=True)),
                ('description', models.TextField(blank=True, default='', help_text='漏洞描述')),
                ('raw_output', models.JSONField(blank=True, default=dict, help_text='工具原始输出')),
                ('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
                ('target', models.ForeignKey(help_text='所属的扫描目标', on_delete=django.db.models.deletion.CASCADE, related_name='vulnerabilities', to='targets.target')),
            ],
            options={
                'verbose_name': '漏洞',
                'verbose_name_plural': '漏洞',
                'db_table': 'vulnerability',
                'ordering': ['-created_at'],
                'indexes': [models.Index(fields=['target'], name='vulnerabili_target__755a02_idx'), models.Index(fields=['vuln_type'], name='vulnerabili_vuln_ty_3010cd_idx'), models.Index(fields=['severity'], name='vulnerabili_severit_1a798b_idx'), models.Index(fields=['source'], name='vulnerabili_source_7c7552_idx'), models.Index(fields=['url'], name='vulnerabili_url_4dcc4d_idx'), models.Index(fields=['-created_at'], name='vulnerabili_created_e25ff7_idx')],
            },
        ),
        migrations.CreateModel(
            name='VulnerabilitySnapshot',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('url', models.CharField(help_text='漏洞所在的URL', max_length=2000)),
                ('vuln_type', models.CharField(help_text='漏洞类型(如 xss, sqli)', max_length=100)),
                ('severity', models.CharField(choices=[('unknown', '未知'), ('info', '信息'), ('low', '低'), ('medium', '中'), ('high', '高'), ('critical', '危急')], default='unknown', help_text='严重性(未知/信息/低/中/高/危急)', max_length=20)),
                ('source', models.CharField(blank=True, default='', help_text='来源工具(如 dalfox, nuclei, crlfuzz)', max_length=50)),
                ('cvss_score', models.DecimalField(blank=True, decimal_places=1, help_text='CVSS 评分(0.0-10.0)', max_digits=3, null=True)),
                ('description', models.TextField(blank=True, default='', help_text='漏洞描述')),
                ('raw_output', models.JSONField(blank=True, default=dict, help_text='工具原始输出')),
                ('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
                ('scan', models.ForeignKey(help_text='所属的扫描任务', on_delete=django.db.models.deletion.CASCADE, related_name='vulnerability_snapshots', to='scan.scan')),
            ],
            options={
                'verbose_name': '漏洞快照',
                'verbose_name_plural': '漏洞快照',
                'db_table': 'vulnerability_snapshot',
                'ordering': ['-created_at'],
                'indexes': [models.Index(fields=['scan'], name='vulnerabili_scan_id_7b81c9_idx'), models.Index(fields=['url'], name='vulnerabili_url_11a707_idx'), models.Index(fields=['vuln_type'], name='vulnerabili_vuln_ty_6b90ee_idx'), models.Index(fields=['severity'], name='vulnerabili_severit_4eae0d_idx'), models.Index(fields=['source'], name='vulnerabili_source_968b1f_idx'), models.Index(fields=['-created_at'], name='vulnerabili_created_53a12e_idx')],
            },
        ),
        migrations.CreateModel(
            name='WebSite',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('url', models.TextField(help_text='最终访问的完整URL')),
                ('host', models.CharField(blank=True, default='', help_text='主机名(域名或IP地址)', max_length=253)),
                ('location', models.TextField(blank=True, default='', help_text='重定向地址(HTTP 3xx 响应头 Location)')),
                ('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
                ('title', models.TextField(blank=True, default='', help_text='网页标题(HTML <title> 标签内容)')),
                ('webserver', models.TextField(blank=True, default='', help_text='服务器类型(HTTP 响应头 Server 值)')),
                ('response_body', models.TextField(blank=True, default='', help_text='HTTP响应体')),
                ('content_type', models.TextField(blank=True, default='', help_text='响应类型(HTTP Content-Type 响应头)')),
                ('tech', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, default=list, help_text='技术栈(服务器/框架/语言等)', size=None)),
                ('status_code', models.IntegerField(blank=True, help_text='HTTP状态码', null=True)),
                ('content_length', models.IntegerField(blank=True, help_text='响应体大小(单位字节)', null=True)),
                ('vhost', models.BooleanField(blank=True, help_text='是否支持虚拟主机', null=True)),
                ('response_headers', models.TextField(blank=True, default='', help_text='原始HTTP响应头')),
                ('target', models.ForeignKey(help_text='所属的扫描目标(主关联字段,表示所属关系,不能为空)', on_delete=django.db.models.deletion.CASCADE, related_name='websites', to='targets.target')),
            ],
            options={
                'verbose_name': '站点',
                'verbose_name_plural': '站点',
                'db_table': 'website',
                'ordering': ['-created_at'],
                'indexes': [models.Index(fields=['-created_at'], name='website_created_c9cfd2_idx'), models.Index(fields=['url'], name='website_url_b18883_idx'), models.Index(fields=['host'], name='website_host_996b50_idx'), models.Index(fields=['target'], name='website_target__2a353b_idx'), models.Index(fields=['title'], name='website_title_c2775b_idx'), models.Index(fields=['status_code'], name='website_status__51663d_idx'), django.contrib.postgres.indexes.GinIndex(fields=['tech'], name='website_tech_e3f0cb_gin'), django.contrib.postgres.indexes.GinIndex(fields=['response_headers'], name='website_resp_headers_trgm_idx', opclasses=['gin_trgm_ops']), django.contrib.postgres.indexes.GinIndex(fields=['url'], name='website_url_trgm_idx', opclasses=['gin_trgm_ops']), django.contrib.postgres.indexes.GinIndex(fields=['title'], name='website_title_trgm_idx', opclasses=['gin_trgm_ops'])],
                'constraints': [models.UniqueConstraint(fields=('url', 'target'), name='unique_website_url_target')],
            },
        ),
        migrations.CreateModel(
            name='WebsiteSnapshot',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('url', models.TextField(help_text='站点URL')),
                ('host', models.CharField(blank=True, default='', help_text='主机名(域名或IP地址)', max_length=253)),
                ('title', models.TextField(blank=True, default='', help_text='页面标题')),
                ('status_code', models.IntegerField(blank=True, help_text='HTTP状态码', null=True)),
                ('content_length', models.BigIntegerField(blank=True, help_text='内容长度', null=True)),
                ('location', models.TextField(blank=True, default='', help_text='重定向位置')),
                ('webserver', models.TextField(blank=True, default='', help_text='Web服务器')),
                ('content_type', models.TextField(blank=True, default='', help_text='内容类型')),
                ('tech', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=100), blank=True, default=list, help_text='技术栈', size=None)),
                ('response_body', models.TextField(blank=True, default='', help_text='HTTP响应体')),
                ('vhost', models.BooleanField(blank=True, help_text='虚拟主机标志', null=True)),
                ('response_headers', models.TextField(blank=True, default='', help_text='原始HTTP响应头')),
                ('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
                ('scan', models.ForeignKey(help_text='所属的扫描任务', on_delete=django.db.models.deletion.CASCADE, related_name='website_snapshots', to='scan.scan')),
            ],
            options={
                'verbose_name': '网站快照',
                'verbose_name_plural': '网站快照',
                'db_table': 'website_snapshot',
                'ordering': ['-created_at'],
                'indexes': [models.Index(fields=['scan'], name='website_sna_scan_id_26b6dc_idx'), models.Index(fields=['url'], name='website_sna_url_801a70_idx'), models.Index(fields=['host'], name='website_sna_host_348fe1_idx'), models.Index(fields=['title'], name='website_sna_title_b1a5ee_idx'), models.Index(fields=['-created_at'], name='website_sna_created_2c149a_idx'), django.contrib.postgres.indexes.GinIndex(fields=['tech'], name='website_sna_tech_3d6d2f_gin'), django.contrib.postgres.indexes.GinIndex(fields=['response_headers'], name='ws_snap_resp_hdr_trgm', opclasses=['gin_trgm_ops']), django.contrib.postgres.indexes.GinIndex(fields=['url'], name='ws_snap_url_trgm', opclasses=['gin_trgm_ops']), django.contrib.postgres.indexes.GinIndex(fields=['title'], name='ws_snap_title_trgm', opclasses=['gin_trgm_ops'])],
                'constraints': [models.UniqueConstraint(fields=('scan', 'url'), name='unique_website_per_scan_snapshot')],
            },
        ),
    ]


================================================
FILE: backend/apps/asset/migrations/0002_create_search_views.py
================================================
"""
创建资产搜索物化视图(使用 pg_ivm 增量维护)

这些视图用于资产搜索功能,提供高性能的全文搜索能力。
"""

from django.db import migrations


class Migration(migrations.Migration):
    """创建资产搜索所需的增量物化视图"""

    dependencies = [
        ('asset', '0001_initial'),
    ]

    operations = [
        # 1. 确保 pg_ivm 扩展已安装
        migrations.RunSQL(
            sql="CREATE EXTENSION IF NOT EXISTS pg_ivm;",
            reverse_sql="DROP EXTENSION IF EXISTS pg_ivm;",
        ),
        
        # 2. 创建 Website 搜索视图
        # 注意:pg_ivm 不支持 ArrayField,所以 tech 字段需要从原表 JOIN 获取
        migrations.RunSQL(
            sql="""
                SELECT pgivm.create_immv('asset_search_view', $$
                    SELECT 
                        w.id,
                        w.url,
                        w.host,
                        w.title,
                        w.status_code,
                        w.response_headers,
                        w.response_body,
                        w.content_type,
                        w.content_length,
                        w.webserver,
                        w.location,
                        w.vhost,
                        w.created_at,
                        w.target_id
                    FROM website w
                $$);
            """,
            reverse_sql="DROP TABLE IF EXISTS asset_search_view CASCADE;",
        ),
        
        # 3. 创建 Endpoint 搜索视图
        migrations.RunSQL(
            sql="""
                SELECT pgivm.create_immv('endpoint_search_view', $$
                    SELECT 
                        e.id,
                        e.url,
                        e.host,
                        e.title,
                        e.status_code,
                        e.response_headers,
                        e.response_body,
                        e.content_type,
                        e.content_length,
                        e.webserver,
                        e.location,
                        e.vhost,
                        e.created_at,
                        e.target_id
                    FROM endpoint e
                $$);
            """,
            reverse_sql="DROP TABLE IF EXISTS endpoint_search_view CASCADE;",
        ),
        
        # 4. 为搜索视图创建索引(加速查询)
        migrations.RunSQL(
            sql=[
                # Website 搜索视图索引
                "CREATE INDEX IF NOT EXISTS asset_search_view_host_idx ON asset_search_view (host);",
                "CREATE INDEX IF NOT EXISTS asset_search_view_url_idx ON asset_search_view (url);",
                "CREATE INDEX IF NOT EXISTS asset_search_view_title_idx ON asset_search_view (title);",
                "CREATE INDEX IF NOT EXISTS asset_search_view_status_idx ON asset_search_view (status_code);",
                "CREATE INDEX IF NOT EXISTS asset_search_view_created_idx ON asset_search_view (created_at DESC);",
                # Endpoint 搜索视图索引
                "CREATE INDEX IF NOT EXISTS endpoint_search_view_host_idx ON endpoint_search_view (host);",
                "CREATE INDEX IF NOT EXISTS endpoint_search_view_url_idx ON endpoint_search_view (url);",
                "CREATE INDEX IF NOT EXISTS endpoint_search_view_title_idx ON endpoint_search_view (title);",
                "CREATE INDEX IF NOT EXISTS endpoint_search_view_status_idx ON endpoint_search_view (status_code);",
                "CREATE INDEX IF NOT EXISTS endpoint_search_view_created_idx ON endpoint_search_view (created_at DESC);",
            ],
            reverse_sql=[
                "DROP INDEX IF EXISTS asset_search_view_host_idx;",
                "DROP INDEX IF EXISTS asset_search_view_url_idx;",
                "DROP INDEX IF EXISTS asset_search_view_title_idx;",
                "DROP INDEX IF EXISTS asset_search_view_status_idx;",
                "DROP INDEX IF EXISTS asset_search_view_created_idx;",
                "DROP INDEX IF EXISTS endpoint_search_view_host_idx;",
                "DROP INDEX IF EXISTS endpoint_search_view_url_idx;",
                "DROP INDEX IF EXISTS endpoint_search_view_title_idx;",
                "DROP INDEX IF EXISTS endpoint_search_view_status_idx;",
                "DROP INDEX IF EXISTS endpoint_search_view_created_idx;",
            ],
        ),
    ]


================================================
FILE: backend/apps/asset/migrations/0003_add_screenshot_models.py
================================================
# Generated by Django 5.2.7 on 2026-01-07 02:21

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('asset', '0002_create_search_views'),
        ('scan', '0001_initial'),
        ('targets', '0001_initial'),
    ]

    operations = [
        migrations.CreateModel(
            name='Screenshot',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('url', models.TextField(help_text='截图对应的 URL')),
                ('image', models.BinaryField(help_text='截图 WebP 二进制数据(压缩后)')),
                ('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
                ('updated_at', models.DateTimeField(auto_now=True, help_text='更新时间')),
                ('target', models.ForeignKey(help_text='所属目标', on_delete=django.db.models.deletion.CASCADE, related_name='screenshots', to='targets.target')),
            ],
            options={
                'verbose_name': '截图',
                'verbose_name_plural': '截图',
                'db_table': 'screenshot',
                'ordering': ['-created_at'],
                'indexes': [models.Index(fields=['target'], name='screenshot_target__2f01f6_idx'), models.Index(fields=['-created_at'], name='screenshot_created_c0ad4b_idx')],
                'constraints': [models.UniqueConstraint(fields=('target', 'url'), name='unique_screenshot_per_target')],
            },
        ),
        migrations.CreateModel(
            name='ScreenshotSnapshot',
            fields=[
                ('id', models.AutoField(primary_key=True, serialize=False)),
                ('url', models.TextField(help_text='截图对应的 URL')),
                ('image', models.BinaryField(help_text='截图 WebP 二进制数据(压缩后)')),
                ('created_at', models.DateTimeField(auto_now_add=True, help_text='创建时间')),
                ('scan', models.ForeignKey(help_text='所属的扫描任务', on_delete=django.db.models.deletion.CASCADE, related_name='screenshot_snapshots', to='scan.scan')),
            ],
            options={
                'verbose_name': '截图快照',
                'verbose_name_plural': '截图快照',
                'db_table': 'screenshot_snapshot',
                'ordering': ['-created_at'],
                'indexes': [models.Index(fields=['scan'], name='screenshot__scan_id_fb8c4d_idx'), models.Index(fields=['-created_at'], name='screenshot__created_804117_idx')],
                'constraints': [models.UniqueConstraint(fields=('scan', 'url'), name='unique_screenshot_per_scan_snapshot')],
            },
        ),
    ]


================================================
FILE: backend/apps/asset/migrations/0004_add_status_code_to_screenshot.py
================================================
# Generated by Django 5.2.7 on 2026-01-07 13:29

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('asset', '0003_add_screenshot_models'),
    ]

    operations = [
        migrations.AddField(
            model_name='screenshot',
            name='status_code',
            field=models.SmallIntegerField(blank=True, help_text='HTTP 响应状态码', null=True),
        ),
        migrations.AddField(
            model_name='screenshotsnapshot',
            name='status_code',
            field=models.SmallIntegerField(blank=True, help_text='HTTP 响应状态码', null=True),
        ),
    ]


================================================
FILE: backend/apps/asset/migrations/__init__.py
================================================


================================================
FILE: backend/apps/asset/models/__init__.py
================================================
# 导入所有模型,确保Django能发现它们

# 业务模型
from .asset_models import (
    Subdomain,
    WebSite,
    Endpoint,
    Directory,
    HostPortMapping,
    Vulnerability,
)

# 快照模型
from .snapshot_models import (
    SubdomainSnapshot,
    WebsiteSnapshot,
    DirectorySnapshot,
    HostPortMappingSnapshot,
    EndpointSnapshot,
    VulnerabilitySnapshot,
)

# 截图模型
from .screenshot_models import (
    Screenshot,
    ScreenshotSnapshot,
)

# 统计模型
from .statistics_models import AssetStatistics, StatisticsHistory

# 导出所有模型供外部导入
__all__ = [
    # 业务模型
    'Subdomain',
    'WebSite', 
    'Endpoint',
    'Directory',
    'HostPortMapping',
    'Vulnerability',
    # 快照模型
    'SubdomainSnapshot',
    'WebsiteSnapshot', 
    'DirectorySnapshot',
    'HostPortMappingSnapshot',
    'EndpointSnapshot',
    'VulnerabilitySnapshot',
    # 截图模型
    'Screenshot',
    'ScreenshotSnapshot',
    # 统计模型
    'AssetStatistics',
    'StatisticsHistory',
]


================================================
FILE: backend/apps/asset/models/asset_models.py
================================================

from django.db import models
from django.contrib.postgres.fields import ArrayField
from django.contrib.postgres.indexes import GinIndex
from django.core.validators import MinValueValidator, MaxValueValidator


class Subdomain(models.Model):
    """
    子域名模型(纯资产表)
    
    设计特点:
    - 只存储子域名资产信息
    - 与其他资产表(IPAddress、Port)无直接关联
    - 扫描历史记录存储在 SubdomainSnapshot 快照表中
    """

    id = models.AutoField(primary_key=True)
    target = models.ForeignKey(
        'targets.Target',  # 使用字符串引用避免循环导入
        on_delete=models.CASCADE,
        related_name='subdomains',
        help_text='所属的扫描目标(主关联字段,表示所属关系,不能为空)'
    )
    name = models.CharField(max_length=1000, help_text='子域名名称')
    created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间')

    class Meta:
        db_table = 'subdomain'
        verbose_name = '子域名'
        verbose_name_plural = '子域名'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['-created_at']),
            models.Index(fields=['name', 'target']),  # 复合索引,优化 get_by_names_and_target_id 批量查询
            models.Index(fields=['target']),     # 优化从target_id快速查找下面的子域名
            models.Index(fields=['name']),            # 优化从name快速查找子域名,搜索场景
            # pg_trgm GIN 索引,支持 LIKE '%keyword%' 模糊搜索
            GinIndex(
                name='subdomain_name_trgm_idx',
                fields=['name'],
                opclasses=['gin_trgm_ops']
            ),
        ]
        constraints = [
            # 普通唯一约束:name + target 组合唯一
            models.UniqueConstraint(
                fields=['name', 'target'],
                name='unique_subdomain_name_target'
            )
        ]

    def __str__(self):
        return str(self.name or f'Subdomain {self.id}')


class Endpoint(models.Model):
    """端点模型"""

    id = models.AutoField(primary_key=True)
    target = models.ForeignKey(
        'targets.Target',  # 使用字符串引用
        on_delete=models.CASCADE,
        related_name='endpoints',
        help_text='所属的扫描目标(主关联字段,表示所属关系,不能为空)'
    )
    
    url = models.TextField(help_text='最终访问的完整URL')
    host = models.CharField(
        max_length=253,
        blank=True,
        default='',
        help_text='主机名(域名或IP地址)'
    )
    location = models.TextField(
        blank=True,
        default='',
        help_text='重定向地址(HTTP 3xx 响应头 Location)'
    )
    created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间')
    title = models.TextField(
        blank=True,
        default='',
        help_text='网页标题(HTML <title> 标签内容)'
    )
    webserver = models.TextField(
        blank=True,
        default='',
        help_text='服务器类型(HTTP 响应头 Server 值)'
    )
    response_body = models.TextField(
        blank=True,
        default='',
        help_text='HTTP响应体'
    )
    content_type = models.TextField(
        blank=True,
        default='',
        help_text='响应类型(HTTP Content-Type 响应头)'
    )
    tech = ArrayField(
        models.CharField(max_length=100),
        blank=True,
        default=list,
        help_text='技术栈(服务器/框架/语言等)'
    )
    status_code = models.IntegerField(
        null=True,
        blank=True,
        help_text='HTTP状态码'
    )
    content_length = models.IntegerField(
        null=True,
        blank=True,
        help_text='响应体大小(单位字节)'
    )
    vhost = models.BooleanField(
        null=True,
        blank=True,
        help_text='是否支持虚拟主机'
    )
    matched_gf_patterns = ArrayField(
        models.CharField(max_length=100),
        blank=True,
        default=list,
        help_text='匹配的GF模式列表,用于识别敏感端点(如api, debug, config等)'
    )
    response_headers = models.TextField(
        blank=True,
        default='',
        help_text='原始HTTP响应头'
    )

    class Meta:
        db_table = 'endpoint'
        verbose_name = '端点'
        verbose_name_plural = '端点'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['-created_at']),
            models.Index(fields=['target']),       # 优化从 target_id快速查找下面的端点(主关联字段)
            models.Index(fields=['url']),          # URL索引,优化查询性能
            models.Index(fields=['host']),         # host索引,优化根据主机名查询
            models.Index(fields=['status_code']),  # 状态码索引,优化筛选
            models.Index(fields=['title']),        # title索引,优化智能过滤搜索
            GinIndex(fields=['tech']),             # GIN索引,优化 tech 数组字段的 __contains 查询
            # pg_trgm GIN 索引,支持 LIKE '%keyword%' 模糊搜索
            GinIndex(
                name='endpoint_resp_headers_trgm_idx',
                fields=['response_headers'],
                opclasses=['gin_trgm_ops']
            ),
            GinIndex(
                name='endpoint_url_trgm_idx',
                fields=['url'],
                opclasses=['gin_trgm_ops']
            ),
            GinIndex(
                name='endpoint_title_trgm_idx',
                fields=['title'],
                opclasses=['gin_trgm_ops']
            ),
        ]
        constraints = [
            # 普通唯一约束:url + target 组合唯一
            models.UniqueConstraint(
                fields=['url', 'target'],
                name='unique_endpoint_url_target'
            )
        ]

    def __str__(self):
        return str(self.url or f'Endpoint {self.id}')


class WebSite(models.Model):
    """站点模型"""

    id = models.AutoField(primary_key=True)
    target = models.ForeignKey(
        'targets.Target',  # 使用字符串引用
        on_delete=models.CASCADE,
        related_name='websites',
        help_text='所属的扫描目标(主关联字段,表示所属关系,不能为空)'
    )

    url = models.TextField(help_text='最终访问的完整URL')
    host = models.CharField(
        max_length=253,
        blank=True,
        default='',
        help_text='主机名(域名或IP地址)'
    )
    location = models.TextField(
        blank=True,
        default='',
        help_text='重定向地址(HTTP 3xx 响应头 Location)'
    )
    created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间')
    title = models.TextField(
        blank=True,
        default='',
        help_text='网页标题(HTML <title> 标签内容)'
    )
    webserver = models.TextField(
        blank=True,
        default='',
        help_text='服务器类型(HTTP 响应头 Server 值)'
    )
    response_body = models.TextField(
        blank=True,
        default='',
        help_text='HTTP响应体'
    )
    content_type = models.TextField(
        blank=True,
        default='',
        help_text='响应类型(HTTP Content-Type 响应头)'
    )
    tech = ArrayField(
        models.CharField(max_length=100),
        blank=True,
        default=list,
        help_text='技术栈(服务器/框架/语言等)'
    )
    status_code = models.IntegerField(
        null=True,
        blank=True,
        help_text='HTTP状态码'
    )
    content_length = models.IntegerField(
        null=True,
        blank=True,
        help_text='响应体大小(单位字节)'
    )
    vhost = models.BooleanField(
        null=True,
        blank=True,
        help_text='是否支持虚拟主机'
    )
    response_headers = models.TextField(
        blank=True,
        default='',
        help_text='原始HTTP响应头'
    )

    class Meta:
        db_table = 'website'
        verbose_name = '站点'
        verbose_name_plural = '站点'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['-created_at']),
            models.Index(fields=['url']),  # URL索引,优化查询性能
            models.Index(fields=['host']),  # host索引,优化根据主机名查询
            models.Index(fields=['target']),     # 优化从 target_id快速查找下面的站点
            models.Index(fields=['title']),      # title索引,优化智能过滤搜索
            models.Index(fields=['status_code']),  # 状态码索引,优化智能过滤搜索
            GinIndex(fields=['tech']),  # GIN索引,优化 tech 数组字段的 __contains 查询
            # pg_trgm GIN 索引,支持 LIKE '%keyword%' 模糊搜索
            GinIndex(
                name='website_resp_headers_trgm_idx',
                fields=['response_headers'],
                opclasses=['gin_trgm_ops']
            ),
            GinIndex(
                name='website_url_trgm_idx',
                fields=['url'],
                opclasses=['gin_trgm_ops']
            ),
            GinIndex(
                name='website_title_trgm_idx',
                fields=['title'],
                opclasses=['gin_trgm_ops']
            ),
        ]
        constraints = [
            # 普通唯一约束:url + target 组合唯一
            models.UniqueConstraint(
                fields=['url', 'target'],
                name='unique_website_url_target'
            )
        ]

    def __str__(self):
        return str(self.url or f'Website {self.id}')


class Directory(models.Model):
    """
    目录模型
    """

    id = models.AutoField(primary_key=True)
    target = models.ForeignKey(
        'targets.Target',
        on_delete=models.CASCADE,
        related_name='directories',
        help_text='所属的扫描目标'
    )
    
    url = models.CharField(
        null=False,
        blank=False,
        max_length=2000,
        help_text='完整请求 URL'
    )
    status = models.IntegerField(
        null=True,
        blank=True,
        help_text='HTTP 响应状态码'
    )
    content_length = models.BigIntegerField(
        null=True,
        blank=True,
        help_text='响应体字节大小(Content-Length 或实际长度)'
    )
    words = models.IntegerField(
        null=True,
        blank=True,
        help_text='响应体中单词数量(按空格分割)'
    )
    lines = models.IntegerField(
        null=True,
        blank=True,
        help_text='响应体行数(按换行符分割)'
    )
    content_type = models.CharField(
        max_length=200,
        blank=True,
        default='',
        help_text='响应头 Content-Type 值'
    )
    duration = models.BigIntegerField(
        null=True,
        blank=True,
        help_text='请求耗时(单位:纳秒)'
    )
    
    created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间')

    class Meta:
        db_table = 'directory'
        verbose_name = '目录'
        verbose_name_plural = '目录'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['-created_at']),
            models.Index(fields=['target']),     # 优化从target_id快速查找下面的目录
            models.Index(fields=['url']),        # URL索引,优化搜索和唯一约束
            models.Index(fields=['status']),     # 状态码索引,优化筛选
            # pg_trgm GIN 索引,支持 LIKE '%keyword%' 模糊搜索
            GinIndex(
                name='directory_url_trgm_idx',
                fields=['url'],
                opclasses=['gin_trgm_ops']
            ),
        ]
        constraints = [
            # 普通唯一约束:target + url 组合唯一
            models.UniqueConstraint(
                fields=['target', 'url'],
                name='unique_directory_url_target'
            ),
        ]

    def __str__(self):
        return str(self.url or f'Directory {self.id}')


class HostPortMapping(models.Model):
    """
    主机端口映射表
    
    设计特点:
    - 存储主机(host)、IP、端口的三元映射关系
    - 只关联 target_id,不关联其他资产表
    - target + host + ip + port 组成复合唯一约束
    """

    id = models.AutoField(primary_key=True)
    
    # ==================== 关联字段 ====================
    target = models.ForeignKey(
        'targets.Target',
        on_delete=models.CASCADE,
        related_name='host_port_mappings',
        help_text='所属的扫描目标'
    )
    
    # ==================== 核心字段 ====================
    host = models.CharField(
        max_length=1000,
        blank=False,
        help_text='主机名(域名或IP)'
    )
    ip = models.GenericIPAddressField(
        blank=False,
        help_text='IP地址'
    )
    port = models.IntegerField(
        blank=False,
        validators=[
            MinValueValidator(1, message='端口号必须大于等于1'),
            MaxValueValidator(65535, message='端口号必须小于等于65535')
        ],
        help_text='端口号(1-65535)'
    )
    
    # ==================== 时间字段 ====================
    created_at = models.DateTimeField(
        auto_now_add=True,
        help_text='创建时间'
    )

    class Meta:
        db_table = 'host_port_mapping'
        verbose_name = '主机端口映射'
        verbose_name_plural = '主机端口映射'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['target']),           # 优化按目标查询
            models.Index(fields=['host']),             # 优化按主机名查询
            models.Index(fields=['ip']),               # 优化按IP查询
            models.Index(fields=['port']),             # 优化按端口查询
            models.Index(fields=['host', 'ip']),       # 优化组合查询
            models.Index(fields=['-created_at']),   # 优化时间排序
        ]
        constraints = [
            # 复合唯一约束:target + host + ip + port 组合唯一
            models.UniqueConstraint(
                fields=['target', 'host', 'ip', 'port'],
                name='unique_target_host_ip_port'
            ),
        ]

    def __str__(self):
        return f'{self.host} ({self.ip}:{self.port})'


class Vulnerability(models.Model):
    """
    漏洞模型(资产表)
    
    存储发现的漏洞资产,与 Target 关联。
    扫描历史记录存储在 VulnerabilitySnapshot 快照表中。
    """
    
    # 延迟导入避免循环引用
    from apps.common.definitions import VulnSeverity

    id = models.AutoField(primary_key=True)
    target = models.ForeignKey(
        'targets.Target',
        on_delete=models.CASCADE,
        related_name='vulnerabilities',
        help_text='所属的扫描目标'
    )
    
    # ==================== 核心字段 ====================
    url = models.CharField(max_length=2000, help_text='漏洞所在的URL')
    vuln_type = models.CharField(max_length=100, help_text='漏洞类型(如 xss, sqli)')
    severity = models.CharField(
        max_length=20,
        choices=VulnSeverity.choices,
        default=VulnSeverity.UNKNOWN,
        help_text='严重性(未知/信息/低/中/高/危急)'
    )
    source = models.CharField(max_length=50, blank=True, default='', help_text='来源工具(如 dalfox, nuclei, crlfuzz)')
    cvss_score = models.DecimalField(max_digits=3, decimal_places=1, null=True, blank=True, help_text='CVSS 评分(0.0-10.0)')
    description = models.TextField(blank=True, default='', help_text='漏洞描述')
    raw_output = models.JSONField(blank=True, default=dict, help_text='工具原始输出')
    
    # ==================== 时间字段 ====================
    created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间')

    class Meta:
        db_table = 'vulnerability'
        verbose_name = '漏洞'
        verbose_name_plural = '漏洞'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['target']),
            models.Index(fields=['vuln_type']),
            models.Index(fields=['severity']),
            models.Index(fields=['source']),
            models.Index(fields=['url']),          # url索引,优化智能过滤搜索
            models.Index(fields=['-created_at']),
        ]

    def __str__(self):
        return f'{self.vuln_type} - {self.url[:50]}'


================================================
FILE: backend/apps/asset/models/screenshot_models.py
================================================
from django.db import models


class ScreenshotSnapshot(models.Model):
    """
    截图快照
    
    记录:某次扫描中捕获的网站截图
    """

    id = models.AutoField(primary_key=True)
    scan = models.ForeignKey(
        'scan.Scan',
        on_delete=models.CASCADE,
        related_name='screenshot_snapshots',
        help_text='所属的扫描任务'
    )
    url = models.TextField(help_text='截图对应的 URL')
    status_code = models.SmallIntegerField(null=True, blank=True, help_text='HTTP 响应状态码')
    image = models.BinaryField(help_text='截图 WebP 二进制数据(压缩后)')
    created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间')

    class Meta:
        db_table = 'screenshot_snapshot'
        verbose_name = '截图快照'
        verbose_name_plural = '截图快照'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['scan']),
            models.Index(fields=['-created_at']),
        ]
        constraints = [
            models.UniqueConstraint(
                fields=['scan', 'url'],
                name='unique_screenshot_per_scan_snapshot'
            ),
        ]

    def __str__(self):
        return f'{self.url} (Scan #{self.scan_id})'


class Screenshot(models.Model):
    """
    截图资产
    
    存储:目标的最新截图(从快照同步)
    """

    id = models.AutoField(primary_key=True)
    target = models.ForeignKey(
        'targets.Target',
        on_delete=models.CASCADE,
        related_name='screenshots',
        help_text='所属目标'
    )
    url = models.TextField(help_text='截图对应的 URL')
    status_code = models.SmallIntegerField(null=True, blank=True, help_text='HTTP 响应状态码')
    image = models.BinaryField(help_text='截图 WebP 二进制数据(压缩后)')
    created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间')
    updated_at = models.DateTimeField(auto_now=True, help_text='更新时间')

    class Meta:
        db_table = 'screenshot'
        verbose_name = '截图'
        verbose_name_plural = '截图'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['target']),
            models.Index(fields=['-created_at']),
        ]
        constraints = [
            models.UniqueConstraint(
                fields=['target', 'url'],
                name='unique_screenshot_per_target'
            ),
        ]

    def __str__(self):
        return f'{self.url} (Target #{self.target_id})'


================================================
FILE: backend/apps/asset/models/snapshot_models.py
================================================
from django.db import models
from django.contrib.postgres.fields import ArrayField
from django.contrib.postgres.indexes import GinIndex
from django.core.validators import MinValueValidator, MaxValueValidator


class SubdomainSnapshot(models.Model):
    """子域名快照"""

    id = models.AutoField(primary_key=True)
    scan = models.ForeignKey(
        'scan.Scan',
        on_delete=models.CASCADE,
        related_name='subdomain_snapshots',
        help_text='所属的扫描任务'
    )
    
    name = models.CharField(max_length=1000, help_text='子域名名称')
    created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间')
    
    class Meta:
        db_table = 'subdomain_snapshot'
        verbose_name = '子域名快照'
        verbose_name_plural = '子域名快照'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['scan']),
            models.Index(fields=['name']),
            models.Index(fields=['-created_at']),
            # pg_trgm GIN 索引,支持 LIKE '%keyword%' 模糊搜索
            GinIndex(
                name='subdomain_snap_name_trgm',
                fields=['name'],
                opclasses=['gin_trgm_ops']
            ),
        ]
        constraints = [
            # 唯一约束:同一次扫描中,同一个子域名只能记录一次
            models.UniqueConstraint(
                fields=['scan', 'name'],
                name='unique_subdomain_per_scan_snapshot'
            ),
        ]

    def __str__(self):
        return f'{self.name} (Scan #{self.scan_id})'

class WebsiteSnapshot(models.Model):
    """
    网站快照
    
    记录:某次扫描中发现的网站
    """

    id = models.AutoField(primary_key=True)
    scan = models.ForeignKey(
        'scan.Scan',
        on_delete=models.CASCADE,
        related_name='website_snapshots',
        help_text='所属的扫描任务'
    )
    
    # 扫描结果数据
    url = models.TextField(help_text='站点URL')
    host = models.CharField(max_length=253, blank=True, default='', help_text='主机名(域名或IP地址)')
    title = models.TextField(blank=True, default='', help_text='页面标题')
    status_code = models.IntegerField(null=True, blank=True, help_text='HTTP状态码')
    content_length = models.BigIntegerField(null=True, blank=True, help_text='内容长度')
    location = models.TextField(blank=True, default='', help_text='重定向位置')
    webserver = models.TextField(blank=True, default='', help_text='Web服务器')
    content_type = models.TextField(blank=True, default='', help_text='内容类型')
    tech = ArrayField(
        models.CharField(max_length=100),
        blank=True,
        default=list,
        help_text='技术栈'
    )
    response_body = models.TextField(blank=True, default='', help_text='HTTP响应体')
    vhost = models.BooleanField(null=True, blank=True, help_text='虚拟主机标志')
    response_headers = models.TextField(
        blank=True,
        default='',
        help_text='原始HTTP响应头'
    )
    created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间')

    class Meta:
        db_table = 'website_snapshot'
        verbose_name = '网站快照'
        verbose_name_plural = '网站快照'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['scan']),
            models.Index(fields=['url']),
            models.Index(fields=['host']),  # host索引,优化根据主机名查询
            models.Index(fields=['title']),  # title索引,优化标题搜索
            models.Index(fields=['-created_at']),
            GinIndex(fields=['tech']),  # GIN索引,优化数组字段查询
            # pg_trgm GIN 索引,支持 LIKE '%keyword%' 模糊搜索
            GinIndex(
                name='ws_snap_resp_hdr_trgm',
                fields=['response_headers'],
                opclasses=['gin_trgm_ops']
            ),
            GinIndex(
                name='ws_snap_url_trgm',
                fields=['url'],
                opclasses=['gin_trgm_ops']
            ),
            GinIndex(
                name='ws_snap_title_trgm',
                fields=['title'],
                opclasses=['gin_trgm_ops']
            ),
        ]
        constraints = [
            # 唯一约束:同一次扫描中,同一个URL只能记录一次
            models.UniqueConstraint(
                fields=['scan', 'url'],
                name='unique_website_per_scan_snapshot'
            ),
        ]

    def __str__(self):
        return f'{self.url} (Scan #{self.scan_id})'


class DirectorySnapshot(models.Model):
    """
    目录快照
    
    记录:某次扫描中发现的目录
    """

    id = models.AutoField(primary_key=True)
    scan = models.ForeignKey(
        'scan.Scan',
        on_delete=models.CASCADE,
        related_name='directory_snapshots',
        help_text='所属的扫描任务'
    )
    
    # 扫描结果数据
    url = models.CharField(max_length=2000, help_text='目录URL')
    status = models.IntegerField(null=True, blank=True, help_text='HTTP状态码')
    content_length = models.BigIntegerField(null=True, blank=True, help_text='内容长度')
    words = models.IntegerField(null=True, blank=True, help_text='响应体中单词数量(按空格分割)')
    lines = models.IntegerField(null=True, blank=True, help_text='响应体行数(按换行符分割)')
    content_type = models.CharField(max_length=200, blank=True, default='', help_text='响应头 Content-Type 值')
    duration = models.BigIntegerField(null=True, blank=True, help_text='请求耗时(单位:纳秒)')
    created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间')

    class Meta:
        db_table = 'directory_snapshot'
        verbose_name = '目录快照'
        verbose_name_plural = '目录快照'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['scan']),
            models.Index(fields=['url']),
            models.Index(fields=['status']),  # 状态码索引,优化筛选
            models.Index(fields=['content_type']),  # content_type索引,优化内容类型搜索
            models.Index(fields=['-created_at']),
            # pg_trgm GIN 索引,支持 LIKE '%keyword%' 模糊搜索
            GinIndex(
                name='dir_snap_url_trgm',
                fields=['url'],
                opclasses=['gin_trgm_ops']
            ),
        ]
        constraints = [
            # 唯一约束:同一次扫描中,同一个目录URL只能记录一次
            models.UniqueConstraint(
                fields=['scan', 'url'],
                name='unique_directory_per_scan_snapshot'
            ),
        ]

    def __str__(self):
        return f'{self.url} (Scan #{self.scan_id})'


class HostPortMappingSnapshot(models.Model):
    """
    主机端口映射快照表
    
    设计特点:
    - 存储某次扫描中发现的主机(host)、IP、端口的三元映射关系
    - 主关联 scan_id,记录扫描历史
    - scan + host + ip + port 组成复合唯一约束
    """

    id = models.AutoField(primary_key=True)
    
    # ==================== 关联字段 ====================
    scan = models.ForeignKey(
        'scan.Scan',
        on_delete=models.CASCADE,
        related_name='host_port_mapping_snapshots',
        help_text='所属的扫描任务(主关联)'
    )
    
    # ==================== 核心字段 ====================
    host = models.CharField(
        max_length=1000,
        blank=False,
        help_text='主机名(域名或IP)'
    )
    ip = models.GenericIPAddressField(
        blank=False,
        help_text='IP地址'
    )
    port = models.IntegerField(
        blank=False,
        validators=[
            MinValueValidator(1, message='端口号必须大于等于1'),
            MaxValueValidator(65535, message='端口号必须小于等于65535')
        ],
        help_text='端口号(1-65535)'
    )
    
    # ==================== 时间字段 ====================
    created_at = models.DateTimeField(
        auto_now_add=True,
        help_text='创建时间'
    )

    class Meta:
        db_table = 'host_port_mapping_snapshot'
        verbose_name = '主机端口映射快照'
        verbose_name_plural = '主机端口映射快照'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['scan']),             # 优化按扫描查询
            models.Index(fields=['host']),             # 优化按主机名查询
            models.Index(fields=['ip']),               # 优化按IP查询
            models.Index(fields=['port']),             # 优化按端口查询
            models.Index(fields=['host', 'ip']),       # 优化组合查询
            models.Index(fields=['scan', 'host']),     # 优化扫描+主机查询
            models.Index(fields=['-created_at']),   # 优化时间排序
        ]
        constraints = [
            # 复合唯一约束:同一次扫描中,scan + host + ip + port 组合唯一
            models.UniqueConstraint(
                fields=['scan', 'host', 'ip', 'port'],
                name='unique_scan_host_ip_port_snapshot'
            ),
        ]

    def __str__(self):
        return f'{self.host} ({self.ip}:{self.port}) [Scan #{self.scan_id}]'


class EndpointSnapshot(models.Model):
    """
    端点快照
    
    记录:某次扫描中发现的端点
    """

    id = models.AutoField(primary_key=True)
    scan = models.ForeignKey(
        'scan.Scan',
        on_delete=models.CASCADE,
        related_name='endpoint_snapshots',
        help_text='所属的扫描任务'
    )
    
    # 扫描结果数据
    url = models.TextField(help_text='端点URL')
    host = models.CharField(
        max_length=253,
        blank=True,
        default='',
        help_text='主机名(域名或IP地址)'
    )
    title = models.TextField(blank=True, default='', help_text='页面标题')
    status_code = models.IntegerField(null=True, blank=True, help_text='HTTP状态码')
    content_length = models.IntegerField(null=True, blank=True, help_text='内容长度')
    location = models.TextField(blank=True, default='', help_text='重定向位置')
    webserver = models.TextField(blank=True, default='', help_text='Web服务器')
    content_type = models.TextField(blank=True, default='', help_text='内容类型')
    tech = ArrayField(
        models.CharField(max_length=100),
        blank=True,
        default=list,
        help_text='技术栈'
    )
    response_body = models.TextField(blank=True, default='', help_text='HTTP响应体')
    vhost = models.BooleanField(null=True, blank=True, help_text='虚拟主机标志')
    matched_gf_patterns = ArrayField(
        models.CharField(max_length=100),
        blank=True,
        default=list,
        help_text='匹配的GF模式列表'
    )
    response_headers = models.TextField(
        blank=True,
        default='',
        help_text='原始HTTP响应头'
    )
    created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间')

    class Meta:
        db_table = 'endpoint_snapshot'
        verbose_name = '端点快照'
        verbose_name_plural = '端点快照'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['scan']),
            models.Index(fields=['url']),
            models.Index(fields=['host']),  # host索引,优化根据主机名查询
            models.Index(fields=['title']),  # title索引,优化标题搜索
            models.Index(fields=['status_code']),  # 状态码索引,优化筛选
            models.Index(fields=['webserver']),  # webserver索引,优化服务器搜索
            models.Index(fields=['-created_at']),
            GinIndex(fields=['tech']),  # GIN索引,优化数组字段查询
            # pg_trgm GIN 索引,支持 LIKE '%keyword%' 模糊搜索
            GinIndex(
                name='ep_snap_resp_hdr_trgm',
                fields=['response_headers'],
                opclasses=['gin_trgm_ops']
            ),
            GinIndex(
                name='ep_snap_url_trgm',
                fields=['url'],
                opclasses=['gin_trgm_ops']
            ),
            GinIndex(
                name='ep_snap_title_trgm',
                fields=['title'],
                opclasses=['gin_trgm_ops']
            ),
        ]
        constraints = [
            # 唯一约束:同一次扫描中,同一个URL只能记录一次
            models.UniqueConstraint(
                fields=['scan', 'url'],
                name='unique_endpoint_per_scan_snapshot'
            ),
        ]

    def __str__(self):
        return f'{self.url} (Scan #{self.scan_id})'


class VulnerabilitySnapshot(models.Model):
    """
    漏洞快照
    
    记录:某次扫描中发现的漏洞
    """
    
    # 延迟导入避免循环引用
    from apps.common.definitions import VulnSeverity

    id = models.AutoField(primary_key=True)
    scan = models.ForeignKey(
        'scan.Scan',
        on_delete=models.CASCADE,
        related_name='vulnerability_snapshots',
        help_text='所属的扫描任务'
    )
    
    # ==================== 核心字段 ====================
    url = models.CharField(max_length=2000, help_text='漏洞所在的URL')
    vuln_type = models.CharField(max_length=100, help_text='漏洞类型(如 xss, sqli)')
    severity = models.CharField(
        max_length=20,
        choices=VulnSeverity.choices,
        default=VulnSeverity.UNKNOWN,
        help_text='严重性(未知/信息/低/中/高/危急)'
    )
    source = models.CharField(max_length=50, blank=True, default='', help_text='来源工具(如 dalfox, nuclei, crlfuzz)')
    cvss_score = models.DecimalField(max_digits=3, decimal_places=1, null=True, blank=True, help_text='CVSS 评分(0.0-10.0)')
    description = models.TextField(blank=True, default='', help_text='漏洞描述')
    raw_output = models.JSONField(blank=True, default=dict, help_text='工具原始输出')
    
    # ==================== 时间字段 ====================
    created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间')

    class Meta:
        db_table = 'vulnerability_snapshot'
        verbose_name = '漏洞快照'
        verbose_name_plural = '漏洞快照'
        ordering = ['-created_at']
        indexes = [
            models.Index(fields=['scan']),
            models.Index(fields=['url']),  # url索引,优化URL搜索
            models.Index(fields=['vuln_type']),
            models.Index(fields=['severity']),
            models.Index(fields=['source']),
            models.Index(fields=['-created_at']),
        ]

    def __str__(self):
        return f'{self.vuln_type} - {self.url[:50]} (Scan #{self.scan_id})'

================================================
FILE: backend/apps/asset/models/statistics_models.py
================================================
from django.db import models


class AssetStatistics(models.Model):
    """
    资产统计表
    
    存储预聚合的全局统计数据,避免仪表盘实时 COUNT 大表。
    由定时任务(Prefect Flow)定期刷新。
    """

    id = models.AutoField(primary_key=True)
    
    # ==================== 当前统计字段 ====================
    total_targets = models.IntegerField(default=0, help_text='目标总数')
    total_subdomains = models.IntegerField(default=0, help_text='子域名总数')
    total_ips = models.IntegerField(default=0, help_text='IP地址总数')
    total_endpoints = models.IntegerField(default=0, help_text='端点总数')
    total_websites = models.IntegerField(default=0, help_text='网站总数')
    total_vulns = models.IntegerField(default=0, help_text='漏洞总数')
    total_assets = models.IntegerField(default=0, help_text='总资产数(子域名+IP+端点+网站)')
    
    # ==================== 上次统计字段(用于计算趋势)====================
    prev_targets = models.IntegerField(default=0, help_text='上次目标总数')
    prev_subdomains = models.IntegerField(default=0, help_text='上次子域名总数')
    prev_ips = models.IntegerField(default=0, help_text='上次IP地址总数')
    prev_endpoints = models.IntegerField(default=0, help_text='上次端点总数')
    prev_websites = models.IntegerField(default=0, help_text='上次网站总数')
    prev_vulns = models.IntegerField(default=0, help_text='上次漏洞总数')
    prev_assets = models.IntegerField(default=0, help_text='上次总资产数')
    
    # ==================== 时间字段 ====================
    updated_at = models.DateTimeField(auto_now=True, help_text='最后更新时间')

    class Meta:
        db_table = 'asset_statistics'
        verbose_name = '资产统计'
        verbose_name_plural = '资产统计'

    def __str__(self):
        return f'AssetStatistics (updated: {self.updated_at})'

    @classmethod
    def get_or_create_singleton(cls) -> 'AssetStatistics':
        """获取或创建单例统计记录"""
        obj, _ = cls.objects.get_or_create(pk=1)
        return obj


class StatisticsHistory(models.Model):
    """
    统计历史表(用于折线图)
    
    每天记录一条快照,用于展示趋势。
    由定时任务在刷新统计时自动写入。
    """
    
    date = models.DateField(unique=True, help_text='统计日期')
    
    # 各类资产数量
    total_targets = models.IntegerField(default=0, help_text='目标总数')
    total_subdomains = models.IntegerField(default=0, help_text='子域名总数')
    total_ips = models.IntegerField(default=0, help_text='IP地址总数')
    total_endpoints = models.IntegerField(default=0, help_text='端点总数')
    total_websites = models.IntegerField(default=0, help_text='网站总数')
    total_vulns = models.IntegerField(default=0, help_text='漏洞总数')
    total_assets = models.IntegerField(default=0, help_text='总资产数')
    
    created_at = models.DateTimeField(auto_now_add=True, help_text='创建时间')
    updated_at = models.DateTimeField(auto_now=True, help_text='更新时间')
    
    class Meta:
        db_table = 'statistics_history'
        verbose_name = '统计历史'
        verbose_name_plural = '统计历史'
        ordering = ['-date']
        indexes = [
            models.Index(fields=['date']),
        ]
    
    def __str__(self):
        return f'StatisticsHistory ({self.date})'


================================================
FILE: backend/apps/asset/repositories/__init__.py
================================================
"""Asset Repositories - 数据访问层"""

# 资产模块 Repositories
from .asset import (
    DjangoSubdomainRepository,
    DjangoWebSiteRepository,
    DjangoDirectoryRepository,
    DjangoHostPortMappingRepository,
    DjangoEndpointRepository,
)

# 快照模块 Repositories
from .snapshot import (
    DjangoSubdomainSnapshotRepository,
    DjangoHostPortMappingSnapshotRepository,
    DjangoWebsiteSnapshotRepository,
    DjangoDirectorySnapshotRepository,
    DjangoEndpointSnapshotRepository,
)

# 统计模块 Repository
from .statistics_repository import AssetStatisticsRepository

__all__ = [
    # 资产模块
    'DjangoSubdomainRepository',
    'DjangoWebSiteRepository',
    'DjangoDirectoryRepository',
    'DjangoHostPortMappingRepository',
    'DjangoEndpointRepository',
    # 快照模块
    'DjangoSubdomainSnapshotRepository',
    'DjangoHostPortMappingSnapshotRepository',
    'DjangoWebsiteSnapshotRepository',
    'DjangoDirectorySnapshotRepository',
    'DjangoEndpointSnapshotRepository',
    # 统计模块
    'AssetStatisticsRepository',
]




================================================
FILE: backend/apps/asset/repositories/asset/__init__.py
================================================
"""Asset Repositories - 数据访问层"""

from .subdomain_repository import DjangoSubdomainRepository
from .website_repository import DjangoWebSiteRepository
from .directory_repository import DjangoDirectoryRepository
from .host_port_mapping_repository import DjangoHostPortMappingRepository
from .endpoint_repository import DjangoEndpointRepository

__all__ = [
    'DjangoSubdomainRepository',
    'DjangoWebSiteRepository',
    'DjangoDirectoryRepository',
    'DjangoHostPortMappingRepository',
    'DjangoEndpointRepository',
]


================================================
FILE: backend/apps/asset/repositories/asset/directory_repository.py
================================================
"""
Django ORM 实现的 Directory Repository
"""

import logging
from typing import List, Iterator
from django.db import transaction

from apps.asset.models.asset_models import Directory
from apps.asset.dtos import DirectoryDTO
from apps.common.decorators import auto_ensure_db_connection
from apps.common.utils import deduplicate_for_bulk

logger = logging.getLogger(__name__)


@auto_ensure_db_connection
class DjangoDirectoryRepository:
    """Django ORM 实现的 Directory Repository"""

    def bulk_upsert(self, items: List[DirectoryDTO]) -> int:
        """
        批量创建或更新 Directory(upsert)
        
        存在则更新所有字段,不存在则创建。
        使用 Django 原生 update_conflicts。
        
        注意:自动按模型唯一约束去重,保留最后一条记录。
        
        Args:
            items: Directory DTO 列表
            
        Returns:
            int: 处理的记录数
        """
        if not items:
            return 0
        
        try:
            # 自动按模型唯一约束去重
            unique_items = deduplicate_for_bulk(items, Directory)
            
            # 直接从 DTO 字段构建 Model
            directories = [
                Directory(
                    target_id=item.target_id,
                    url=item.url,
                    status=item.status,
                    content_length=item.content_length,
                    words=item.words,
                    lines=item.lines,
                    content_type=item.content_type or '',
                    duration=item.duration
                )
                for item in unique_items
            ]
            
            with transaction.atomic():
                Directory.objects.bulk_create(
                    directories,
                    update_conflicts=True,
                    unique_fields=['target', 'url'],
                    update_fields=[
                        'status', 'content_length', 'words',
                        'lines', 'content_type', 'duration'
                    ],
                    batch_size=1000
                )
            
            logger.debug(f"批量 upsert Directory 成功: {len(unique_items)} 条")
            return len(unique_items)
                
        except Exception as e:
            logger.error(f"批量 upsert Directory 失败: {e}")
            raise

    def bulk_create_ignore_conflicts(self, items: List[DirectoryDTO]) -> int:
        """
        批量创建 Directory(存在即跳过)
        
        与 bulk_upsert 不同,此方法不会更新已存在的记录。
        适用于批量添加场景,只提供 URL,没有其他字段数据。
        
        注意:自动按模型唯一约束去重,保留最后一条记录。
        
        Args:
            items: Directory DTO 列表
            
        Returns:
            int: 处理的记录数
        """
        if not items:
            return 0
        
        try:
            # 自动按模型唯一约束去重
            unique_items = deduplicate_for_bulk(items, Directory)
            
            directories = [
                Directory(
                    target_id=item.target_id,
                    url=item.url,
                    status=item.status,
                    content_length=item.content_length,
                    words=item.words,
                    lines=item.lines,
                    content_type=item.content_type or '',
                    duration=item.duration
                )
                for item in unique_items
            ]
            
            with transaction.atomic():
                Directory.objects.bulk_create(
                    directories,
                    ignore_conflicts=True,
                    batch_size=1000
                )
            
            logger.debug(f"批量创建 Directory 成功(ignore_conflicts): {len(unique_items)} 条")
            return len(unique_items)
                
        except Exception as e:
            logger.error(f"批量创建 Directory 失败: {e}")
            raise

    def count_by_target(self, target_id: int) -> int:
        """统计目标下的目录总数"""
        return Directory.objects.filter(target_id=target_id).count()

    def get_all(self):
        """获取所有目录"""
        return Directory.objects.all().order_by('-created_at')

    def get_by_target(self, target_id: int):
        """获取目标下的所有目录"""
        return Directory.objects.filter(target_id=target_id).order_by('-created_at')

    def get_urls_for_export(self, target_id: int, batch_size: int = 1000) -> Iterator[str]:
        """流式导出目标下的所有目录 URL"""
        try:
            queryset = (
                Directory.objects
                .filter(target_id=target_id)
                .values_list('url', flat=True)
                .order_by('url')
                .iterator(chunk_size=batch_size)
            )
            for url in queryset:
                yield url
        except Exception as e:
            logger.error("流式导出目录 URL 失败 - Target ID: %s, 错误: %s", target_id, e)
            raise

    def iter_raw_data_for_export(
        self, 
        target_id: int,
        batch_size: int = 1000
    ) -> Iterator[dict]:
        """
        流式获取原始数据用于 CSV 导出
        
        Args:
            target_id: 目标 ID
            batch_size: 每批数据量
        
        Yields:
            包含所有目录字段的字典
        """
        qs = (
            Directory.objects
            .filter(target_id=target_id)
            .values(
                'url', 'status', 'content_length', 'words',
                'lines', 'content_type', 'duration', 'created_at'
            )
            .order_by('url')
        )
        
        for row in qs.iterator(chunk_size=batch_size):
            yield row


================================================
FILE: backend/apps/asset/repositories/asset/endpoint_repository.py
================================================
"""Endpoint Repository - Django ORM 实现"""

import logging
from typing import List, Iterator

from apps.asset.models import Endpoint
from apps.asset.dtos.asset import EndpointDTO
from apps.common.decorators import auto_ensure_db_connection
from apps.common.utils import deduplicate_for_bulk
from django.db import transaction

logger = logging.getLogger(__name__)


@auto_ensure_db_connection
class DjangoEndpointRepository:
    """端点 Repository - 负责端点表的数据访问"""
    
    def bulk_upsert(self, items: List[EndpointDTO]) -> int:
        """
        批量创建或更新端点(upsert)
        
        存在则更新所有字段,不存在则创建。
        使用 Django 原生 update_conflicts。
        
        注意:自动按模型唯一约束去重,保留最后一条记录。
        
        Args:
            items: 端点 DTO 列表
            
        Returns:
            int: 处理的记录数
        """
        if not items:
            return 0
        
        try:
            # 自动按模型唯一约束去重
            unique_items = deduplicate_for_bulk(items, Endpoint)
            
            # 直接从 DTO 字段构建 Model
            endpoints = [
                Endpoint(
                    target_id=item.target_id,
                    url=item.url,
                    host=item.host or '',
                    title=item.title or '',
                    status_code=item.status_code,
                    content_length=item.content_length,
                    webserver=item.webserver or '',
                    response_body=item.response_body or '',
                    content_type=item.content_type or '',
                    tech=item.tech if item.tech else [],
                    vhost=item.vhost,
                    location=item.location or '',
                    matched_gf_patterns=item.matched_gf_patterns if item.matched_gf_patterns else [],
                    response_headers=item.response_headers if item.response_headers else ''
                )
                for item in unique_items
            ]
            
            with transaction.atomic():
                Endpoint.objects.bulk_create(
                    endpoints,
                    update_conflicts=True,
                    unique_fields=['url', 'target'],
                    update_fields=[
                        'host', 'title', 'status_code', 'content_length',
                        'webserver', 'response_body', 'content_type', 'tech',
                        'vhost', 'location', 'matched_gf_patterns', 'response_headers'
                    ],
                    batch_size=1000
                )
            
            logger.debug(f"批量 upsert 端点成功: {len(unique_items)} 条")
            return len(unique_items)
                
        except Exception as e:
            logger.error(f"批量 upsert 端点失败: {e}")
            raise
    
    def get_all(self):
        """获取所有端点(全局查询)"""
        return Endpoint.objects.all().order_by('-created_at')
    
    def get_by_target(self, target_id: int):
        """
        获取目标下的所有端点
        
        Args:
            target_id: 目标 ID
            
        Returns:
            QuerySet: 端点查询集
        """
        return Endpoint.objects.filter(target_id=target_id).order_by('-created_at')
    
    def count_by_target(self, target_id: int) -> int:
        """
        统计目标下的端点数量
        
        Args:
            target_id: 目标 ID
            
        Returns:
            int: 端点数量
        """
        return Endpoint.objects.filter(target_id=target_id).count()

    def bulk_create_ignore_conflicts(self, items: List[EndpointDTO]) -> int:
        """
        批量创建端点(存在即跳过)
        
        与 bulk_upsert 不同,此方法不会更新已存在的记录。
        适用于快速扫描场景,只提供 URL,没有其他字段数据。
        
        注意:自动按模型唯一约束去重,保留最后一条记录。
        
        Args:
            items: 端点 DTO 列表
            
        Returns:
            int: 处理的记录数
        """
        if not items:
            return 0
        
        try:
            # 自动按模型唯一约束去重
            unique_items = deduplicate_for_bulk(items, Endpoint)
            
            # 直接从 DTO 字段构建 Model
            endpoints = [
                Endpoint(
                    target_id=item.target_id,
                    url=item.url,
                    host=item.host or '',
                    title=item.title or '',
                    status_code=item.status_code,
                    content_length=item.content_length,
                    webserver=item.webserver or '',
                    response_body=item.response_body or '',
                    content_type=item.content_type or '',
                    tech=item.tech if item.tech else [],
                    vhost=item.vhost,
                    location=item.location or '',
                    matched_gf_patterns=item.matched_gf_patterns if item.matched_gf_patterns else [],
                    response_headers=item.response_headers if item.response_headers else ''
                )
                for item in unique_items
            ]
            
            with transaction.atomic():
                Endpoint.objects.bulk_create(
                    endpoints,
                    ignore_conflicts=True,
                    batch_size=1000
                )
            
            logger.debug(f"批量创建端点成功(ignore_conflicts): {len(unique_items)} 条")
            return len(unique_items)
                
        except Exception as e:
            logger.error(f"批量创建端点失败: {e}")
            raise

    def iter_raw_data_for_export(
        self, 
        target_id: int,
        batch_size: int = 1000
    ) -> Iterator[dict]:
        """
        流式获取原始数据用于 CSV 导出
        
        Args:
            target_id: 目标 ID
            batch_size: 每批数据量
        
        Yields:
            包含所有端点字段的字典
        """
        qs = (
            Endpoint.objects
            .filter(target_id=target_id)
            .values(
                'url', 'host', 'location', 'title', 'status_code',
                'content_length', 'content_type', 'webserver', 'tech',
                'response_body', 'response_headers', 'vhost', 'matched_gf_patterns', 'created_at'
            )
            .order_by('url')
        )
        
        for row in qs.iterator(chunk_size=batch_size):
            yield row


================================================
FILE: backend/apps/asset/repositories/asset/host_port_mapping_repository.py
================================================
"""HostPortMapping Repository - Django ORM 实现"""

import logging
from typing import List, Iterator, Dict, Optional

from django.db.models import QuerySet, Min

from apps.asset.models.asset_models import HostPortMapping
from apps.asset.dtos.asset import HostPortMappingDTO
from apps.common.decorators import auto_ensure_db_connection
from apps.common.utils import deduplicate_for_bulk

logger = logging.getLogger(__name__)


@auto_ensure_db_connection
class DjangoHostPortMappingRepository:
    """HostPortMapping Repository - Django ORM 实现
    
    职责:纯数据访问,不包含业务逻辑
    """

    def bulk_create_ignore_conflicts(self, items: List[HostPortMappingDTO]) -> int:
        """
        批量创建主机端口关联(忽略冲突)
        
        注意:自动按模型唯一约束去重,保留最后一条记录。
        
        Args:
            items: 主机端口关联 DTO 列表
        
        Returns:
            int: 实际创建的记录数
        """
        try:
            logger.debug("准备批量创建主机端口关联 - 数量: %d", len(items))
            
            if not items:
                logger.debug("主机端口关联为空,跳过创建")
                return 0
            
            # 自动按模型唯一约束去重
            unique_items = deduplicate_for_bulk(items, HostPortMapping)
                
            records = [
                HostPortMapping(
                    target_id=item.target_id,
                    host=item.host,
                    ip=item.ip,
                    port=item.port
                )
                for item in unique_items
            ]
            
            created = HostPortMapping.objects.bulk_create(
                records, 
                ignore_conflicts=True
            )
            
            created_count = len(created) if created else 0
            logger.debug("主机端口关联创建完成 - 数量: %d", created_count)
            
            return created_count
            
        except Exception as e:
            logger.error(
                "批量创建主机端口关联失败 - 数量: %d, 错误: %s",
                len(items),
                str(e),
                exc_info=True
            )
            raise

    def get_for_export(self, target_id: int, batch_size: int = 1000):
        queryset = (
            HostPortMapping.objects
            .filter(target_id=target_id)
            .order_by("host", "port")
            .values("host", "port")
            .iterator(chunk_size=batch_size)
        )
        for item in queryset:
            yield item

    def get_ips_for_export(self, target_id: int, batch_size: int = 1000) -> Iterator[str]:
        """流式导出目标下的所有唯一 IP 地址。"""
        queryset = (
            HostPortMapping.objects
            .filter(target_id=target_id)
            .values_list("ip", flat=True)
            .distinct()
            .order_by("ip")
            .iterator(chunk_size=batch_size)
        )
        for ip in queryset:
            yield ip

    def get_queryset_by_target(self, target_id: int) -> QuerySet:
        """获取目标下的 QuerySet"""
        return HostPortMapping.objects.filter(target_id=target_id)

    def get_all_queryset(self) -> QuerySet:
        """获取所有记录的 QuerySet"""
        return HostPortMapping.objects.all()

    def get_queryset_by_ip(self, ip: str, target_id: Optional[int] = None) -> QuerySet:
        """获取指定 IP 的 QuerySet"""
        qs = HostPortMapping.objects.filter(ip=ip)
        if target_id:
            qs = qs.filter(target_id=target_id)
        return qs

    def iter_raw_data_for_export(
        self, 
        target_id: int,
        batch_size: int = 1000
    ) -> Iterator[dict]:
        """
        流式获取原始数据用于 CSV 导出
        
        Args:
            target_id: 目标 ID
            batch_size: 每批数据量
        
        Yields:
            {
                'ip': '192.168.1.1',
                'host': 'example.com',
                'port': 80,
                'created_at': datetime
            }
        """
        qs = (
            HostPortMapping.objects
            .filter(target_id=target_id)
            .values('ip', 'host', 'port', 'created_at')
            .order_by('ip', 'host', 'port')
        )
        
        for row in qs.iterator(chunk_size=batch_size):
            yield row


================================================
FILE: backend/apps/asset/repositories/asset/subdomain_repository.py
================================================
"""Subdomain Repository - Django ORM 实现"""

import logging
from typing import List, Iterator

from django.db import transaction

from apps.asset.models.asset_models import Subdomain
from apps.asset.dtos import SubdomainDTO
from apps.common.decorators import auto_ensure_db_connection
from apps.common.utils import deduplicate_for_bulk

logger = logging.getLogger(__name__)


@auto_ensure_db_connection
class DjangoSubdomainRepository:
    """基于 Django ORM 的子域名仓储实现"""

    def bulk_create_ignore_conflicts(self, items: List[SubdomainDTO]) -> None:
        """
        批量创建子域名,忽略冲突
        
        注意:自动按模型唯一约束去重,保留最后一条记录。
        
        Args:
            items: 子域名 DTO 列表
        """
        if not items:
            return

        try:
            # 自动按模型唯一约束去重
            unique_items = deduplicate_for_bulk(items, Subdomain)
            
            subdomain_objects = [
                Subdomain(
                    name=item.name,
                    target_id=item.target_id,
                )
                for item in unique_items
            ]

            with transaction.atomic():
                Subdomain.objects.bulk_create(
                    subdomain_objects,
                    ignore_conflicts=True,
                )

            logger.debug(f"成功处理 {len(unique_items)} 条子域名记录")

        except Exception as e:
            logger.error(f"批量插入子域名失败: {e}")
            raise
    
    def get_all(self):
        """获取所有子域名"""
        return Subdomain.objects.all().order_by('-created_at')

    def get_by_target(self, target_id: int):
        """获取目标下的所有子域名"""
        return Subdomain.objects.filter(target_id=target_id).order_by('-created_at')
    
    def count_by_target(self, target_id: int) -> int:
        """统计目标下的域名数量"""
        return Subdomain.objects.filter(target_id=target_id).count()
    
    def get_domains_for_export(self, target_id: int, batch_size: int = 1000) -> Iterator[str]:
        """流式导出域名"""
        queryset = Subdomain.objects.filter(
            target_id=target_id
        ).only('name').iterator(chunk_size=batch_size)
        
        for subdomain in queryset:
            yield subdomain.name
    
    def get_by_names_and_target_id(self, names: set, target_id: int) -> dict:
        """根据域名列表和目标ID批量查询 Subdomain"""
        subdomains = Subdomain.objects.filter(
            name__in=names,
            target_id=target_id
        ).only('id', 'name')
        
        return {sd.name: sd for sd in subdomains}

    def iter_raw_data_for_export(
        self, 
        target_id: int,
        batch_size: int = 1000
    ) -> Iterator[dict]:
        """
        流式获取原始数据用于 CSV 导出
        
        Args:
            target_id: 目标 ID
            batch_size: 每批数据量
        
        Yields:
            {'name': 'sub.example.com', 'created_at': datetime}
        """
        qs = (
            Subdomain.objects
            .filter(target_id=target_id)
            .values('name', 'created_at')
            .order_by('name')
        )
        
        for row in qs.iterator(chunk_size=batch_size):
            yield row


================================================
FILE: backend/apps/asset/repositories/asset/website_repository.py
================================================
"""
Django ORM 实现的 WebSite Repository
"""

import logging
from typing import List, Generator, Optional, Iterator
from django.db import transaction

from apps.asset.models.asset_models import WebSite
from apps.asset.dtos import WebSiteDTO
from apps.common.decorators import auto_ensure_db_connection
from apps.common.utils import deduplicate_for_bulk

logger = logging.getLogger(__name__)


@auto_ensure_db_connection
class DjangoWebSiteRepository:
    """Django ORM 实现的 WebSite Repository"""

    def bulk_upsert(self, items: List[WebSiteDTO]) -> int:
        """
        批量创建或更新 WebSite(upsert)
        
        存在则更新所有字段,不存在则创建。
        使用 Django 原生 update_conflicts。
        
        注意:自动按模型唯一约束去重,保留最后一条记录。
        
        Args:
            items: WebSite DTO 列表
            
        Returns:
            int: 处理的记录数
        """
        if not items:
            return 0
        
        try:
            # 自动按模型唯一约束去重
            unique_items = deduplicate_for_bulk(items, WebSite)
            
            # 直接从 DTO 字段构建 Model
            websites = [
                WebSite(
                    target_id=item.target_id,
                    url=item.url,
                    host=item.host or '',
                    location=item.location or '',
                    title=item.title or '',
                    webserver=item.webserver or '',
                    response_body=item.response_body or '',
                    content_type=item.content_type or '',
                    tech=item.tech if item.tech else [],
                    status_code=item.status_code,
                    content_length=item.content_length,
                    vhost=item.vhost,
                    response_headers=item.response_headers if item.response_headers else ''
                )
                for item in unique_items
            ]
            
            with transaction.atomic():
                WebSite.objects.bulk_create(
                    websites,
                    update_conflicts=True,
                    unique_fields=['url', 'target'],
                    update_fields=[
                        'host', 'location', 'title', 'webserver',
                        'response_body', 'content_type', 'tech',
                        'status_code', 'content_length', 'vhost', 'response_headers'
                    ],
                    batch_size=1000
                )
            
            logger.debug(f"批量 upsert WebSite 成功: {len(unique_items)} 条")
            return len(unique_items)
                
        except Exception as e:
            logger.error(f"批量 upsert WebSite 失败: {e}")
            raise

    def get_urls_for_export(self, target_id: int, batch_size: int = 1000) -> Generator[str, None, None]:
        """
        流式导出目标下的所有站点 URL
        """
        try:
            queryset = WebSite.objects.filter(
                target_id=target_id
            ).values_list('url', flat=True).iterator(chunk_size=batch_size)
            
            for url in queryset:
                yield url
        except Exception as e:
            logger.error(f"流式导出站点 URL 失败 - Target ID: {target_id}, 错误: {e}")
            raise

    def get_all(self):
        """获取所有网站"""
        return WebSite.objects.all().order_by('-created_at')

    def get_by_target(self, target_id: int):
        """获取目标下的所有网站"""
        return WebSite.objects.filter(target_id=target_id).order_by('-created_at')

    def count_by_target(self, target_id: int) -> int:
        """统计目标下的站点总数"""
        return WebSite.objects.filter(target_id=target_id).count()

    def get_by_url(self, url: str, target_id: int) -> Optional[int]:
        """根据 URL 和 target_id 查找站点 ID"""
        website = WebSite.objects.filter(url=url, target_id=target_id).first()
        return website.id if website else None

    def bulk_create_ignore_conflicts(self, items: List[WebSiteDTO]) -> int:
        """
        批量创建 WebSite(存在即跳过)
        
        注意:自动按模型唯一约束去重,保留最后一条记录。
        """
        if not items:
            return 0
        
        try:
            # 自动按模型唯一约束去重
            unique_items = deduplicate_for_bulk(items, WebSite)
            
            websites = [
                WebSite(
                    target_id=item.target_id,
                    url=item.url,
                    host=item.host or '',
                    location=item.location or '',
                    title=item.title or '',
                    webserver=item.webserver or '',
                    response_body=item.response_body or '',
                    content_type=item.content_type or '',
                    tech=item.tech if item.tech else [],
                    status_code=item.status_code,
                    content_length=item.content_length,
                    vhost=item.vhost,
                    response_headers=item.response_headers if item.response_headers else ''
                )
                for item in unique_items
            ]
            
            with transaction.atomic():
                WebSite.objects.bulk_create(
                    websites,
                    ignore_conflicts=True,
                    batch_size=1000
                )
            
            logger.debug(f"批量创建 WebSite 成功(ignore_conflicts): {len(unique_items)} 条")
            return len(unique_items)
                
        except Exception as e:
            logger.error(f"批量创建 WebSite 失败: {e}")
            raise

    def iter_raw_data_for_export(
        self, 
        target_id: int,
        batch_size: int = 1000
    ) -> Iterator[dict]:
        """
        流式获取原始数据用于 CSV 导出
        
        Args:
            target_id: 目标 ID
            batch_size: 每批数据量
        
        Yields:
            包含所有网站字段的字典
        """
        qs = (
            WebSite.objects
            .filter(target_id=target_id)
            .values(
                'url', 'host', 'location', 'title', 'status_code',
                'content_length', 'content_type', 'webserver', 'tech',
                'response_body', 'response_headers', 'vhost', 'created_at'
            )
            .order_by('url')
        )
        
        for row in qs.iterator(chunk_size=batch_size):
            yield row


================================================
FILE: backend/apps/asset/repositories/snapshot/__init__.py
================================================
"""Snapshot Repositories - 数据访问层"""

from .subdomain_snapshot_repository import DjangoSubdomainSnapshotRepository
from .host_port_mapping_snapshot_repository import DjangoHostPortMappingSnapshotRepository
from .website_snapshot_repository import DjangoWebsiteSnapshotRepository
from .directory_snapshot_repository import DjangoDirectorySnapshotRepository
from .endpoint_snapshot_repository import DjangoEndpointSnapshotRepository
from .vulnerability_snapshot_repository import DjangoVulnerabilitySnapshotRepository

__all__ = [
    'DjangoSubdomainSnapshotRepository',
    'DjangoHostPortMappingSnapshotRepository', 
    'DjangoWebsiteSnapshotRepository',
    'DjangoDirectorySnapshotRepository',
    'DjangoEndpointSnapshotRepository',
    'DjangoVulnerabilitySnapshotRepository',
]


================================================
FILE: backend/apps/asset/repositories/snapshot/directory_snapshot_repository.py
================================================
"""Directory Snapshot Repository - 目录快照数据访问层"""

import logging
from typing import List, Iterator
from django.db import transaction

from apps.asset.models import DirectorySnapshot
from apps.asset.dtos.snapshot import DirectorySnapshotDTO
from apps.common.decorators import auto_ensure_db_connection
from apps.common.utils import deduplicate_for_bulk

logger = logging.getLogger(__name__)


@auto_ensure_db_connection
class DjangoDirectorySnapshotRepository:
    """
    目录快照仓储(Django ORM 实现)
    
    负责目录快照表的数据访问操作
    """
    
    def save_snapshots(self, items: List[DirectorySnapshotDTO]) -> None:
        """
        批量保存目录快照记录
        
        使用 ignore_conflicts 策略,如果快照已存在(相同 scan + url)则跳过
        
        注意:会自动按 (scan_id, url) 去重,保留最后一条记录。
        
        Args:
            items: 目录快照 DTO 列表
        
        Raises:
            ValueError: items 为空
            Exception: 数据库操作失败
        """
        if not items:
            logger.warning("目录快照列表为空,跳过保存")
            return
        
        try:
            # 根据模型唯一约束自动去重
            unique_items = deduplicate_for_bulk(items, DirectorySnapshot)
            
            # 转换为 Django 模型对象
            snapshot_objects = [
                DirectorySnapshot(
                    scan_id=item.scan_id,
                    url=item.url,
                    status=item.status,
                    content_length=item.content_length,
                    words=item.words,
                    lines=item.lines,
                    content_type=item.content_type,
                    duration=item.duration
                )
                for item in unique_items
            ]
            
            with transaction.atomic():
                # 批量插入,忽略冲突
                # 如果 scan + url 已存在,跳过
                DirectorySnapshot.objects.bulk_create(
                    snapshot_objects,
                    ignore_conflicts=True
                )
            
            logger.debug("成功保存 %d 条目录快照记录", len(unique_items))
            
        except Exception as e:
            logger.error(
                "批量保存目录快照失败 - 数量: %d, 错误: %s",
                len(items),
                str(e),
                exc_info=True
            )
            raise
    
    def get_by_scan(self, scan_id: int):
        return DirectorySnapshot.objects.filter(scan_id=scan_id).order_by('-created_at')

    def get_all(self):
        return DirectorySnapshot.objects.all().order_by('-created_at')

    def iter_raw_data_for_export(
        self, 
        scan_id: int,
        batch_size: int = 1000
    ) -> Iterator[dict]:
        """
        流式获取原始数据用于 CSV 导出
        
        Args:
            scan_id: 扫描 ID
            batch_size: 每批数据量
        
        Yields:
            包含所有目录字段的字典
        """
        qs = (
            DirectorySnapshot.objects
            .filter(scan_id=scan_id)
            .values(
                'url', 'status', 'content_length', 'words',
                'lines', 'content_type', 'duration', 'created_at'
            )
            .order_by('url')
        )
        
        for row in qs.iterator(chunk_size=batch_size):
            yield row


================================================
FILE: backend/apps/asset/repositories/snapshot/endpoint_snapshot_repository.py
================================================
"""EndpointSnapshot Repository - Django ORM 实现"""

import logging
from typing import List, Iterator

from apps.asset.models.snapshot_models import EndpointSnapshot
from apps.asset.dtos.snapshot import EndpointSnapshotDTO
from apps.common.decorators import auto_ensure_db_connection
from apps.common.utils import deduplicate_for_bulk

logger = logging.getLogger(__name__)


@auto_ensure_db_connection
class DjangoEndpointSnapshotRepository:
    """端点快照 Repository - 负责端点快照表的数据访问"""

    def save_snapshots(self, items: List[EndpointSnapshotDTO]) -> None:
        """
        保存端点快照
        
        注意:会自动按 (scan_id, url) 去重,保留最后一条记录。
        
        Args:
            items: 端点快照 DTO 列表
        
        Note:
            - 保存完整的快照数据
            - 基于唯一约束 (scan + url) 自动去重
        """
        try:
            logger.debug("准备保存端点快照 - 数量: %d", len(items))
            
            if not items:
                logger.debug("端点快照为空,跳过保存")
                return
            
            # 根据模型唯一约束自动去重
            unique_items = deduplicate_for_bulk(items, EndpointSnapshot)
                
            # 构建快照对象
            snapshots = []
            for item in unique_items:
                snapshots.append(EndpointSnapshot(
                    scan_id=item.scan_id,
                    url=item.url,
                    host=item.host if item.host else '',
                    title=item.title,
                    status_code=item.status_code,
                    content_length=item.content_length,
                    location=item.location,
                    webserver=item.webserver,
                    content_type=item.content_type,
                    tech=item.tech if item.tech else [],
                    response_body=item.response_body,
                    vhost=item.vhost,
                    matched_gf_patterns=item.matched_gf_patterns if item.matched_gf_patterns else [],
                    response_headers=item.response_headers if item.response_headers else ''
                ))
            
            # 批量创建(忽略冲突,基于唯一约束去重)
            EndpointSnapshot.objects.bulk_create(
                snapshots, 
                ignore_conflicts=True
            )
            
            logger.debug("端点快照保存成功 - 数量: %d", len(snapshots))
            
        except Exception as e:
            logger.error(
                "保存端点快照失败 - 数量: %d, 错误: %s",
                len(items),
                str(e),
                exc_info=True
            )
            raise
    
    def get_by_scan(self, scan_id: int):
        return EndpointSnapshot.objects.filter(scan_id=scan_id).order_by('-created_at')

    def get_all(self):
        return EndpointSnapshot.objects.all().order_by('-created_at')

    def iter_raw_data_for_export(
        self, 
        scan_id: int,
        batch_size: int = 1000
    ) -> Iterator[dict]:
        """
        流式获取原始数据用于 CSV 导出
        
        Args:
            scan_id: 扫描 ID
            batch_size: 每批数据量
        
        Yields:
            包含所有端点字段的字典
        """
        qs = (
            EndpointSnapshot.objects
            .filter(scan_id=scan_id)
            .values(
                'url', 'host', 'location', 'title', 'status_code',
                'content_length', 'content_type', 'webserver', 'tech',
                'response_body', 'response_headers', 'vhost', 'matched_gf_patterns', 'created_at'
            )
            .order_by('url')
        )
        
        for row in qs.iterator(chunk_size=batch_size):
            yield row


================================================
FILE: backend/apps/asset/repositories/snapshot/host_port_mapping_snapshot_repository.py
================================================
"""HostPortMappingSnapshot Repository - Django ORM 实现"""

import logging
from typing import List, Iterator

from apps.asset.models.snapshot_models import HostPortMappingSnapshot
from apps.asset.dtos.snapshot import HostPortMappingSnapshotDTO
from apps.common.decorators import auto_ensure_db_connection
from apps.common.utils import deduplicate_for_bulk

logger = logging.getLogger(__name__)


@auto_ensure_db_connection
class DjangoHostPortMappingSnapshotRepository:
    """HostPortMappingSnapshot Repository - Django ORM 实现,负责主机端口映射快照表的数据访问"""

    def save_snapshots(self, items: List[HostPortMappingSnapshotDTO]) -> None:
        """
        保存主机端口关联快照
        
        注意:会自动按 (scan_id, host, ip, port) 去重,保留最后一条记录。
        
        Args:
            items: 主机端口关联快照 DTO 列表
        
        Note:
            - 保存完整的快照数据
            - 基于唯一约束 (scan + host + ip + port) 自动去重
        """
        try:
            logger.debug("准备保存主机端口关联快照 - 数量: %d", len(items))
            
            if not items:
                logger.debug("主机端口关联快照为空,跳过保存")
                return
            
            # 根据模型唯一约束自动去重
            unique_items = deduplicate_for_bulk(items, HostPortMappingSnapshot)
                
            # 构建快照对象
            snapshots = []
            for item in unique_items:
                snapshots.append(HostPortMappingSnapshot(
                    scan_id=item.scan_id,
                    host=item.host,
                    ip=item.ip,
                    port=item.port
                ))
            
            # 批量创建(忽略冲突,基于唯一约束去重)
            HostPortMappingSnapshot.objects.bulk_create(
                snapshots, 
                ignore_conflicts=True
            )
            
            logger.debug("主机端口关联快照保存成功 - 数量: %d", len(snapshots))
            
        except Exception as e:
            logger.error(
                "保存主机端口关联快照失败 - 数量: %d, 错误: %s",
                len(items),
                str(e),
                exc_info=True
            )
            raise
    
    def get_ip_aggregation_by_scan(self, scan_id: int, filter_query: str = None):
        from django.db.models import Min
        from apps.common.utils.filter_utils import apply_filters

        qs = HostPortMappingSnapshot.objects.filter(scan_id=scan_id)
        
        # 应用智能过滤
        if filter_query:
            field_mapping = {
                'ip': 'ip',
                'port': 'port',
                'host': 'host',
            }
            qs = apply_filters(qs, filter_query, field_mapping)

        ip_aggregated = (
            qs
            .values('ip')
            .annotate(
                created_at=Min('created_at')
            )
            .order_by('-created_at')
        )

        results = []
        for item in ip_aggregated:
            ip = item['ip']
            mappings = (
                HostPortMappingSnapshot.objects
                .filter(scan_id=scan_id, ip=ip)
                .values('host', 'port')
                .distinct()
            )

            hosts = sorted({m['host'] for m in mappings})
            ports = sorted({m['port'] for m in mappings})

            results.append({
                'ip': ip,
                'hosts': hosts,
                'ports': ports,
                'created_at': item['created_at'],
            })

        return results

    def get_all_ip_aggregation(self, filter_query: str = None):
        """获取所有 IP 聚合数据"""
        from django.db.models import Min
        from apps.common.utils.filter_utils import apply_filters

        qs = HostPortMappingSnapshot.objects.all()
        
        # 应用智能过滤
        if filter_query:
            field_mapping = {
                'ip': 'ip',
                'port': 'port',
                'host': 'host',
            }
            qs = apply_filters(qs, filter_query, field_mapping)

        ip_aggregated = (
            qs
            .values('ip')
            .annotate(created_at=Min('created_at'))
            .order_by('-created_at')
        )

        results = []
        for item in ip_aggregated:
            ip = item['ip']
            mappings = (
                HostPortMappingSnapshot.objects
                .filter(ip=ip)
                .values('host', 'port')
                .distinct()
            )
            hosts = sorted({m['host'] for m in mappings})
            ports = sorted({m['port'] for m in mappings})
            results.append({
                'ip': ip,
                'hosts': hosts,
                'ports': ports,
                'created_at': item['created_at'],
            })
        return results

    def get_ips_for_export(self, scan_id: int, batch_size: int = 1000) -> Iterator[str]:
        """流式导出扫描下的所有唯一 IP 地址。"""
        queryset = (
            HostPortMappingSnapshot.objects
            .filter(scan_id=scan_id)
            .values_list("ip", flat=True)
            .distinct()
            .order_by("ip")
            .iterator(chunk_size=batch_size)
        )
        for ip in queryset:
            yield ip

    def iter_raw_data_for_export(
        self, 
        scan_id: int,
        batch_size: int = 1000
    ) -> Iterator[dict]:
        """
        流式获取原始数据用于 CSV 导出
        
        Args:
            scan_id: 扫描 ID
            batch_size: 每批数据量
        
        Yields:
            {
                'ip': '192.168.1.1',
                'host': 'example.com',
                'port': 80,
                'created_at': datetime
            }
        """
        qs = (
            HostPortMappingSnapshot.objects
            .filter(scan_id=scan_id)
            .values('ip', 'host', 'port', 'created_at')
            .order_by('ip', 'host', 'port')
        )
        
        for row in qs.iterator(chunk_size=batch_size):
            yield row


================================================
FILE: backend/apps/asset/repositories/snapshot/subdomain_snapshot_repository.py
================================================
"""Django ORM 实现的 SubdomainSnapshot Repository"""

import logging
from typing import List, Iterator

from apps.asset.models.snapshot_models import SubdomainSnapshot
from apps.asset.dtos import SubdomainSnapshotDTO
from apps.common.decorators import auto_ensure_db_connection
from apps.common.utils import deduplicate_for_bulk

logger = logging.getLogger(__name__)


@auto_ensure_db_connection
class DjangoSubdomainSnapshotRepository:
    """子域名快照 Repository - 负责子域名快照表的数据访问"""

    def save_subdomain_snapshots(self, items: List[SubdomainSnapshotDTO]) -> None:
        """
        保存子域名快照
        
        注意:会自动按 (scan_id, name) 去重,保留最后一条记录。
        
        Args:
            items: 子域名快照 DTO 列表
        
        Note:
            - 保存完整的快照数据
            - 基于唯一约束自动去重(忽略冲突)
        """
        try:
            logger.debug("准备保存子域名快照 - 数量: %d", len(items))
            
            if not items:
                logger.debug("子域名快照为空,跳过保存")
                return
            
            # 根据模型唯一约束自动去重
            unique_items = deduplicate_for_bulk(items, SubdomainSnapshot)
                
            # 构建快照对象
            snapshots = []
            for item in unique_items:
                snapshots.append(SubdomainSnapshot(
                    scan_id=item.scan_id,
                    name=item.name,
                ))
            
            # 批量创建(忽略冲突,基于唯一约束去重)
            SubdomainSnapshot.objects.bulk_create(snapshots, ignore_conflicts=True)
            
            logger.debug("子域名快照保存成功 - 数量: %d", len(snapshots))
            
        except Exception as e:
            logger.error(
                "保存子域名快照失败 - 数量: %d, 错误: %s",
                len(items),
                str(e),
                exc_info=True
            )
            raise
    
    def get_by_scan(self, scan_id: int):
        return SubdomainSnapshot.objects.filter(scan_id=scan_id).order_by('-created_at')

    def get_all(self):
        return SubdomainSnapshot.objects.all().order_by('-created_at')

    def iter_raw_data_for_export(
        self, 
        scan_id: int,
        batch_size: int = 1000
    ) -> Iterator[dict]:
        """
        流式获取原始数据用于 CSV 导出
        
        Args:
            scan_id: 扫描 ID
            batch_size: 每批数据量
        
        Yields:
            {'name': 'sub.example.com', 'created_at': datetime}
        """
        qs = (
            SubdomainSnapshot.objects
            .filter(scan_id=scan_id)
            .values('name', 'created_at')
            .order_by('name')
        )
        
        for row in qs.iterator(chunk_size=batch_size):
            yield row


================================================
FILE: backend/apps/asset/repositories/snapshot/vulnerability_snapshot_repository.py
================================================
"""Vulnerability Snapshot Repository - 漏洞快照数据访问层"""

import logging
from typing import List

from django.db import transaction

from apps.asset.models import VulnerabilitySnapshot
from apps.asset.dtos.snapshot import VulnerabilitySnapshotDTO
from apps.common.decorators import auto_ensure_db_connection
from apps.common.utils import deduplicate_for_bulk

logger = logging.getLogger(__name__)


@auto_ensure_db_connection
class DjangoVulnerabilitySnapshotRepository:
    """漏洞快照仓储(Django ORM 实现)"""

    def save_snapshots(self, items: List[VulnerabilitySnapshotDTO]) -> None:
        """批量保存漏洞快照记录。

        使用 ``ignore_conflicts`` 策略,如果快照已存在则跳过。
        具体唯一约束由数据库模型控制。
        
        注意:会自动按唯一约束字段去重,保留最后一条记录。
        """
        if not items:
            logger.warning("漏洞快照列表为空,跳过保存")
            return

        try:
            # 根据模型唯一约束自动去重
            unique_items = deduplicate_for_bulk(items, VulnerabilitySnapshot)
            
            snapshot_objects = [
                VulnerabilitySnapshot(
                    scan_id=item.scan_id,
                    url=item.url,
                    vuln_type=item.vuln_type,
                    severity=item.severity,
                    source=item.source,
                    cvss_score=item.cvss_score,
                    description=item.description,
                    raw_output=item.raw_output,
                )
                for item in unique_items
            ]

            with transaction.atomic():
                VulnerabilitySnapshot.objects.bulk_create(
                    snapshot_objects,
                    ignore_conflicts=True,
                )

            logger.debug("成功保存 %d 条漏洞快照记录", len(unique_items))

        except Exception as e:
            logger.error(
                "批量保存漏洞快照失败 - 数量: %d, 错误: %s",
                len(items),
                str(e),
                exc_info=True,
            )
            raise

    def get_by_scan(self, scan_id: int):
        """按扫描任务获取漏洞快照 QuerySet。"""
        return VulnerabilitySnapshot.objects.filter(scan_id=scan_id).order_by("-created_at")

    def get_all(self):
        return VulnerabilitySnapshot.objects.all().order_by('-created_at')


================================================
FILE: backend/apps/asset/repositories/snapshot/website_snapshot_repository.py
================================================
"""WebsiteSnapshot Repository - Django ORM 实现"""

import logging
from typing import List, Iterator

from apps.asset.models.snapshot_models import WebsiteSnapshot
from apps.asset.dtos.snapshot import WebsiteSnapshotDTO
from apps.common.decorators import auto_ensure_db_connection
from apps.common.utils import deduplicate_for_bulk

logger = logging.getLogger(__name__)


@auto_ensure_db_connection
class DjangoWebsiteSnapshotRepository:
    """网站快照 Repository - 负责网站快照表的数据访问"""

    def save_snapshots(self, items: List[WebsiteSnapshotDTO]) -> None:
        """
        保存网站快照
        
        注意:会自动按 (scan_id, url) 去重,保留最后一条记录。
        
        Args:
            items: 网站快照 DTO 列表
        
        Note:
            - 保存完整的快照数据
            - 基于唯一约束 (scan + subdomain + url) 自动去重
        """
        try:
            logger.debug("准备保存网站快照 - 数量: %d", len(items))
            
            if not items:
                logger.debug("网站快照为空,跳过保存")
                return
            
            # 根据模型唯一约束自动去重
            unique_items = deduplicate_for_bulk(items, WebsiteSnapshot)
                
            # 构建快照对象
            snapshots = []
            for item in unique_items:
                snapshots.append(WebsiteSnapshot(
                    scan_id=item.scan_id,
                    url=item.url,
                    host=item.host,
                    title=item.title,
                    status_code=item.status_code,
                    content_length=item.content_length,
                    location=item.location,
                    webserver=item.webserver,
                    content_type=item.content_type,
                    tech=item.tech if item.tech else [],
                    response_body=item.response_body,
                    vhost=item.vhost,
                    response_headers=item.response_headers if item.response_headers else ''
                ))
            
            # 批量创建(忽略冲突,基于唯一约束去重)
            WebsiteSnapshot.objects.bulk_create(
                snapshots, 
                ignore_conflicts=True
            )
            
            logger.debug("网站快照保存成功 - 数量: %d", len(snapshots))
            
        except Exception as e:
            logger.error(
                "保存网站快照失败 - 数量: %d, 错误: %s",
                len(items),
                str(e),
                exc_info=True
            )
            raise
    
    def get_by_scan(self, scan_id: int):
        return WebsiteSnapshot.objects.filter(scan_id=scan_id).order_by('-created_at')

    def get_all(self):
        return WebsiteSnapshot.objects.all().order_by('-created_at')

    def iter_raw_data_for_export(
        self, 
        scan_id: int,
        batch_size: int = 1000
    ) -> Iterator[dict]:
        """
        流式获取原始数据用于 CSV 导出
        
        Args:
            scan_id: 扫描 ID
            batch_size: 每批数据量
        
        Yields:
            包含所有网站字段的字典
        """
        qs = (
            WebsiteSnapshot.objects
            .filter(scan_id=scan_id)
            .values(
                'url', 'host', 'location', 'title', 'status_code',
                'content_length', 'content_type', 'webserver', 'tech',
                'response_body', 'response_headers', 'vhost', 'created_at'
            )
            .order_by('url')
        )
        
        for row in qs.iterator(chunk_size=batch_size):
            yield row


================================================
FILE: backend/apps/asset/repositories/statistics_repository.py
================================================
"""资产统计 Repository"""
import logging
from datetime import date, timedelta
from typing import Optional, List

from apps.asset.models import AssetStatistics, StatisticsHistory

logger = logging.getLogger(__name__)


class AssetStatisticsRepository:
    """
    资产统计数据访问层
    
    职责:
    - 读取/更新预聚合的统计数据
    """

    def get_statistics(self) -> Optional[AssetStatistics]:
        """
        获取统计数据
        
        Returns:
            统计数据对象,不存在则返回 None
        """
        return AssetStatistics.objects.first()

    def get_or_create_statistics(self) -> AssetStatistics:
        """
        获取或创建统计数据(单例)
        
        Returns:
            统计数据对象
        """
        return AssetStatistics.get_or_create_singleton()

    def update_statistics(
        self,
        total_targets: int,
        total_subdomains: int,
        total_ips: int,
        total_endpoints: int,
        total_websites: int,
        total_vulns: int,
    ) -> AssetStatistics:
        """
        更新统计数据
        
        Args:
            total_targets: 目标总数
            total_subdomains: 子域名总数
            total_ips: IP 总数
            total_endpoints: 端点总数
            total_websites: 网站总数
            total_vulns: 漏洞总数
        
        Returns:
            更新后的统计数据对象
        """
        stats = self.get_or_create_statistics()
        
        # 1. 保存当前值到 prev_* 字段
        stats.prev_targets = stats.total_targets
        stats.prev_subdomains = stats.total_subdomains
        stats.prev_ips = stats.total_ips
        stats.prev_endpoints = stats.total_endpoints
        stats.prev_websites = stats.total_websites
        stats.prev_vulns = stats.total_vulns
        stats.prev_assets = stats.total_assets
        
        # 2. 更新当前值
        stats.total_targets = total_targets
        stats.total_subdomains = total_subdomains
        stats.total_ips = total_ips
        stats.total_endpoints = total_endpoints
        stats.total_websites = total_websites
        stats.total_vulns = total_vulns
        stats.total_assets = total_subdomains + total_ips + total_endpoints + total_websites
        stats.save()
        
        logger.info(
            "更新资产统计: targets=%d, subdomains=%d, ips=%d, endpoints=%d, websites=%d, vulns=%d, assets=%d",
            total_targets, total_subdomains, total_ips, total_endpoints, total_websites, total_vulns, stats.total_assets
        )
        return stats

    def save_daily_snapshot(self, stats: AssetStatistics) -> StatisticsHistory:
        """
        保存每日统计快照(幂等,每天只存一条)
        
        Args:
            stats: 当前统计数据
        
        Returns:
            历史记录对象
        """
        history, created = StatisticsHistory.objects.update_or_create(
            date=date.today(),
            defaults={
                'total_targets': stats.total_targets,
                'total_subdomains': stats.total_subdomains,
                'total_ips': stats.total_ips,
                'total_endpoints': stats.total_endpoints,
                'total_websites': stats.total_websites,
                'total_vulns': stats.total_vulns,
                'total_assets': stats.total_assets,
            }
        )
        action = "创建" if created else "更新"
        logger.info(f"{action}统计快照: date={history.date}, assets={history.total_assets}")
        return history

    def get_history(self, days: int = 7) -> List[StatisticsHistory]:
        """
        获取历史统计数据(用于折线图)
        
        Args:
            days: 获取最近多少天的数据,默认 7 天
        
        Returns:
            历史记录列表,按日期升序
        """
        start_date = date.today() - timedelta(days=days - 1)
        return list(
            StatisticsHistory.objects
            .filter(date__gte=start_date)
            .order_by('date')
        )


================================================
FILE: backend/apps/asset/serializers.py
================================================
from rest_framework import serializers
from .models import Subdomain, WebSite, Directory, HostPortMapping, Endpoint, Vulnerability
from .models.snapshot_models import (
    SubdomainSnapshot,
    WebsiteSnapshot,
    DirectorySnapshot,
    EndpointSnapshot,
    VulnerabilitySnapshot,
)
from .models.screenshot_models import Screenshot, ScreenshotSnapshot


# 注意:IPAddress 和 Port 模型已被重构为 HostPortMapping
# 以下是基于新架构的序列化器实现

# class PortSerializer(serializers.ModelSerializer):
#     """端口序列化器"""
#     
#     class Meta:
#         model = Port
#         fields = ['number', 'service_name', 'description', 'is_uncommon']


class SubdomainSerializer(serializers.ModelSerializer):
    """子域名序列化器"""
    
    class Meta:
        model = Subdomain
        fields = [
            'id', 'name', 'created_at', 'target'
        ]
        read_only_fields = ['id', 'created_at']


class SubdomainListSerializer(serializers.ModelSerializer):
    """子域名列表序列化器(用于扫描详情)"""
    
    # 注意:Subdomain 模型已简化,只保留核心字段
    # cname, is_cdn, cdn_name 等字段已移至 SubdomainSnapshot
    # ports 和 ip_addresses 关系已被重构为 HostPortMapping
    
    class Meta:
        model = Subdomain
        fields = [
            'id', 'name', 'created_at'
        ]
        read_only_fields = ['id', 'created_at']


# class IPAddressListSerializer(serializers.ModelSerializer):
#     """IP 地址列表序列化器"""
#
#     subdomain = serializers.CharField(source='subdomain.name', allow_blank=True, default='')
#     created_at = serializers.DateTimeField(read_only=True)
#     ports = PortSerializer(many=True, read_only=True)
#
#     class Meta:
#         model = IPAddress
#         fields = [
#             'id',
#             'ip',
#             'subdomain',
#             'reverse_pointer',
#             'created_at',
#             'ports',
#         ]
#         read_only_fields = fields


class WebSiteSerializer(serializers.ModelSerializer):
    """站点序列化器(目标详情页)"""
    
    subdomain = serializers.CharField(source='subdomain.name', allow_blank=True, default='')
    responseHeaders = serializers.CharField(source='response_headers', read_only=True)  # 原始HTTP响应头
    
    class Meta:
        model = WebSite
        fields = [
            'id',
            'url',
            'host',
            'location', 
            'title',
            'webserver',
            'content_type',
            'status_code',
            'content_length',
            'response_body',
            'tech',
            'vhost',
            'responseHeaders',  # HTTP响应头
            'subdomain',
            'created_at',
        ]
        read_only_fields = fields


class VulnerabilitySerializer(serializers.ModelSerializer):
    """漏洞资产序列化器(按目标查看漏洞资产)。"""

    class Meta:
        model = Vulnerability
        fields = [
            'id',
            'target',
            'url',
            'vuln_type',
            'severity',
            'source',
            'cvss_score',
            'description',
            'raw_output',
            'created_at',
        ]
        read_only_fields = fields


class VulnerabilitySnapshotSerializer(serializers.ModelSerializer):
    """漏洞快照序列化器(用于扫描历史漏洞列表)。"""

    class Meta:
        model = VulnerabilitySnapshot
        fields = [
            'id',
            'url',
            'vuln_type',
            'severity',
            'source',
            'cvss_score',
            'description',
            'raw_output',
            'created_at',
        ]
        read_only_fields = fields


class EndpointListSerializer(serializers.ModelSerializer):
    """端点列表序列化器(用于目标端点列表页)"""

    # GF 匹配模式(gf-patterns 工具匹配的敏感 URL 模式)
    gfPatterns = serializers.ListField(
        child=serializers.CharField(),
        source='matched_gf_patterns',
        read_only=True,
    )
    responseHeaders = serializers.CharField(source='response_headers', read_only=True)  # 原始HTTP响应头

    class Meta:
        model = Endpoint
        fields = [
            'id',
            'url',
            'location',
            'status_code',
            'title',
            'content_length',
            'content_type',
            'webserver',
            'response_body',
            'tech',
            'vhost',
            'responseHeaders',  # HTTP响应头
            'gfPatterns',
            'created_at',
        ]
        read_only_fields = fields


class DirectorySerializer(serializers.ModelSerializer):
    """目录序列化器"""
    
    created_at = serializers.DateTimeField(read_only=True)
    
    class Meta:
        model = Directory
        fields = [
            'id',
            'url',
            'status',
            'content_length',
            'words',
            'lines',
            'content_type',
            'duration',
            'created_at',
        ]
        read_only_fields = fields


class IPAddressAggregatedSerializer(serializers.Serializer):
    """
    IP 地址聚合序列化器
    
    基于 HostPortMapping 模型,按 IP 聚合显示:
    - ip: IP 地址
    - hosts: 该 IP 关联的所有主机名列表
    - ports: 该 IP 关联的所有端口列表
    - created_at: 创建时间
    """
    ip = serializers.IPAddressField(read_only=True)
    hosts = serializers.ListField(child=serializers.CharField(), read_only=True)
    ports = serializers.ListField(child=serializers.IntegerField(), read_only=True)
    created_at = serializers.DateTimeField(read_only=True)


# ==================== 快照序列化器 ====================

class SubdomainSnapshotSerializer(serializers.ModelSerializer):
    """子域名快照序列化器(用于扫描历史)"""
    
    class Meta:
        model = SubdomainSnapshot
        fields = ['id', 'name', 'created_at']
        read_only_fields = fields


class WebsiteSnapshotSerializer(serializers.ModelSerializer):
    """网站快照序列化器(用于扫描历史)"""
    
    subdomain_name = serializers.CharField(source='subdomain.name', read_only=True)
    responseHeaders = serializers.CharField(source='response_headers', read_only=True)  # 原始HTTP响应头
    
    class Meta:
        model = WebsiteSnapshot
        fields = [
            'id',
            'url',
            'location',
            'title',
            'webserver',
            'content_type',
            'status_code',
            'content_length',
            'response_body',
            'tech',
            'vhost',
            'responseHeaders',  # HTTP响应头
            'subdomain_name',
            'created_at',
        ]
        read_only_fields = fields


class DirectorySnapshotSerializer(serializers.ModelSerializer):
    """目录快照序列化器(用于扫描历史)"""
    
    class Meta:
        model = DirectorySnapshot
        fields = [
            'id',
            'url',
            'status',
            'content_length',
            'words',
            'lines',
            'content_type',
            'duration',
            'created_at',
        ]
        read_only_fields = fields


class EndpointSnapshotSerializer(serializers.ModelSerializer):
    """端点快照序列化器(用于扫描历史)"""

    # GF 匹配模式(gf-patterns 工具匹配的敏感 URL 模式)
    gfPatterns = serializers.ListField(
        child=serializers.CharField(),
        source='matched_gf_patterns',
        read_only=True,
    )
    responseHeaders = serializers.CharField(source='response_headers', read_only=True)  # 原始HTTP响应头

    class Meta:
        model = EndpointSnapshot
        fields = [
            'id',
            'url',
            'host',
            'location',
            'title',
            'webserver',
            'content_type',
            'status_code',
            'content_length',
            'response_body',
            'tech',
            'vhost',
            'responseHeaders',  # HTTP响应头
            'gfPatterns',
            'created_at',
        ]
        read_only_fields = fields


# ==================== 截图序列化器 ====================

class ScreenshotListSerializer(serializers.ModelSerializer):
    """截图资产列表序列化器(不包含 image 字段)"""
    
    class Meta:
        model = Screenshot
        fields = ['id', 'url', 'status_code', 'created_at', 'updated_at']
        read_only_fields = fields


class ScreenshotSnapshotListSerializer(serializers.ModelSerializer):
    """截图快照列表序列化器(不包含 image 字段)"""
    
    class Meta:
        model = ScreenshotSnapshot
        fields = ['id', 'url', 'status_code', 'created_at']
        read_only_fields = fields


================================================
FILE: backend/apps/asset/services/__init__.py
================================================
"""Asset Services - 业务逻辑层"""

# 资产模块 Services
from .asset import (
    SubdomainService,
    WebSiteService,
    DirectoryService,
    HostPortMappingService,
    EndpointService,
    VulnerabilityService,
)

# 快照模块 Services
from .snapshot import (
    SubdomainSnapshotsService,
    HostPortMappingSnapshotsService,
    WebsiteSnapshotsService,
    DirectorySnapshotsService,
    EndpointSnapshotsService,
    VulnerabilitySnapshotsService,
)

# 统计模块 Service
from .statistics_service import AssetStatisticsService

__all__ = [
    # 资产模块
    'SubdomainService',
    'WebSiteService',
    'DirectoryService',
    'HostPortMappingService',
    'EndpointService',
    'VulnerabilityService',
    # 快照模块
    'SubdomainSnapshotsService',
    'HostPortMappingSnapshotsService',
    'WebsiteSnapshotsService',
    'DirectorySnapshotsService',
    'EndpointSnapshotsService',
    'VulnerabilitySnapshotsService',
    # 统计模块
    'AssetStatisticsService',
]


================================================
FILE: backend/apps/asset/services/asset/__init__.py
================================================
"""Asset Services - 资产模块的业务逻辑层"""

from .subdomain_service import SubdomainService
from .website_service import WebSiteService
from .directory_service import DirectoryService
from .host_port_mapping_service import HostPortMappingService
from .endpoint_service import EndpointService
from .vulnerability_service import VulnerabilityService

__all__ = [
    'SubdomainService',
    'WebSiteService',
    'DirectoryService',
    'HostPortMappingService',
    'EndpointService',
    'VulnerabilityService',
]


================================================
FILE: backend/apps/asset/services/asset/directory_service.py
================================================
"""Directory Service - 目录业务逻辑层"""

import logging
from typing import List, Iterator, Optional

from apps.asset.repositories import DjangoDirectoryRepository
from apps.asset.dtos import DirectoryDTO
from apps.common.validators import is_valid_url, is_url_match_target
from apps.common.utils.filter_utils import apply_filters

logger = logging.getLogger(__name__)


class DirectoryService:
    """目录业务逻辑层"""
    
    # 智能过滤字段映射
    FILTER_FIELD_MAPPING = {
        'url': 'url',
        'status': 'status',
    }
    
    def __init__(self, repository=None):
        """初始化目录服务"""
        self.repo = repository or DjangoDirectoryRepository()
    
    def bulk_upsert(self, directory_dtos: List[DirectoryDTO]) -> int:
        """
        批量创建或更新目录(upsert)
        
        存在则更新所有字段,不存在则创建。
        
        Args:
            directory_dtos: DirectoryDTO 列表
            
        Returns:
            int: 处理的记录数
        """
        if not directory_dtos:
            return 0
        
        try:
            return self.repo.bulk_upsert(directory_dtos)
        except Exception as e:
            logger.error(f"批量 upsert 目录失败: {e}")
            raise
    
    def bulk_create_urls(self, target_id: int, target_name: str, target_type: str, urls: List[str]) -> int:
        """
        批量创建目录(仅 URL,使用 ignore_conflicts)
        
        验证 URL 格式和匹配,过滤无效/不匹配 URL,去重后批量创建。
        已存在的记录会被跳过。
        
        Args:
            target_id: 目标 ID
            target_name: 目标名称(用于匹配验证)
            target_type: 目标类型 ('domain', 'ip', 'cidr')
            urls: URL 列表
            
        Returns:
            int: 实际创建的记录数
        """
        if not urls:
            return 0
        
        # 过滤有效 URL 并去重
        valid_urls = []
        seen = set()
        
        for url in urls:
            if not isinstance(url, str):
                continue
            url = url.strip()
            if not url or url in seen:
                continue
            if not is_valid_url(url):
                continue
            
            # 匹配验证(前端已阻止不匹配的提交,后端作为双重保障)
            if not is_url_match_target(url, target_name, target_type):
                continue
            
            seen.add(url)
            valid_urls.append(url)
        
        if not valid_urls:
            return 0
        
        # 获取创建前的数量
        count_before = self.repo.count_by_target(target_id)
        
        # 创建 DTO 列表并批量创建
        directory_dtos = [
            DirectoryDTO(url=url, target_id=target_id)
            for url in valid_urls
        ]
        self.repo.bulk_create_ignore_conflicts(directory_dtos)
        
        # 获取创建后的数量
        count_after = self.repo.count_by_target(target_id)
        return count_after - count_before
    
    def get_directories_by_target(self, target_id: int, filter_query: Optional[str] = None):
        """获取目标下的所有目录"""
        queryset = self.repo.get_by_target(target_id)
        if filter_query:
            queryset = apply_filters(queryset, filter_query, self.FILTER_FIELD_MAPPING)
        return queryset
    
    def get_all(self, filter_query: Optional[str] = None):
        """获取所有目录"""
        queryset = self.repo.get_all()
        if filter_query:
            queryset = apply_filters(queryset, filter_query, self.FILTER_FIELD_MAPPING)
        return queryset

    def iter_directory_urls_by_target(self, target_id: int, chunk_size: int = 1000) -> Iterator[str]:
        """流式获取目标下的所有目录 URL"""
        return self.repo.get_urls_for_export(target_id=target_id, batch_size=chunk_size)

    def iter_raw_data_for_csv_export(self, target_id: int) -> Iterator[dict]:
        """
        流式获取原始数据用于 CSV 导出
        
        Args:
            target_id: 目标 ID
        
        Yields:
            原始数据字典
        """
        return self.repo.iter_raw_data_for_export(target_id=target_id)


__all__ = ['DirectoryService']


================================================
FILE: backend/apps/asset/services/asset/endpoint_service.py
================================================
"""
Endpoint 服务层

处理 URL/端点相关的业务逻辑
"""

import logging
from typing import List, Iterator, Optional

from apps.asset.dtos.asset import EndpointDTO
from apps.asset.repositories.asset import DjangoEndpointRepository
from apps.common.validators import is_valid_url, is_url_match_target
from apps.common.utils.filter_utils import apply_filters

logger = logging.getLogger(__name__)


class EndpointService:
    """
    Endpoint 服务类
    
    提供 Endpoint(URL/端点)相关的业务逻辑
    """
    
    # 智能过滤字段映射
    FILTER_FIELD_MAPPING = {
        'url': 'url',
        'host': 'host',
        'title': 'title',
        'status_code': 'status_code',
        'tech': 'tech',
    }
    
    def __init__(self):
        """初始化 Endpoint 服务"""
        self.repo = DjangoEndpointRepository()
    
    def bulk_upsert(self, endpoints: List[EndpointDTO]) -> int:
        """
        批量创建或更新端点(upsert)
        
        存在则更新所有字段,不存在则创建。
        
        Args:
            endpoints: 端点数据列表
            
        Returns:
            int: 处理的记录数
        """
        if not endpoints:
            return 0
        
        try:
            return self.repo.bulk_upsert(endpoints)
        except Exception as e:
            logger.error(f"批量 upsert 端点失败: {e}")
            raise
    
    def bulk_create_urls(self, target_id: int, target_name: str, target_type: str, urls: List[str]) -> int:
        """
        批量创建端点(仅 URL,使用 ignore_conflicts)
        
        验证 URL 格式和匹配,过滤无效/不匹配 URL,去重后批量创建。
        已存在的记录会被跳过。
        
        Args:
            target_id: 目标 ID
            target_name: 目标名称(用于匹配验证)
            target_type: 目标类型 ('domain', 'ip', 'cidr')
            urls: URL 列表
            
        Returns:
            int: 实际创建的记录数
        """
        if not urls:
            return 0
        
        # 过滤有效 URL 并去重
        valid_urls = []
        seen = set()
        
        for url in urls:
            if not isinstance(url, str):
                continue
            url = url.strip()
            if not url or url in seen:
                continue
            if not is_valid_url(url):
                continue
            
            # 匹配验证(前端已阻止不匹配的提交,后端作为双重保障)
            if not is_url_match_target(url, target_name, target_type):
                continue
            
            seen.add(url)
            valid_urls.append(url)
        
        if not valid_urls:
            return 0
        
        # 获取创建前的数量
        count_before = self.repo.count_by_target(target_id)
        
        # 创建 DTO 列表并批量创建
        endpoint_dtos = [
            EndpointDTO(url=url, target_id=target_id)
            for url in valid_urls
        ]
        self.repo.bulk_create_ignore_conflicts(endpoint_dtos)
        
        # 获取创建后的数量
        count_after = self.repo.count_by_target(target_id)
        return count_after - count_before
    
    def get_endpoints_by_target(self, target_id: int, filter_query: Optional[str] = None):
        """获取目标下的所有端点"""
        queryset = self.repo.get_by_target(target_id)
        if filter_query:
            queryset = apply_filters(queryset, filter_query, self.FILTER_FIELD_MAPPING, json_array_fields=['tech'])
        return queryset
    
    def count_endpoints_by_target(self, target_id: int) -> int:
        """
        统计目标下的端点数量
        
        Args:
            target_id: 目标 ID
            
        Returns:
            int: 端点数量
        """
        return self.repo.count_by_target(target_id)

    def get_all(self, filter_query: Optional[str] = None):
        """获取所有端点(全局查询)"""
        queryset = self.repo.get_all()
        if filter_query:
            queryset = apply_filters(queryset, filter_query, self.FILTER_FIELD_MAPPING, json_array_fields=['tech'])
        return queryset
    
    def iter_endpoint_urls_by_target(self, target_id: int, chunk_size: int = 1000) -> Iterator[str]:
        """流式获取目标下的所有端点 URL,用于导出。"""
        queryset = self.repo.get_by_target(target_id)
        for url in queryset.values_list('url', flat=True).iterator(chunk_size=chunk_size):
            yield url

    def iter_raw_data_for_csv_export(self, target_id: int) -> Iterator[dict]:
        """
        流式获取原始数据用于 CSV 导出
        
        Args:
            target_id: 目标 ID
        
        Yields:
            原始数据字典
        """
        return self.repo.iter_raw_data_for_export(target_id=target_id)


================================================
FILE: backend/apps/asset/services/asset/host_port_mapping_service.py
================================================
"""HostPortMapping Service - 业务逻辑层"""

import logging
from typing import List, Iterator, Optional, Dict

from django.db.models import Min

from apps.asset.repositories.asset import DjangoHostPortMappingRepository
from apps.asset.dtos.asset import HostPortMappingDTO
from apps.common.utils.filter_utils import apply_filters

logger = logging.getLogger(__name__)


class HostPortMappingService:
    """主机端口映射服务 - 负责主机端口映射数据的业务逻辑
    
    职责:
    - 业务逻辑处理(过滤、聚合)
    - 调用 Repository 进行数据访问
    """
    
    # 智能过滤字段映射
    FILTER_FIELD_MAPPING = {
        'ip': 'ip',
        'port': 'port',
        'host': 'host',
    }
    
    def __init__(self):
        self.repo = DjangoHostPortMappingRepository()
    
    def bulk_create_ignore_conflicts(self, items: List[HostPortMappingDTO]) -> int:
        """
        批量创建主机端口映射(忽略冲突)
        
        Args:
            items: 主机端口映射 DTO 列表
        
        Returns:
            int: 实际创建的记录数
        
        Note:
            使用数据库唯一约束 + ignore_conflicts 自动去重
        """
        try:
            logger.debug("Service: 准备批量创建主机端口映射 - 数量: %d", len(items))
            
            created_count = self.repo.bulk_create_ignore_conflicts(items)
            
            logger.info("Service: 主机端口映射创建成功 - 数量: %d", created_count)
            
            return created_count
            
        except Exception as e:
            logger.error(
                "Service: 批量创建主机端口映射失败 - 数量: %d, 错误: %s",
                len(items),
                str(e),
                exc_info=True
            )
            raise

    def iter_host_port_by_target(self, target_id: int, batch_size: int = 1000):
        return self.repo.get_for_export(target_id=target_id, batch_size=batch_size)

    def get_ip_aggregation_by_target(
        self, 
        target_id: int, 
        filter_query: Optional[str] = None
    ) -> List[Dict]:
        """获取目标下的 IP 聚合数据
        
        Args:
            target_id: 目标 ID
            filter_query: 智能过滤语法字符串
        
        Returns:
            聚合后的 IP 数据列表
        """
        # 从 Repository 获取基础 QuerySet
        qs = self.repo.get_queryset_by_target(target_id)
        
        # Service 层应用过滤逻辑
        if filter_query:
            qs = apply_filters(qs, filter_query, self.FILTER_FIELD_MAPPING)
        
        # Service 层处理聚合逻辑
        return self._aggregate_by_ip(qs, filter_query, target_id=target_id)

    def get_all_ip_aggregation(self, filter_query: Optional[str] = None) -> List[Dict]:
        """获取所有 IP 聚合数据(全局查询)
        
        Args:
            filter_query: 智能过滤语法字符串
        
        Returns:
            聚合后的 IP 数据列表
        """
        # 从 Repository 获取基础 QuerySet
        qs = self.repo.get_all_queryset()
        
        # Service 层应用过滤逻辑
        if filter_query:
            qs = apply_filters(qs, filter_query, self.FILTER_FIELD_MAPPING)
        
        # Service 层处理聚合逻辑
        return self._aggregate_by_ip(qs, filter_query)

    def _aggregate_by_ip(
        self, 
        qs, 
        filter_query: Optional[str] = None,
        target_id: Optional[int] = None
    ) -> List[Dict]:
        """按 IP 聚合数据
        
        Args:
            qs: 已过滤的 QuerySet
            filter_query: 过滤条件(用于子查询)
            target_id: 目标 ID(用于子查询限定范围)
        
        Returns:
            聚合后的数据列表
        """
        ip_aggregated = (
            qs
            .values('ip')
            .annotate(created_at=Min('created_at'))
            .order_by('-created_at')
        )

        results = []
        for item in ip_aggregated:
            ip = item['ip']
            
            # 获取该 IP 的所有 host 和 port(也需要应用过滤条件)
            mappings_qs = self.repo.get_queryset_by_ip(ip, target_id=target_id)
            if filter_query:
                mappings_qs = apply_filters(mappings_qs, filter_query, self.FILTER_FIELD_MAPPING)
            
            mappings = mappings_qs.values('host', 'port').distinct()
            hosts = sorted({m['host'] for m in mappings})
            ports = sorted({m['port'] for m in mappings})
            
            results.append({
                'ip': ip,
                'hosts': hosts,
                'ports': ports,
                'created_at': item['created_at'],
            })
        
        return results

    def iter_ips_by_target(self, target_id: int, batch_size: int = 1000) -> Iterator[str]:
        """流式获取目标下的所有唯一 IP 地址。"""
        return self.repo.get_ips_for_export(target_id=target_id, batch_size=batch_size)

    def iter_raw_data_for_csv_export(self, target_id: int) -> Iterator[dict]:
        """
        流式获取原始数据用于 CSV 导出
        
        Args:
            target_id: 目标 ID
        
        Yields:
            原始数据字典 {ip, host, port, created_at}
        """
        return self.repo.iter_raw_data_for_export(target_id=target_id)


================================================
FILE: backend/apps/asset/services/asset/subdomain_service.py
================================================
import logging
from typing import List, Dict, Optional
from dataclasses import dataclass

from apps.asset.repositories import DjangoSubdomainRepository
from apps.asset.dtos import SubdomainDTO
from apps.common.validators import is_valid_domain
from apps.common.utils.filter_utils import apply_filters

logger = logging.getLogger(__name__)


@dataclass
class BulkCreateResult:
    """批量创建结果"""
    created_count: int
    skipped_count: int
    invalid_count: int
    mismatched_count: int
    total_received: int


class SubdomainService:
    """子域名业务逻辑层"""
    
    # 智能过滤字段映射
    FILTER_FIELD_MAPPING = {
        'name': 'name',
    }
    
    def __init__(self, repository=None):
        """
        初始化子域名服务
        
        Args:
            repository: 子域名仓储实例(用于依赖注入)
        """
        self.repo = repository or DjangoSubdomainRepository()
    
    # ==================== 查询操作 ====================
    
    def get_all(self, filter_query: Optional[str] = None):
        """
        获取所有子域名
        
        Args:
            filter_query: 智能过滤语法字符串
        
        Returns:
            QuerySet: 子域名查询集
        """
        logger.debug("获取所有子域名")
        queryset = self.repo.get_all()
        if filter_query:
            queryset = apply_filters(queryset, filter_query, self.FILTER_FIELD_MAPPING)
        return queryset
    
    def get_subdomains_by_target(self, target_id: int, filter_query: Optional[str] = None):
        """
        获取目标下的子域名
        
        Args:
            target_id: 目标 ID
            filter_query: 智能过滤语法字符串
        
        Returns:
            QuerySet: 子域名查询集
        """
        queryset = self.repo.get_by_target(target_id)
        if filter_query:
            queryset = apply_filters(queryset, filter_query, self.FILTER_FIELD_MAPPING)
        return queryset

    def count_subdomains_by_target(self, target_id: int) -> int:
        """
        统计目标下的子域名数量
        
        Args:
            target_id: 目标 ID
        
        Returns:
            int: 子域名数量
        """
        logger.debug("统计目标下子域名数量 - Target ID: %d", target_id)
        return self.repo.count_by_target(target_id)
    
    def get_by_names_and_target_id(self, names: set, target_id: int) -> dict:
        """
        根据域名列表和目标ID批量查询子域名
        
        Args:
            names: 域名集合
            target_id: 目标 ID
        
        Returns:
            dict: {域名: Subdomain对象}
        """
        logger.debug("批量查询子域名 - 数量: %d, Target ID: %d", len(names), target_id)
        return self.repo.get_by_names_and_target_id(names, target_id)
    
    def get_subdomain_names_by_target(self, target_id: int) -> List[str]:
        """
        获取目标下的所有子域名名称
        
        Args:
            target_id: 目标 ID
        
        Returns:
            List[str]: 子域名名称列表
        """
        logger.debug("获取目标下所有子域名 - Target ID: %d", target_id)
        return list(self.repo.get_domains_for_export(target_id=target_id))
    
    def iter_subdomain_names_by_target(self, target_id: int, chunk_size: int = 1000):
        """
        流式获取目标下的所有子域名名称(内存优化)
        
        Args:
            target_id: 目标 ID
            chunk_size: 批次大小
        
        Yields:
            str: 子域名名称
        """
        logger.debug("流式获取目标下所有子域名 - Target ID: %d, 批次大小: %d", target_id, chunk_size)
        return self.repo.get_domains_for_export(target_id=target_id, batch_size=chunk_size)

    def iter_raw_data_for_csv_export(self, target_id: int):
        """
        流式获取原始数据用于 CSV 导出
        
        Args:
            target_id: 目标 ID
        
        Yields:
            原始数据字典 {name, created_at}
        """
        return self.repo.iter_raw_data_for_export(target_id=target_id)

    # ==================== 创建操作 ====================

    def bulk_create_ignore_conflicts(self, items: List[SubdomainDTO]) -> None:
        """
        批量创建子域名,忽略冲突
        
        Args:
            items: 子域名 DTO 列表
        
        Note:
            使用 ignore_conflicts 策略,重复记录会被跳过
        """
        logger.debug("批量创建子域名 - 数量: %d", len(items))
        return self.repo.bulk_create_ignore_conflicts(items)

    def bulk_create_subdomains(
        self,
        target_id: int,
        target_name: str,
        subdomains: List[str]
    ) -> BulkCreateResult:
        """
        批量创建子域名(带验证)
        
        Args:
            target_id: 目标 ID
            target_name: 目标域名(用于匹配验证)
            subdomains: 子域名列表
        
        Returns:
            BulkCreateResult: 创建结果统计
        """
        total_received = len(subdomains)
        target_name = target_name.lower().strip()
        
        def is_subdomain_match(subdomain: str) -> bool:
            """验证子域名是否匹配目标域名"""
            if subdomain == target_name:
                return True
            if subdomain.endswith('.' + target_name):
                return True
            return False
        
        # 过滤有效的子域名
        valid_subdomains = []
        invalid_count = 0
        mismatched_count = 0
        
        for subdomain in subdomains:
            if not isinstance(subdomain, str) or not subdomain.strip():
                continue
            
            subdomain = subdomain.lower().strip()
            
            # 验证格式
            if not is_valid_domain(subdomain):
                invalid_count += 1
                continue
            
            # 验证匹配
            if not is_subdomain_match(subdomain):
                mismatched_count += 1
                continue
            
            valid_subdomains.append(subdomain)
        
        # 去重
        unique_subdomains = list(set(valid_subdomains))
        duplicate_count = len(valid_subdomains) - len(unique_subdomains)
        
        if not unique_subdomains:
            return BulkCreateResult(
                created_count=0,
                skipped_count=duplicate_count,
                invalid_count=invalid_count,
                mismatched_count=mismatched_count,
                total_received=total_received,
            )
        
        # 获取创建前的数量
        count_before = self.repo.count_by_target(target_id)
        
        # 创建 DTO 列表并批量创建
        subdomain_dtos = [
            SubdomainDTO(name=name, target_id=target_id)
            for name in unique_subdomains
        ]
        self.repo.bulk_create_ignore_conflicts(subdomain_dtos)
        
        # 获取创建后的数量
        count_after = self.repo.count_by_target(target_id)
        created_count = count_after - count_before
        
        # 计算因数据库冲突跳过的数量
        db_skipped = len(unique_subdomains) - created_count
        
        return BulkCreateResult(
            created_count=created_count,
            skipped_count=duplicate_count + db_skipped,
            invalid_count=invalid_count,
            mismatched_count=mismatched_count,
            total_received=total_received,
        )


__all__ = ['SubdomainService', 'BulkCreateResult']


================================================
FILE: backend/apps/asset/services/asset/vulnerability_service.py
================================================
"""Vulnerability Service - 漏洞资产业务逻辑层"""

import logging
from typing import List, Optional

from apps.asset.models import Vulnerability
from apps.asset.dtos.asset import VulnerabilityDTO
from apps.common.decorators import auto_ensure_db_connection
from apps.common.utils import deduplicate_for_bulk
from apps.common.utils.filter_utils import apply_filters

logger = logging.getLogger(__name__)


@auto_ensure_db_connection
class VulnerabilityService:
    """漏洞资产服务

    当前提供基础的批量创建能力,使用 ignore_conflicts 依赖数据库唯一约束去重。
    """
    
    # 智能过滤字段映射
    FILTER_FIELD_MAPPING = {
        'type': 'vuln_type',
        'severity': 'severity',
        'source': 'source',
        'url': 'url',
    }

    def bulk_create_ignore_conflicts(self, items: List[VulnerabilityDTO]) -> None:
        """批量创建漏洞资产记录,忽略冲突。

        注意:会自动按 (target_id, url, vuln_type, source) 去重,保留最后一条记录。

        Note:
            - 是否去重取决于模型上的唯一/部分唯一约束;
            - 当前 Vulnerability 模型未定义唯一约束,因此会保留全部记录。
        """
        if not items:
            logger.debug("漏洞资产列表为空,跳过保存")
            return

        try:
            # 根据模型唯一约束自动去重(如果模型没有唯一约束则跳过)
            unique_items = deduplicate_for_bulk(items, Vulnerability)
            
            vulns = [
                Vulnerability(
                    target_id=item.target_id,
                    url=item.url,
                    vuln_type=item.vuln_type,
                    severity=item.severity,
                    source=item.source,
                    cvss_score=item.cvss_score,
                    description=item.description,
                    raw_output=item.raw_output,
                )
                for item in unique_items
            ]

            Vulnerability.objects.bulk_create(vulns, ignore_conflicts=True)
            logger.info("漏洞资产保存成功 - 数量: %d", len(vulns))

        except Exception as e:
            logger.error(
                "批量保存漏洞资产失败 - 数量: %d, 错误: %s",
                len(items),
                str(e),
                exc_info=True,
            )
            raise

    # ==================== 查询方法 ====================

    def get_all(self, filter_query: Optional[str] = None):
        """获取所有漏洞 QuerySet(用于全局漏洞列表)。

        Args:
            filter_query: 智能过滤语法字符串

        Returns:
            QuerySet[Vulnerability]: 所有漏洞,按创建时间倒序
        """
        queryset = Vulnerability.objects.all().order_by("-created_at")
        if filter_query:
            queryset = apply_filters(queryset, filter_query, self.FILTER_FIELD_MAPPING)
        return queryset

    def get_vulnerabilities_by_target(self, target_id: int, filter_query: Optional[str] = None):
        """按目标获取漏洞 QuerySet(用于分页)。

        Args:
            target_id: 目标 ID
            filter_query: 智能过滤语法字符串

        Returns:
            QuerySet[Vulnerability]: 目标下的所有漏洞,按创建时间倒序
        """
        queryset = Vulnerability.objects.filter(target_id=target_id).order_by("-created_at")
        if filter_query:
            queryset = apply_filters(queryset, filter_query, self.FILTER_FIELD_MAPPING)
        return queryset

    def count_by_target(self, target_id: int) -> int:
        """统计目标下的漏洞数量。"""
        return Vulnerability.objects.filter(target_id=target_id).count()


================================================
FILE: backend/apps/asset/services/asset/website_service.py
================================================
"""WebSite Service - 网站业务逻辑层"""

import logging
from typing import List, Iterator, Optional

from apps.asset.repositories import DjangoWebSiteRepository
from apps.asset.dtos import WebSiteDTO
from apps.common.validators import is_valid_url, is_url_match_target
from apps.common.utils.filter_utils import apply_filters

logger = logging.getLogger(__name__)


class WebSiteService:
    """网站业务逻辑层"""
    
    # 智能过滤字段映射
    FILTER_F
Download .txt
gitextract__pp8fuf4/

├── .dockerignore
├── .github/
│   └── workflows/
│       └── docker-build.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── SECURITY.md
├── VERSION
├── backend/
│   ├── .gitignore
│   ├── apps/
│   │   ├── __init__.py
│   │   ├── asset/
│   │   │   ├── __init__.py
│   │   │   ├── apps.py
│   │   │   ├── dtos/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── asset/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── directory_dto.py
│   │   │   │   │   ├── endpoint_dto.py
│   │   │   │   │   ├── host_port_mapping_dto.py
│   │   │   │   │   ├── ip_address_dto.py
│   │   │   │   │   ├── port_dto.py
│   │   │   │   │   ├── subdomain_dto.py
│   │   │   │   │   ├── vulnerability_dto.py
│   │   │   │   │   └── website_dto.py
│   │   │   │   └── snapshot/
│   │   │   │       ├── __init__.py
│   │   │   │       ├── directory_snapshot_dto.py
│   │   │   │       ├── endpoint_snapshot_dto.py
│   │   │   │       ├── host_port_mapping_snapshot_dto.py
│   │   │   │       ├── subdomain_snapshot_dto.py
│   │   │   │       ├── vulnerability_snapshot_dto.py
│   │   │   │       └── website_snapshot_dto.py
│   │   │   ├── migrations/
│   │   │   │   ├── 0001_initial.py
│   │   │   │   ├── 0002_create_search_views.py
│   │   │   │   ├── 0003_add_screenshot_models.py
│   │   │   │   ├── 0004_add_status_code_to_screenshot.py
│   │   │   │   └── __init__.py
│   │   │   ├── models/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── asset_models.py
│   │   │   │   ├── screenshot_models.py
│   │   │   │   ├── snapshot_models.py
│   │   │   │   └── statistics_models.py
│   │   │   ├── repositories/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── asset/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── directory_repository.py
│   │   │   │   │   ├── endpoint_repository.py
│   │   │   │   │   ├── host_port_mapping_repository.py
│   │   │   │   │   ├── subdomain_repository.py
│   │   │   │   │   └── website_repository.py
│   │   │   │   ├── snapshot/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── directory_snapshot_repository.py
│   │   │   │   │   ├── endpoint_snapshot_repository.py
│   │   │   │   │   ├── host_port_mapping_snapshot_repository.py
│   │   │   │   │   ├── subdomain_snapshot_repository.py
│   │   │   │   │   ├── vulnerability_snapshot_repository.py
│   │   │   │   │   └── website_snapshot_repository.py
│   │   │   │   └── statistics_repository.py
│   │   │   ├── serializers.py
│   │   │   ├── services/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── asset/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── directory_service.py
│   │   │   │   │   ├── endpoint_service.py
│   │   │   │   │   ├── host_port_mapping_service.py
│   │   │   │   │   ├── subdomain_service.py
│   │   │   │   │   ├── vulnerability_service.py
│   │   │   │   │   └── website_service.py
│   │   │   │   ├── playwright_screenshot_service.py
│   │   │   │   ├── screenshot_service.py
│   │   │   │   ├── search_service.py
│   │   │   │   ├── snapshot/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── directory_snapshots_service.py
│   │   │   │   │   ├── endpoint_snapshots_service.py
│   │   │   │   │   ├── host_port_mapping_snapshots_service.py
│   │   │   │   │   ├── subdomain_snapshots_service.py
│   │   │   │   │   ├── vulnerability_snapshots_service.py
│   │   │   │   │   └── website_snapshots_service.py
│   │   │   │   └── statistics_service.py
│   │   │   ├── urls.py
│   │   │   └── views/
│   │   │       ├── __init__.py
│   │   │       ├── asset_views.py
│   │   │       └── search_views.py
│   │   ├── common/
│   │   │   ├── __init__.py
│   │   │   ├── apps.py
│   │   │   ├── authentication.py
│   │   │   ├── container_bootstrap.py
│   │   │   ├── decorators/
│   │   │   │   ├── __init__.py
│   │   │   │   └── db_connection.py
│   │   │   ├── definitions.py
│   │   │   ├── error_codes.py
│   │   │   ├── exception_handlers.py
│   │   │   ├── management/
│   │   │   │   └── commands/
│   │   │   │       ├── db_health_check.py
│   │   │   │       ├── db_monitor.py
│   │   │   │       └── init_admin.py
│   │   │   ├── migrations/
│   │   │   │   ├── 0001_initial.py
│   │   │   │   └── __init__.py
│   │   │   ├── models/
│   │   │   │   ├── __init__.py
│   │   │   │   └── blacklist.py
│   │   │   ├── normalizer.py
│   │   │   ├── pagination.py
│   │   │   ├── permissions.py
│   │   │   ├── prefect_django_setup.py
│   │   │   ├── response_helpers.py
│   │   │   ├── serializers/
│   │   │   │   ├── __init__.py
│   │   │   │   └── blacklist_serializers.py
│   │   │   ├── services/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── blacklist_service.py
│   │   │   │   └── system_log_service.py
│   │   │   ├── signals.py
│   │   │   ├── urls.py
│   │   │   ├── utils/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── blacklist_filter.py
│   │   │   │   ├── csv_utils.py
│   │   │   │   ├── dedup.py
│   │   │   │   ├── filter_utils.py
│   │   │   │   └── hash.py
│   │   │   ├── validators.py
│   │   │   ├── views/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── auth_views.py
│   │   │   │   ├── blacklist_views.py
│   │   │   │   ├── health_views.py
│   │   │   │   ├── system_log_views.py
│   │   │   │   └── version_views.py
│   │   │   └── websocket_auth.py
│   │   ├── engine/
│   │   │   ├── __init__.py
│   │   │   ├── apps.py
│   │   │   ├── consumers/
│   │   │   │   ├── __init__.py
│   │   │   │   └── worker_deploy_consumer.py
│   │   │   ├── management/
│   │   │   │   ├── __init__.py
│   │   │   │   └── commands/
│   │   │   │       ├── __init__.py
│   │   │   │       ├── init_default_engine.py
│   │   │   │       ├── init_fingerprints.py
│   │   │   │       ├── init_nuclei_templates.py
│   │   │   │       └── init_wordlists.py
│   │   │   ├── migrations/
│   │   │   │   ├── 0001_initial.py
│   │   │   │   └── __init__.py
│   │   │   ├── models/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── engine.py
│   │   │   │   └── fingerprints.py
│   │   │   ├── repositories/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── django_engine_repository.py
│   │   │   │   ├── django_wordlist_repository.py
│   │   │   │   ├── django_worker_repository.py
│   │   │   │   └── nuclei_repo_repository.py
│   │   │   ├── routing.py
│   │   │   ├── scheduler.py
│   │   │   ├── serializers/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── engine_serializer.py
│   │   │   │   ├── fingerprints/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── arl.py
│   │   │   │   │   ├── ehole.py
│   │   │   │   │   ├── fingerprinthub.py
│   │   │   │   │   ├── fingers.py
│   │   │   │   │   ├── goby.py
│   │   │   │   │   └── wappalyzer.py
│   │   │   │   ├── nuclei_template_repo_serializer.py
│   │   │   │   ├── wordlist_serializer.py
│   │   │   │   └── worker_serializer.py
│   │   │   ├── services/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── deploy_service.py
│   │   │   │   ├── engine_service.py
│   │   │   │   ├── fingerprints/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── arl_service.py
│   │   │   │   │   ├── base.py
│   │   │   │   │   ├── ehole.py
│   │   │   │   │   ├── fingerprinthub_service.py
│   │   │   │   │   ├── fingers_service.py
│   │   │   │   │   ├── goby.py
│   │   │   │   │   └── wappalyzer.py
│   │   │   │   ├── nuclei_template_repo_service.py
│   │   │   │   ├── task_distributor.py
│   │   │   │   ├── wordlist_service.py
│   │   │   │   ├── worker_load_service.py
│   │   │   │   └── worker_service.py
│   │   │   ├── urls.py
│   │   │   └── views/
│   │   │       ├── __init__.py
│   │   │       ├── engine_views.py
│   │   │       ├── fingerprints/
│   │   │       │   ├── __init__.py
│   │   │       │   ├── arl.py
│   │   │       │   ├── base.py
│   │   │       │   ├── ehole.py
│   │   │       │   ├── fingerprinthub.py
│   │   │       │   ├── fingers.py
│   │   │       │   ├── goby.py
│   │   │       │   └── wappalyzer.py
│   │   │       ├── nuclei_template_repo_views.py
│   │   │       ├── wordlist_views.py
│   │   │       └── worker_views.py
│   │   ├── scan/
│   │   │   ├── __init__.py
│   │   │   ├── apps.py
│   │   │   ├── configs/
│   │   │   │   ├── command_templates.py
│   │   │   │   └── engine_config_example.yaml
│   │   │   ├── flows/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── directory_scan_flow.py
│   │   │   │   ├── fingerprint_detect_flow.py
│   │   │   │   ├── initiate_scan_flow.py
│   │   │   │   ├── port_scan_flow.py
│   │   │   │   ├── screenshot_flow.py
│   │   │   │   ├── site_scan_flow.py
│   │   │   │   ├── subdomain_discovery_flow.py
│   │   │   │   ├── url_fetch/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── domain_name_url_fetch_flow.py
│   │   │   │   │   ├── main_flow.py
│   │   │   │   │   ├── sites_url_fetch_flow.py
│   │   │   │   │   └── utils.py
│   │   │   │   └── vuln_scan/
│   │   │   │       ├── __init__.py
│   │   │   │       ├── endpoints_vuln_scan_flow.py
│   │   │   │       ├── main_flow.py
│   │   │   │       └── utils.py
│   │   │   ├── handlers/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── initiate_scan_flow_handlers.py
│   │   │   │   └── scan_flow_handlers.py
│   │   │   ├── management/
│   │   │   │   └── __init__.py
│   │   │   ├── migrations/
│   │   │   │   ├── 0001_initial.py
│   │   │   │   ├── 0002_add_cached_screenshots_count.py
│   │   │   │   ├── 0003_add_wecom_fields.py
│   │   │   │   └── __init__.py
│   │   │   ├── models/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── scan_log_model.py
│   │   │   │   ├── scan_models.py
│   │   │   │   ├── scheduled_scan_model.py
│   │   │   │   └── subfinder_provider_settings_model.py
│   │   │   ├── notifications/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── consumers.py
│   │   │   │   ├── models.py
│   │   │   │   ├── receivers.py
│   │   │   │   ├── repositories.py
│   │   │   │   ├── routing.py
│   │   │   │   ├── serializers.py
│   │   │   │   ├── services.py
│   │   │   │   ├── types.py
│   │   │   │   ├── urls.py
│   │   │   │   └── views.py
│   │   │   ├── orchestrators/
│   │   │   │   ├── __init__.py
│   │   │   │   └── flow_orchestrator.py
│   │   │   ├── providers/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── base.py
│   │   │   │   ├── database_provider.py
│   │   │   │   ├── list_provider.py
│   │   │   │   ├── pipeline_provider.py
│   │   │   │   ├── snapshot_provider.py
│   │   │   │   └── tests/
│   │   │   │       ├── __init__.py
│   │   │   │       ├── test_common_properties.py
│   │   │   │       ├── test_database_provider.py
│   │   │   │       ├── test_list_provider.py
│   │   │   │       ├── test_pipeline_provider.py
│   │   │   │       └── test_snapshot_provider.py
│   │   │   ├── repositories/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── django_scan_repository.py
│   │   │   │   └── scheduled_scan_repository.py
│   │   │   ├── scripts/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── run_cleanup.py
│   │   │   │   ├── run_delete_scans.py
│   │   │   │   ├── run_initiate_scan.py
│   │   │   │   └── run_scheduled_scan.py
│   │   │   ├── serializers/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── mixins.py
│   │   │   │   ├── scan_log_serializers.py
│   │   │   │   ├── scan_serializers.py
│   │   │   │   ├── scheduled_scan_serializers.py
│   │   │   │   └── subfinder_provider_settings_serializers.py
│   │   │   ├── services/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── quick_scan_service.py
│   │   │   │   ├── scan_control_service.py
│   │   │   │   ├── scan_creation_service.py
│   │   │   │   ├── scan_service.py
│   │   │   │   ├── scan_state_service.py
│   │   │   │   ├── scan_stats_service.py
│   │   │   │   ├── scheduled_scan_service.py
│   │   │   │   ├── subfinder_provider_config_service.py
│   │   │   │   └── target_export_service.py
│   │   │   ├── tasks/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── directory_scan/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── export_sites_task.py
│   │   │   │   │   └── run_and_stream_save_directories_task.py
│   │   │   │   ├── fingerprint_detect/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── export_urls_task.py
│   │   │   │   │   └── run_xingfinger_task.py
│   │   │   │   ├── port_scan/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── export_hosts_task.py
│   │   │   │   │   ├── run_and_stream_save_ports_task.py
│   │   │   │   │   └── types.py
│   │   │   │   ├── screenshot/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   └── capture_screenshots_task.py
│   │   │   │   ├── site_scan/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── export_site_urls_task.py
│   │   │   │   │   └── run_and_stream_save_websites_task.py
│   │   │   │   ├── subdomain_discovery/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── merge_and_validate_task.py
│   │   │   │   │   ├── run_subdomain_discovery_task.py
│   │   │   │   │   └── save_domains_task.py
│   │   │   │   ├── tests/
│   │   │   │   │   └── test_task_backward_compatibility.py
│   │   │   │   ├── url_fetch/
│   │   │   │   │   ├── __init__.py
│   │   │   │   │   ├── clean_urls_task.py
│   │   │   │   │   ├── export_sites_task.py
│   │   │   │   │   ├── merge_and_deduplicate_urls_task.py
│   │   │   │   │   ├── run_and_stream_save_urls_task.py
│   │   │   │   │   ├── run_url_fetcher_task.py
│   │   │   │   │   └── save_urls_task.py
│   │   │   │   └── vuln_scan/
│   │   │   │       ├── __init__.py
│   │   │   │       ├── export_endpoints_task.py
│   │   │   │       ├── run_and_stream_save_dalfox_vulns_task.py
│   │   │   │       ├── run_and_stream_save_nuclei_vulns_task.py
│   │   │   │       └── run_vuln_tool_task.py
│   │   │   ├── urls.py
│   │   │   ├── utils/
│   │   │   │   ├── __init__.py
│   │   │   │   ├── command_builder.py
│   │   │   │   ├── command_executor.py
│   │   │   │   ├── config_merger.py
│   │   │   │   ├── config_parser.py
│   │   │   │   ├── directory_cleanup.py
│   │   │   │   ├── fingerprint_helpers.py
│   │   │   │   ├── nuclei_helpers.py
│   │   │   │   ├── performance.py
│   │   │   │   ├── system_load.py
│   │   │   │   ├── user_logger.py
│   │   │   │   ├── wordlist_helpers.py
│   │   │   │   └── workspace_utils.py
│   │   │   └── views/
│   │   │       ├── __init__.py
│   │   │       ├── scan_log_views.py
│   │   │       ├── scan_views.py
│   │   │       ├── scheduled_scan_views.py
│   │   │       └── subfinder_provider_settings_views.py
│   │   └── targets/
│   │       ├── __init__.py
│   │       ├── apps.py
│   │       ├── migrations/
│   │       │   ├── 0001_initial.py
│   │       │   └── __init__.py
│   │       ├── models.py
│   │       ├── repositories/
│   │       │   ├── __init__.py
│   │       │   ├── django_organization_repository.py
│   │       │   └── django_target_repository.py
│   │       ├── scripts/
│   │       │   ├── __init__.py
│   │       │   ├── run_delete_organizations.py
│   │       │   └── run_delete_targets.py
│   │       ├── serializers.py
│   │       ├── services/
│   │       │   ├── __init__.py
│   │       │   ├── organization_service.py
│   │       │   └── target_service.py
│   │       ├── urls.py
│   │       └── views.py
│   ├── config/
│   │   ├── __init__.py
│   │   ├── asgi.py
│   │   ├── logging_config.py
│   │   ├── settings.py
│   │   └── urls.py
│   ├── fingerprints/
│   │   ├── ARL.yaml
│   │   ├── ehole.json
│   │   ├── fingerprinthub_web.json
│   │   ├── fingers_http.json
│   │   ├── goby.json
│   │   └── wappalyzer.json
│   ├── manage.py
│   ├── pyproject.toml
│   ├── requirements.txt
│   ├── resources/
│   │   └── resolvers.txt
│   ├── scripts/
│   │   ├── generate_test_data_sql.py
│   │   ├── performance/
│   │   │   ├── monitor_pg_performance.sh
│   │   │   ├── pg_stats_after_test.sql
│   │   │   ├── pg_stats_before_test.sql
│   │   │   └── start_performance_test.sh
│   │   └── worker-deploy/
│   │       ├── agent.sh
│   │       ├── bootstrap.sh
│   │       ├── install.sh
│   │       ├── start-agent.sh
│   │       └── uninstall.sh
│   └── wordlist/
│       ├── dir_default.txt
│       └── subdomains-top1million-110000.txt
├── docker/
│   ├── agent/
│   │   └── Dockerfile
│   ├── docker-compose.dev.yml
│   ├── docker-compose.yml
│   ├── frontend/
│   │   └── Dockerfile
│   ├── nginx/
│   │   ├── Dockerfile
│   │   ├── nginx.conf
│   │   └── ssl/
│   │       └── README.md
│   ├── postgres/
│   │   ├── Dockerfile
│   │   └── init-user-db.sh
│   ├── restart.sh
│   ├── scripts/
│   │   ├── common.sh
│   │   ├── init-data.sh
│   │   ├── install-pg-ivm.sh
│   │   ├── setup-swap.sh
│   │   ├── setup-system-monitor.sh
│   │   └── test-pg-ivm.sh
│   ├── server/
│   │   ├── Dockerfile
│   │   └── start.sh
│   ├── start.sh
│   ├── stop.sh
│   └── worker/
│       └── Dockerfile
├── docker-push.sh
├── docs/
│   ├── README.md
│   ├── nuclei-template-architecture.md
│   ├── plans/
│   │   ├── 2026-03-08-oss-readiness-design.md
│   │   └── 2026-03-08-oss-readiness.md
│   ├── quick-start.md
│   ├── scan-flow-architecture.md
│   ├── version-management.md
│   └── wordlist-architecture.md
├── frontend/
│   ├── .gitignore
│   ├── app/
│   │   ├── [locale]/
│   │   │   ├── dashboard/
│   │   │   │   ├── data.json
│   │   │   │   └── page.tsx
│   │   │   ├── layout.tsx
│   │   │   ├── login/
│   │   │   │   ├── layout.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── organization/
│   │   │   │   ├── [id]/
│   │   │   │   │   └── page.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── page.tsx
│   │   │   ├── scan/
│   │   │   │   ├── engine/
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── history/
│   │   │   │   │   ├── [id]/
│   │   │   │   │   │   ├── directories/
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   ├── endpoints/
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   ├── ip-addresses/
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   ├── layout.tsx
│   │   │   │   │   │   ├── overview/
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   ├── page.tsx
│   │   │   │   │   │   ├── screenshots/
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   ├── subdomain/
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   ├── vulnerabilities/
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   └── websites/
│   │   │   │   │   │       └── page.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   └── scheduled/
│   │   │   │       └── page.tsx
│   │   │   ├── search/
│   │   │   │   └── page.tsx
│   │   │   ├── settings/
│   │   │   │   ├── api-keys/
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── blacklist/
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── notifications/
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── system-logs/
│   │   │   │   │   └── page.tsx
│   │   │   │   └── workers/
│   │   │   │       └── page.tsx
│   │   │   ├── target/
│   │   │   │   ├── [id]/
│   │   │   │   │   ├── details/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── directories/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── endpoints/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── ip-addresses/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── layout.tsx
│   │   │   │   │   ├── overview/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── page.tsx
│   │   │   │   │   ├── screenshots/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── settings/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── subdomain/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── vulnerabilities/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   └── websites/
│   │   │   │   │       └── page.tsx
│   │   │   │   └── page.tsx
│   │   │   ├── tools/
│   │   │   │   ├── config/
│   │   │   │   │   ├── custom/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── opensource/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── fingerprints/
│   │   │   │   │   ├── arl/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── ehole/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── fingerprinthub/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── fingers/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── goby/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   ├── layout.tsx
│   │   │   │   │   ├── page.tsx
│   │   │   │   │   └── wappalyzer/
│   │   │   │   │       └── page.tsx
│   │   │   │   ├── nuclei/
│   │   │   │   │   ├── [repoId]/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   └── page.tsx
│   │   │   │   ├── page.tsx
│   │   │   │   └── wordlists/
│   │   │   │       └── page.tsx
│   │   │   └── vulnerabilities/
│   │   │       └── page.tsx
│   │   ├── globals.css
│   │   └── layout.tsx
│   ├── components/
│   │   ├── about-dialog.tsx
│   │   ├── app-sidebar.tsx
│   │   ├── auth/
│   │   │   ├── auth-guard.tsx
│   │   │   ├── auth-layout.tsx
│   │   │   ├── change-password-dialog.tsx
│   │   │   └── index.ts
│   │   ├── color-theme-switcher.tsx
│   │   ├── common/
│   │   │   ├── bulk-add-urls-dialog.tsx
│   │   │   └── smart-filter-input.tsx
│   │   ├── dashboard/
│   │   │   ├── asset-distribution-chart.tsx
│   │   │   ├── asset-trend-chart.tsx
│   │   │   ├── dashboard-activity-tabs.tsx
│   │   │   ├── dashboard-data-table.tsx
│   │   │   ├── dashboard-scan-history.tsx
│   │   │   ├── dashboard-scheduled-scans.tsx
│   │   │   ├── dashboard-stat-cards.tsx
│   │   │   ├── recent-vulnerabilities.tsx
│   │   │   └── vuln-severity-chart.tsx
│   │   ├── directories/
│   │   │   ├── directories-columns.tsx
│   │   │   ├── directories-data-table.tsx
│   │   │   └── directories-view.tsx
│   │   ├── disk/
│   │   │   └── disk-stat-cards.tsx
│   │   ├── endpoints/
│   │   │   ├── endpoints-columns.tsx
│   │   │   ├── endpoints-data-table.tsx
│   │   │   ├── endpoints-detail-view.tsx
│   │   │   └── index.ts
│   │   ├── fingerprints/
│   │   │   ├── arl-fingerprint-columns.tsx
│   │   │   ├── arl-fingerprint-data-table.tsx
│   │   │   ├── arl-fingerprint-dialog.tsx
│   │   │   ├── arl-fingerprint-view.tsx
│   │   │   ├── ehole-fingerprint-columns.tsx
│   │   │   ├── ehole-fingerprint-data-table.tsx
│   │   │   ├── ehole-fingerprint-dialog.tsx
│   │   │   ├── ehole-fingerprint-view.tsx
│   │   │   ├── fingerprinthub-fingerprint-columns.tsx
│   │   │   ├── fingerprinthub-fingerprint-data-table.tsx
│   │   │   ├── fingerprinthub-fingerprint-dialog.tsx
│   │   │   ├── fingerprinthub-fingerprint-view.tsx
│   │   │   ├── fingers-fingerprint-columns.tsx
│   │   │   ├── fingers-fingerprint-data-table.tsx
│   │   │   ├── fingers-fingerprint-dialog.tsx
│   │   │   ├── fingers-fingerprint-view.tsx
│   │   │   ├── goby-fingerprint-columns.tsx
│   │   │   ├── goby-fingerprint-data-table.tsx
│   │   │   ├── goby-fingerprint-dialog.tsx
│   │   │   ├── goby-fingerprint-view.tsx
│   │   │   ├── import-fingerprint-dialog.tsx
│   │   │   ├── index.ts
│   │   │   ├── wappalyzer-fingerprint-columns.tsx
│   │   │   ├── wappalyzer-fingerprint-data-table.tsx
│   │   │   ├── wappalyzer-fingerprint-dialog.tsx
│   │   │   └── wappalyzer-fingerprint-view.tsx
│   │   ├── ip-addresses/
│   │   │   ├── index.ts
│   │   │   ├── ip-addresses-columns.tsx
│   │   │   ├── ip-addresses-data-table.tsx
│   │   │   └── ip-addresses-view.tsx
│   │   ├── language-switcher.tsx
│   │   ├── loading-spinner.tsx
│   │   ├── nav-secondary.tsx
│   │   ├── nav-system.tsx
│   │   ├── nav-user.tsx
│   │   ├── notifications/
│   │   │   ├── index.ts
│   │   │   └── notification-drawer.tsx
│   │   ├── organization/
│   │   │   ├── add-organization-dialog.tsx
│   │   │   ├── edit-organization-dialog.tsx
│   │   │   ├── index.ts
│   │   │   ├── organization-columns.tsx
│   │   │   ├── organization-data-table.tsx
│   │   │   ├── organization-detail-view.tsx
│   │   │   ├── organization-list.tsx
│   │   │   └── targets/
│   │   │       ├── add-target-dialog.tsx
│   │   │       ├── index.ts
│   │   │       ├── link-target-dialog.tsx
│   │   │       ├── targets-columns.tsx
│   │   │       ├── targets-data-table.tsx
│   │   │       └── targets-detail-view.tsx
│   │   ├── providers/
│   │   │   ├── index.ts
│   │   │   ├── query-provider.tsx
│   │   │   ├── theme-provider.tsx
│   │   │   └── ui-i18n-provider.tsx
│   │   ├── route-prefetch.tsx
│   │   ├── route-progress.tsx
│   │   ├── scan/
│   │   │   ├── engine/
│   │   │   │   ├── engine-columns.tsx
│   │   │   │   ├── engine-create-dialog.tsx
│   │   │   │   ├── engine-data-table.tsx
│   │   │   │   ├── engine-edit-dialog.tsx
│   │   │   │   └── index.ts
│   │   │   ├── engine-preset-selector.tsx
│   │   │   ├── history/
│   │   │   │   ├── index.ts
│   │   │   │   ├── scan-history-columns.tsx
│   │   │   │   ├── scan-history-data-table.tsx
│   │   │   │   ├── scan-history-list.tsx
│   │   │   │   ├── scan-history-stat-cards.tsx
│   │   │   │   └── scan-overview.tsx
│   │   │   ├── initiate-scan-dialog.tsx
│   │   │   ├── quick-scan-dialog.tsx
│   │   │   ├── scan-config-editor.tsx
│   │   │   ├── scan-log-list.tsx
│   │   │   ├── scan-progress-dialog.tsx
│   │   │   └── scheduled/
│   │   │       ├── create-scheduled-scan-dialog.tsx
│   │   │       ├── edit-scheduled-scan-dialog.tsx
│   │   │       ├── index.ts
│   │   │       ├── scheduled-scan-columns.tsx
│   │   │       └── scheduled-scan-data-table.tsx
│   │   ├── screenshots/
│   │   │   └── screenshots-gallery.tsx
│   │   ├── search/
│   │   │   ├── index.ts
│   │   │   ├── search-page.tsx
│   │   │   ├── search-pagination.tsx
│   │   │   ├── search-result-card.tsx
│   │   │   └── search-results-table.tsx
│   │   ├── settings/
│   │   │   ├── system-logs/
│   │   │   │   ├── ansi-log-viewer.tsx
│   │   │   │   ├── index.ts
│   │   │   │   ├── log-toolbar.tsx
│   │   │   │   └── system-logs-view.tsx
│   │   │   └── workers/
│   │   │       ├── deploy-terminal-dialog.tsx
│   │   │       ├── index.ts
│   │   │       ├── worker-dialog.tsx
│   │   │       └── worker-list.tsx
│   │   ├── site-header.tsx
│   │   ├── subdomains/
│   │   │   ├── bulk-add-subdomains-dialog.tsx
│   │   │   ├── index.ts
│   │   │   ├── subdomains-columns.tsx
│   │   │   ├── subdomains-data-table.tsx
│   │   │   └── subdomains-detail-view.tsx
│   │   ├── target/
│   │   │   ├── add-target-dialog.tsx
│   │   │   ├── all-targets-columns.tsx
│   │   │   ├── all-targets-detail-view.tsx
│   │   │   ├── index.ts
│   │   │   ├── target-overview.tsx
│   │   │   ├── target-settings.tsx
│   │   │   └── targets-data-table.tsx
│   │   ├── theme-toggle.tsx
│   │   ├── tools/
│   │   │   ├── commands/
│   │   │   │   ├── commands-columns.tsx
│   │   │   │   ├── commands-data-table.tsx
│   │   │   │   └── index.ts
│   │   │   ├── config/
│   │   │   │   ├── add-custom-tool-dialog.tsx
│   │   │   │   ├── add-tool-dialog.tsx
│   │   │   │   ├── custom-tools-list.tsx
│   │   │   │   ├── index.ts
│   │   │   │   ├── opensource-tools-list.tsx
│   │   │   │   └── tool-card.tsx
│   │   │   ├── index.ts
│   │   │   ├── wordlist-edit-dialog.tsx
│   │   │   └── wordlist-upload-dialog.tsx
│   │   ├── ui/
│   │   │   ├── alert-dialog.tsx
│   │   │   ├── alert.tsx
│   │   │   ├── avatar.tsx
│   │   │   ├── badge.tsx
│   │   │   ├── button.tsx
│   │   │   ├── calendar.tsx
│   │   │   ├── card-grid-skeleton.tsx
│   │   │   ├── card.tsx
│   │   │   ├── chart.tsx
│   │   │   ├── checkbox.tsx
│   │   │   ├── collapsible.tsx
│   │   │   ├── command.tsx
│   │   │   ├── confirm-dialog.tsx
│   │   │   ├── copyable-popover-content.tsx
│   │   │   ├── data-table/
│   │   │   │   ├── column-header.tsx
│   │   │   │   ├── column-resizer.tsx
│   │   │   │   ├── expandable-cell.tsx
│   │   │   │   ├── index.ts
│   │   │   │   ├── pagination.tsx
│   │   │   │   ├── toolbar.tsx
│   │   │   │   └── unified-data-table.tsx
│   │   │   ├── data-table-skeleton.tsx
│   │   │   ├── datetime-picker.tsx
│   │   │   ├── dialog.tsx
│   │   │   ├── drawer.tsx
│   │   │   ├── dropdown-menu.tsx
│   │   │   ├── dropzone.tsx
│   │   │   ├── field.tsx
│   │   │   ├── form.tsx
│   │   │   ├── hover-card.tsx
│   │   │   ├── input.tsx
│   │   │   ├── label.tsx
│   │   │   ├── master-detail-skeleton.tsx
│   │   │   ├── popover.tsx
│   │   │   ├── progress.tsx
│   │   │   ├── radio-group.tsx
│   │   │   ├── scroll-area.tsx
│   │   │   ├── select.tsx
│   │   │   ├── separator.tsx
│   │   │   ├── shadcn-io/
│   │   │   │   ├── banner/
│   │   │   │   │   └── index.tsx
│   │   │   │   └── status/
│   │   │   │       └── index.tsx
│   │   │   ├── sheet.tsx
│   │   │   ├── sidebar.tsx
│   │   │   ├── skeleton.tsx
│   │   │   ├── sonner.tsx
│   │   │   ├── spinner.tsx
│   │   │   ├── switch.tsx
│   │   │   ├── table.tsx
│   │   │   ├── tabs.tsx
│   │   │   ├── textarea.tsx
│   │   │   ├── toggle-group.tsx
│   │   │   ├── toggle.tsx
│   │   │   ├── tooltip.tsx
│   │   │   └── yaml-editor.tsx
│   │   ├── vulnerabilities/
│   │   │   ├── index.ts
│   │   │   ├── vulnerabilities-columns.tsx
│   │   │   ├── vulnerabilities-data-table.tsx
│   │   │   ├── vulnerabilities-detail-view.tsx
│   │   │   └── vulnerability-detail-dialog.tsx
│   │   └── websites/
│   │       ├── websites-columns.tsx
│   │       ├── websites-data-table.tsx
│   │       └── websites-view.tsx
│   ├── components.json
│   ├── eslint.config.mjs
│   ├── hooks/
│   │   ├── use-api-key-settings.ts
│   │   ├── use-auth.ts
│   │   ├── use-color-theme.ts
│   │   ├── use-commands.ts
│   │   ├── use-dashboard.ts
│   │   ├── use-directories.ts
│   │   ├── use-disk.ts
│   │   ├── use-endpoints.ts
│   │   ├── use-engines.ts
│   │   ├── use-fingerprints.ts
│   │   ├── use-global-blacklist.ts
│   │   ├── use-ip-addresses.ts
│   │   ├── use-mobile.ts
│   │   ├── use-notification-settings.ts
│   │   ├── use-notification-sse.ts
│   │   ├── use-notifications.ts
│   │   ├── use-nuclei-git-settings.ts
│   │   ├── use-nuclei-repos.ts
│   │   ├── use-nuclei-templates.ts
│   │   ├── use-organizations.ts
│   │   ├── use-route-prefetch.ts
│   │   ├── use-scan-logs.ts
│   │   ├── use-scans.ts
│   │   ├── use-scheduled-scans.ts
│   │   ├── use-screenshots.ts
│   │   ├── use-search.ts
│   │   ├── use-step.ts
│   │   ├── use-subdomains.ts
│   │   ├── use-system-logs.ts
│   │   ├── use-targets.ts
│   │   ├── use-tools.ts
│   │   ├── use-version.ts
│   │   ├── use-vulnerabilities.ts
│   │   ├── use-websites.ts
│   │   ├── use-wordlists.ts
│   │   └── use-workers.ts
│   ├── i18n/
│   │   ├── config.ts
│   │   ├── navigation.ts
│   │   └── request.ts
│   ├── lib/
│   │   ├── api-client.ts
│   │   ├── date-utils.ts
│   │   ├── domain-validator.ts
│   │   ├── endpoint-validator.ts
│   │   ├── engine-config.ts
│   │   ├── env.ts
│   │   ├── error-code-map.ts
│   │   ├── error-handler.ts
│   │   ├── i18n-format.ts
│   │   ├── response-parser.ts
│   │   ├── subdomain-validator.ts
│   │   ├── table-utils.ts
│   │   ├── target-validator.ts
│   │   ├── toast-helpers.ts
│   │   ├── url-validator.ts
│   │   └── utils.ts
│   ├── messages/
│   │   ├── en.json
│   │   └── zh.json
│   ├── middleware.ts
│   ├── mock/
│   │   ├── config.ts
│   │   ├── data/
│   │   │   ├── auth.ts
│   │   │   ├── blacklist.ts
│   │   │   ├── dashboard.ts
│   │   │   ├── directories.ts
│   │   │   ├── endpoints.ts
│   │   │   ├── engines.ts
│   │   │   ├── fingerprints.ts
│   │   │   ├── ip-addresses.ts
│   │   │   ├── notification-settings.ts
│   │   │   ├── notifications.ts
│   │   │   ├── nuclei-templates.ts
│   │   │   ├── organizations.ts
│   │   │   ├── scans.ts
│   │   │   ├── scheduled-scans.ts
│   │   │   ├── search.ts
│   │   │   ├── subdomains.ts
│   │   │   ├── system-logs.ts
│   │   │   ├── targets.ts
│   │   │   ├── tools.ts
│   │   │   ├── vulnerabilities.ts
│   │   │   ├── websites.ts
│   │   │   ├── wordlists.ts
│   │   │   └── workers.ts
│   │   └── index.ts
│   ├── next.config.ts
│   ├── package.json
│   ├── postcss.config.mjs
│   ├── public/
│   │   ├── animations/
│   │   │   └── Security000-Purple.json
│   │   └── mockServiceWorker.js
│   ├── services/
│   │   ├── api-key-settings.service.ts
│   │   ├── auth.service.ts
│   │   ├── command.service.ts
│   │   ├── dashboard.service.ts
│   │   ├── directory.service.ts
│   │   ├── disk.service.ts
│   │   ├── endpoint.service.ts
│   │   ├── engine.service.ts
│   │   ├── fingerprint.service.ts
│   │   ├── global-blacklist.service.ts
│   │   ├── ip-address.service.ts
│   │   ├── notification-settings.service.ts
│   │   ├── notification.service.ts
│   │   ├── nuclei-git.service.ts
│   │   ├── nuclei-repo.api.ts
│   │   ├── nuclei.service.ts
│   │   ├── organization.service.ts
│   │   ├── scan.service.ts
│   │   ├── scheduled-scan.service.ts
│   │   ├── screenshot.service.ts
│   │   ├── search.service.ts
│   │   ├── subdomain.service.ts
│   │   ├── system-log.service.ts
│   │   ├── target.service.ts
│   │   ├── tool.service.ts
│   │   ├── version.service.ts
│   │   ├── vulnerability.service.ts
│   │   ├── website.service.ts
│   │   ├── wordlist.service.ts
│   │   └── worker.service.ts
│   ├── styles/
│   │   └── themes/
│   │       ├── bubblegum.css
│   │       ├── clean-slate.css
│   │       ├── cosmic-night.css
│   │       ├── cyberpunk-1.css
│   │       ├── eva-01.css
│   │       ├── index.css
│   │       ├── quantum-rose.css
│   │       ├── vercel-dark.css
│   │       ├── vercel.css
│   │       └── violet-bloom.css
│   ├── tsconfig.json
│   ├── types/
│   │   ├── api-key-settings.types.ts
│   │   ├── api-response.types.ts
│   │   ├── auth.types.ts
│   │   ├── command.types.ts
│   │   ├── common.types.ts
│   │   ├── dashboard.types.ts
│   │   ├── data-table.types.ts
│   │   ├── directory.types.ts
│   │   ├── disk.types.ts
│   │   ├── endpoint.types.ts
│   │   ├── engine.types.ts
│   │   ├── fingerprint.types.ts
│   │   ├── ip-address.types.ts
│   │   ├── notification-settings.types.ts
│   │   ├── notification.types.ts
│   │   ├── nuclei-git.types.ts
│   │   ├── nuclei.types.ts
│   │   ├── organization.types.ts
│   │   ├── psl.d.ts
│   │   ├── scan.types.ts
│   │   ├── scheduled-scan.types.ts
│   │   ├── search.types.ts
│   │   ├── subdomain.types.ts
│   │   ├── system-log.types.ts
│   │   ├── target.types.ts
│   │   ├── tool.types.ts
│   │   ├── version.types.ts
│   │   ├── vulnerability.types.ts
│   │   ├── website.types.ts
│   │   ├── wordlist.types.ts
│   │   └── worker.types.ts
│   └── vercel.json
├── install.sh
├── restart.sh
├── start.sh
├── stop.sh
├── uninstall.sh
└── update.sh
Download .txt
Showing preview only (288K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (3062 symbols across 625 files)

FILE: backend/apps/asset/apps.py
  class AssetConfig (line 4) | class AssetConfig(AppConfig):

FILE: backend/apps/asset/dtos/asset/directory_dto.py
  class DirectoryDTO (line 8) | class DirectoryDTO:

FILE: backend/apps/asset/dtos/asset/endpoint_dto.py
  class EndpointDTO (line 8) | class EndpointDTO:
    method __post_init__ (line 25) | def __post_init__(self):

FILE: backend/apps/asset/dtos/asset/host_port_mapping_dto.py
  class HostPortMappingDTO (line 7) | class HostPortMappingDTO:

FILE: backend/apps/asset/dtos/asset/ip_address_dto.py
  class IPAddressDTO (line 7) | class IPAddressDTO:

FILE: backend/apps/asset/dtos/asset/port_dto.py
  class PortDTO (line 7) | class PortDTO:

FILE: backend/apps/asset/dtos/asset/subdomain_dto.py
  class SubdomainDTO (line 7) | class SubdomainDTO:

FILE: backend/apps/asset/dtos/asset/vulnerability_dto.py
  class VulnerabilityDTO (line 9) | class VulnerabilityDTO:

FILE: backend/apps/asset/dtos/asset/website_dto.py
  class WebSiteDTO (line 8) | class WebSiteDTO:
    method __post_init__ (line 25) | def __post_init__(self):

FILE: backend/apps/asset/dtos/snapshot/directory_snapshot_dto.py
  class DirectorySnapshotDTO (line 9) | class DirectorySnapshotDTO:
    method to_asset_dto (line 28) | def to_asset_dto(self) -> DirectoryDTO:

FILE: backend/apps/asset/dtos/snapshot/endpoint_snapshot_dto.py
  class EndpointSnapshotDTO (line 8) | class EndpointSnapshotDTO:
    method __post_init__ (line 31) | def __post_init__(self):
    method to_asset_dto (line 37) | def to_asset_dto(self):

FILE: backend/apps/asset/dtos/snapshot/host_port_mapping_snapshot_dto.py
  class HostPortMappingSnapshotDTO (line 8) | class HostPortMappingSnapshotDTO:
    method to_asset_dto (line 16) | def to_asset_dto(self):

FILE: backend/apps/asset/dtos/snapshot/subdomain_snapshot_dto.py
  class SubdomainSnapshotDTO (line 11) | class SubdomainSnapshotDTO:
    method to_asset_dto (line 22) | def to_asset_dto(self) -> 'SubdomainDTO':

FILE: backend/apps/asset/dtos/snapshot/vulnerability_snapshot_dto.py
  class VulnerabilitySnapshotDTO (line 9) | class VulnerabilitySnapshotDTO:
    method to_asset_dto (line 29) | def to_asset_dto(self):

FILE: backend/apps/asset/dtos/snapshot/website_snapshot_dto.py
  class WebsiteSnapshotDTO (line 8) | class WebsiteSnapshotDTO:
    method __post_init__ (line 30) | def __post_init__(self):
    method to_asset_dto (line 34) | def to_asset_dto(self):

FILE: backend/apps/asset/migrations/0001_initial.py
  class Migration (line 10) | class Migration(migrations.Migration):

FILE: backend/apps/asset/migrations/0002_create_search_views.py
  class Migration (line 10) | class Migration(migrations.Migration):

FILE: backend/apps/asset/migrations/0003_add_screenshot_models.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: backend/apps/asset/migrations/0004_add_status_code_to_screenshot.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: backend/apps/asset/models/asset_models.py
  class Subdomain (line 8) | class Subdomain(models.Model):
    class Meta (line 28) | class Meta:
    method __str__ (line 53) | def __str__(self):
  class Endpoint (line 57) | class Endpoint(models.Model):
    class Meta (line 134) | class Meta:
    method __str__ (line 172) | def __str__(self):
  class WebSite (line 176) | class WebSite(models.Model):
    class Meta (line 247) | class Meta:
    method __str__ (line 285) | def __str__(self):
  class Directory (line 289) | class Directory(models.Model):
    class Meta (line 342) | class Meta:
    method __str__ (line 367) | def __str__(self):
  class HostPortMapping (line 371) | class HostPortMapping(models.Model):
    class Meta (line 416) | class Meta:
    method __str__ (line 437) | def __str__(self):
  class Vulnerability (line 441) | class Vulnerability(models.Model):
    class Meta (line 477) | class Meta:
    method __str__ (line 491) | def __str__(self):

FILE: backend/apps/asset/models/screenshot_models.py
  class ScreenshotSnapshot (line 4) | class ScreenshotSnapshot(models.Model):
    class Meta (line 23) | class Meta:
    method __str__ (line 39) | def __str__(self):
  class Screenshot (line 43) | class Screenshot(models.Model):
    class Meta (line 63) | class Meta:
    method __str__ (line 79) | def __str__(self):

FILE: backend/apps/asset/models/snapshot_models.py
  class SubdomainSnapshot (line 7) | class SubdomainSnapshot(models.Model):
    class Meta (line 21) | class Meta:
    method __str__ (line 45) | def __str__(self):
  class WebsiteSnapshot (line 48) | class WebsiteSnapshot(models.Model):
    class Meta (line 87) | class Meta:
    method __str__ (line 124) | def __str__(self):
  class DirectorySnapshot (line 128) | class DirectorySnapshot(models.Model):
    class Meta (line 153) | class Meta:
    method __str__ (line 179) | def __str__(self):
  class HostPortMappingSnapshot (line 183) | class HostPortMappingSnapshot(models.Model):
    class Meta (line 228) | class Meta:
    method __str__ (line 250) | def __str__(self):
  class EndpointSnapshot (line 254) | class EndpointSnapshot(models.Model):
    class Meta (line 304) | class Meta:
    method __str__ (line 343) | def __str__(self):
  class VulnerabilitySnapshot (line 347) | class VulnerabilitySnapshot(models.Model):
    class Meta (line 382) | class Meta:
    method __str__ (line 396) | def __str__(self):

FILE: backend/apps/asset/models/statistics_models.py
  class AssetStatistics (line 4) | class AssetStatistics(models.Model):
    class Meta (line 35) | class Meta:
    method __str__ (line 40) | def __str__(self):
    method get_or_create_singleton (line 44) | def get_or_create_singleton(cls) -> 'AssetStatistics':
  class StatisticsHistory (line 50) | class StatisticsHistory(models.Model):
    class Meta (line 72) | class Meta:
    method __str__ (line 81) | def __str__(self):

FILE: backend/apps/asset/repositories/asset/directory_repository.py
  class DjangoDirectoryRepository (line 18) | class DjangoDirectoryRepository:
    method bulk_upsert (line 21) | def bulk_upsert(self, items: List[DirectoryDTO]) -> int:
    method bulk_create_ignore_conflicts (line 77) | def bulk_create_ignore_conflicts(self, items: List[DirectoryDTO]) -> int:
    method count_by_target (line 127) | def count_by_target(self, target_id: int) -> int:
    method get_all (line 131) | def get_all(self):
    method get_by_target (line 135) | def get_by_target(self, target_id: int):
    method get_urls_for_export (line 139) | def get_urls_for_export(self, target_id: int, batch_size: int = 1000) ...
    method iter_raw_data_for_export (line 155) | def iter_raw_data_for_export(

FILE: backend/apps/asset/repositories/asset/endpoint_repository.py
  class DjangoEndpointRepository (line 16) | class DjangoEndpointRepository:
    method bulk_upsert (line 19) | def bulk_upsert(self, items: List[EndpointDTO]) -> int:
    method get_all (line 82) | def get_all(self):
    method get_by_target (line 86) | def get_by_target(self, target_id: int):
    method count_by_target (line 98) | def count_by_target(self, target_id: int) -> int:
    method bulk_create_ignore_conflicts (line 110) | def bulk_create_ignore_conflicts(self, items: List[EndpointDTO]) -> int:
    method iter_raw_data_for_export (line 167) | def iter_raw_data_for_export(

FILE: backend/apps/asset/repositories/asset/host_port_mapping_repository.py
  class DjangoHostPortMappingRepository (line 17) | class DjangoHostPortMappingRepository:
    method bulk_create_ignore_conflicts (line 23) | def bulk_create_ignore_conflicts(self, items: List[HostPortMappingDTO]...
    method get_for_export (line 74) | def get_for_export(self, target_id: int, batch_size: int = 1000):
    method get_ips_for_export (line 85) | def get_ips_for_export(self, target_id: int, batch_size: int = 1000) -...
    method get_queryset_by_target (line 98) | def get_queryset_by_target(self, target_id: int) -> QuerySet:
    method get_all_queryset (line 102) | def get_all_queryset(self) -> QuerySet:
    method get_queryset_by_ip (line 106) | def get_queryset_by_ip(self, ip: str, target_id: Optional[int] = None)...
    method iter_raw_data_for_export (line 113) | def iter_raw_data_for_export(

FILE: backend/apps/asset/repositories/asset/subdomain_repository.py
  class DjangoSubdomainRepository (line 17) | class DjangoSubdomainRepository:
    method bulk_create_ignore_conflicts (line 20) | def bulk_create_ignore_conflicts(self, items: List[SubdomainDTO]) -> N...
    method get_all (line 56) | def get_all(self):
    method get_by_target (line 60) | def get_by_target(self, target_id: int):
    method count_by_target (line 64) | def count_by_target(self, target_id: int) -> int:
    method get_domains_for_export (line 68) | def get_domains_for_export(self, target_id: int, batch_size: int = 100...
    method get_by_names_and_target_id (line 77) | def get_by_names_and_target_id(self, names: set, target_id: int) -> dict:
    method iter_raw_data_for_export (line 86) | def iter_raw_data_for_export(

FILE: backend/apps/asset/repositories/asset/website_repository.py
  class DjangoWebSiteRepository (line 18) | class DjangoWebSiteRepository:
    method bulk_upsert (line 21) | def bulk_upsert(self, items: List[WebSiteDTO]) -> int:
    method get_urls_for_export (line 83) | def get_urls_for_export(self, target_id: int, batch_size: int = 1000) ...
    method get_all (line 98) | def get_all(self):
    method get_by_target (line 102) | def get_by_target(self, target_id: int):
    method count_by_target (line 106) | def count_by_target(self, target_id: int) -> int:
    method get_by_url (line 110) | def get_by_url(self, url: str, target_id: int) -> Optional[int]:
    method bulk_create_ignore_conflicts (line 115) | def bulk_create_ignore_conflicts(self, items: List[WebSiteDTO]) -> int:
    method iter_raw_data_for_export (line 161) | def iter_raw_data_for_export(

FILE: backend/apps/asset/repositories/snapshot/directory_snapshot_repository.py
  class DjangoDirectorySnapshotRepository (line 16) | class DjangoDirectorySnapshotRepository:
    method save_snapshots (line 23) | def save_snapshots(self, items: List[DirectorySnapshotDTO]) -> None:
    method get_by_scan (line 80) | def get_by_scan(self, scan_id: int):
    method get_all (line 83) | def get_all(self):
    method iter_raw_data_for_export (line 86) | def iter_raw_data_for_export(

FILE: backend/apps/asset/repositories/snapshot/endpoint_snapshot_repository.py
  class DjangoEndpointSnapshotRepository (line 15) | class DjangoEndpointSnapshotRepository:
    method save_snapshots (line 18) | def save_snapshots(self, items: List[EndpointSnapshotDTO]) -> None:
    method get_by_scan (line 78) | def get_by_scan(self, scan_id: int):
    method get_all (line 81) | def get_all(self):
    method iter_raw_data_for_export (line 84) | def iter_raw_data_for_export(

FILE: backend/apps/asset/repositories/snapshot/host_port_mapping_snapshot_repository.py
  class DjangoHostPortMappingSnapshotRepository (line 15) | class DjangoHostPortMappingSnapshotRepository:
    method save_snapshots (line 18) | def save_snapshots(self, items: List[HostPortMappingSnapshotDTO]) -> N...
    method get_ip_aggregation_by_scan (line 68) | def get_ip_aggregation_by_scan(self, scan_id: int, filter_query: str =...
    method get_all_ip_aggregation (line 114) | def get_all_ip_aggregation(self, filter_query: str = None):
    method get_ips_for_export (line 156) | def get_ips_for_export(self, scan_id: int, batch_size: int = 1000) -> ...
    method iter_raw_data_for_export (line 169) | def iter_raw_data_for_export(

FILE: backend/apps/asset/repositories/snapshot/subdomain_snapshot_repository.py
  class DjangoSubdomainSnapshotRepository (line 15) | class DjangoSubdomainSnapshotRepository:
    method save_subdomain_snapshots (line 18) | def save_subdomain_snapshots(self, items: List[SubdomainSnapshotDTO]) ...
    method get_by_scan (line 63) | def get_by_scan(self, scan_id: int):
    method get_all (line 66) | def get_all(self):
    method iter_raw_data_for_export (line 69) | def iter_raw_data_for_export(

FILE: backend/apps/asset/repositories/snapshot/vulnerability_snapshot_repository.py
  class DjangoVulnerabilitySnapshotRepository (line 17) | class DjangoVulnerabilitySnapshotRepository:
    method save_snapshots (line 20) | def save_snapshots(self, items: List[VulnerabilitySnapshotDTO]) -> None:
    method get_by_scan (line 67) | def get_by_scan(self, scan_id: int):
    method get_all (line 71) | def get_all(self):

FILE: backend/apps/asset/repositories/snapshot/website_snapshot_repository.py
  class DjangoWebsiteSnapshotRepository (line 15) | class DjangoWebsiteSnapshotRepository:
    method save_snapshots (line 18) | def save_snapshots(self, items: List[WebsiteSnapshotDTO]) -> None:
    method get_by_scan (line 77) | def get_by_scan(self, scan_id: int):
    method get_all (line 80) | def get_all(self):
    method iter_raw_data_for_export (line 83) | def iter_raw_data_for_export(

FILE: backend/apps/asset/repositories/statistics_repository.py
  class AssetStatisticsRepository (line 11) | class AssetStatisticsRepository:
    method get_statistics (line 19) | def get_statistics(self) -> Optional[AssetStatistics]:
    method get_or_create_statistics (line 28) | def get_or_create_statistics(self) -> AssetStatistics:
    method update_statistics (line 37) | def update_statistics(
    method save_daily_snapshot (line 87) | def save_daily_snapshot(self, stats: AssetStatistics) -> StatisticsHis...
    method get_history (line 113) | def get_history(self, days: int = 7) -> List[StatisticsHistory]:

FILE: backend/apps/asset/serializers.py
  class SubdomainSerializer (line 24) | class SubdomainSerializer(serializers.ModelSerializer):
    class Meta (line 27) | class Meta:
  class SubdomainListSerializer (line 35) | class SubdomainListSerializer(serializers.ModelSerializer):
    class Meta (line 42) | class Meta:
  class WebSiteSerializer (line 70) | class WebSiteSerializer(serializers.ModelSerializer):
    class Meta (line 76) | class Meta:
  class VulnerabilitySerializer (line 98) | class VulnerabilitySerializer(serializers.ModelSerializer):
    class Meta (line 101) | class Meta:
  class VulnerabilitySnapshotSerializer (line 118) | class VulnerabilitySnapshotSerializer(serializers.ModelSerializer):
    class Meta (line 121) | class Meta:
  class EndpointListSerializer (line 137) | class EndpointListSerializer(serializers.ModelSerializer):
    class Meta (line 148) | class Meta:
  class DirectorySerializer (line 169) | class DirectorySerializer(serializers.ModelSerializer):
    class Meta (line 174) | class Meta:
  class IPAddressAggregatedSerializer (line 190) | class IPAddressAggregatedSerializer(serializers.Serializer):
  class SubdomainSnapshotSerializer (line 208) | class SubdomainSnapshotSerializer(serializers.ModelSerializer):
    class Meta (line 211) | class Meta:
  class WebsiteSnapshotSerializer (line 217) | class WebsiteSnapshotSerializer(serializers.ModelSerializer):
    class Meta (line 223) | class Meta:
  class DirectorySnapshotSerializer (line 244) | class DirectorySnapshotSerializer(serializers.ModelSerializer):
    class Meta (line 247) | class Meta:
  class EndpointSnapshotSerializer (line 263) | class EndpointSnapshotSerializer(serializers.ModelSerializer):
    class Meta (line 274) | class Meta:
  class ScreenshotListSerializer (line 298) | class ScreenshotListSerializer(serializers.ModelSerializer):
    class Meta (line 301) | class Meta:
  class ScreenshotSnapshotListSerializer (line 307) | class ScreenshotSnapshotListSerializer(serializers.ModelSerializer):
    class Meta (line 310) | class Meta:

FILE: backend/apps/asset/services/asset/directory_service.py
  class DirectoryService (line 14) | class DirectoryService:
    method __init__ (line 23) | def __init__(self, repository=None):
    method bulk_upsert (line 27) | def bulk_upsert(self, directory_dtos: List[DirectoryDTO]) -> int:
    method bulk_create_urls (line 48) | def bulk_create_urls(self, target_id: int, target_name: str, target_ty...
    method get_directories_by_target (line 104) | def get_directories_by_target(self, target_id: int, filter_query: Opti...
    method get_all (line 111) | def get_all(self, filter_query: Optional[str] = None):
    method iter_directory_urls_by_target (line 118) | def iter_directory_urls_by_target(self, target_id: int, chunk_size: in...
    method iter_raw_data_for_csv_export (line 122) | def iter_raw_data_for_csv_export(self, target_id: int) -> Iterator[dict]:

FILE: backend/apps/asset/services/asset/endpoint_service.py
  class EndpointService (line 18) | class EndpointService:
    method __init__ (line 34) | def __init__(self):
    method bulk_upsert (line 38) | def bulk_upsert(self, endpoints: List[EndpointDTO]) -> int:
    method bulk_create_urls (line 59) | def bulk_create_urls(self, target_id: int, target_name: str, target_ty...
    method get_endpoints_by_target (line 115) | def get_endpoints_by_target(self, target_id: int, filter_query: Option...
    method count_endpoints_by_target (line 122) | def count_endpoints_by_target(self, target_id: int) -> int:
    method get_all (line 134) | def get_all(self, filter_query: Optional[str] = None):
    method iter_endpoint_urls_by_target (line 141) | def iter_endpoint_urls_by_target(self, target_id: int, chunk_size: int...
    method iter_raw_data_for_csv_export (line 147) | def iter_raw_data_for_csv_export(self, target_id: int) -> Iterator[dict]:

FILE: backend/apps/asset/services/asset/host_port_mapping_service.py
  class HostPortMappingService (line 15) | class HostPortMappingService:
    method __init__ (line 30) | def __init__(self):
    method bulk_create_ignore_conflicts (line 33) | def bulk_create_ignore_conflicts(self, items: List[HostPortMappingDTO]...
    method iter_host_port_by_target (line 64) | def iter_host_port_by_target(self, target_id: int, batch_size: int = 1...
    method get_ip_aggregation_by_target (line 67) | def get_ip_aggregation_by_target(
    method get_all_ip_aggregation (line 91) | def get_all_ip_aggregation(self, filter_query: Optional[str] = None) -...
    method _aggregate_by_ip (line 110) | def _aggregate_by_ip(
    method iter_ips_by_target (line 155) | def iter_ips_by_target(self, target_id: int, batch_size: int = 1000) -...
    method iter_raw_data_for_csv_export (line 159) | def iter_raw_data_for_csv_export(self, target_id: int) -> Iterator[dict]:

FILE: backend/apps/asset/services/asset/subdomain_service.py
  class BulkCreateResult (line 14) | class BulkCreateResult:
  class SubdomainService (line 23) | class SubdomainService:
    method __init__ (line 31) | def __init__(self, repository=None):
    method get_all (line 42) | def get_all(self, filter_query: Optional[str] = None):
    method get_subdomains_by_target (line 58) | def get_subdomains_by_target(self, target_id: int, filter_query: Optio...
    method count_subdomains_by_target (line 74) | def count_subdomains_by_target(self, target_id: int) -> int:
    method get_by_names_and_target_id (line 87) | def get_by_names_and_target_id(self, names: set, target_id: int) -> dict:
    method get_subdomain_names_by_target (line 101) | def get_subdomain_names_by_target(self, target_id: int) -> List[str]:
    method iter_subdomain_names_by_target (line 114) | def iter_subdomain_names_by_target(self, target_id: int, chunk_size: i...
    method iter_raw_data_for_csv_export (line 128) | def iter_raw_data_for_csv_export(self, target_id: int):
    method bulk_create_ignore_conflicts (line 142) | def bulk_create_ignore_conflicts(self, items: List[SubdomainDTO]) -> N...
    method bulk_create_subdomains (line 155) | def bulk_create_subdomains(

FILE: backend/apps/asset/services/asset/vulnerability_service.py
  class VulnerabilityService (line 16) | class VulnerabilityService:
    method bulk_create_ignore_conflicts (line 30) | def bulk_create_ignore_conflicts(self, items: List[VulnerabilityDTO]) ...
    method get_all (line 75) | def get_all(self, filter_query: Optional[str] = None):
    method get_vulnerabilities_by_target (line 89) | def get_vulnerabilities_by_target(self, target_id: int, filter_query: ...
    method count_by_target (line 104) | def count_by_target(self, target_id: int) -> int:

FILE: backend/apps/asset/services/asset/website_service.py
  class WebSiteService (line 14) | class WebSiteService:
    method __init__ (line 26) | def __init__(self, repository=None):
    method bulk_upsert (line 30) | def bulk_upsert(self, website_dtos: List[WebSiteDTO]) -> int:
    method bulk_create_urls (line 51) | def bulk_create_urls(self, target_id: int, target_name: str, target_ty...
    method get_websites_by_target (line 107) | def get_websites_by_target(self, target_id: int, filter_query: Optiona...
    method get_all (line 114) | def get_all(self, filter_query: Optional[str] = None):
    method get_by_url (line 121) | def get_by_url(self, url: str, target_id: int) -> int:
    method iter_website_urls_by_target (line 125) | def iter_website_urls_by_target(self, target_id: int, chunk_size: int ...
    method iter_raw_data_for_csv_export (line 129) | def iter_raw_data_for_csv_export(self, target_id: int) -> Iterator[dict]:

FILE: backend/apps/asset/services/playwright_screenshot_service.py
  class PlaywrightScreenshotService (line 13) | class PlaywrightScreenshotService:
    method __init__ (line 22) | def __init__(
    method capture_screenshot (line 43) | async def capture_screenshot(self, url: str, page) -> tuple[Optional[b...
    method _capture_with_semaphore (line 82) | async def _capture_with_semaphore(
    method capture_batch (line 107) | async def capture_batch(
    method capture_batch_collect (line 170) | async def capture_batch_collect(

FILE: backend/apps/asset/services/screenshot_service.py
  class ScreenshotService (line 16) | class ScreenshotService:
    method __init__ (line 19) | def __init__(self, max_width: int = 800, target_kb: int = 100):
    method compress_screenshot (line 30) | def compress_screenshot(self, image_path: str) -> Optional[bytes]:
    method compress_from_bytes (line 51) | def compress_from_bytes(self, image_bytes: bytes) -> Optional[bytes]:
    method _compress_image (line 71) | def _compress_image(self, img: Image.Image) -> Optional[bytes]:
    method save_screenshot_snapshot (line 104) | def save_screenshot_snapshot(
    method sync_screenshots_to_asset (line 136) | def sync_screenshots_to_asset(self, scan_id: int, target_id: int) -> int:
    method process_and_save_screenshot (line 169) | def process_and_save_screenshot(self, scan_id: int, url: str, image_pa...

FILE: backend/apps/asset/services/search_service.py
  class SearchQueryParser (line 92) | class SearchQueryParser:
    method parse (line 114) | def parse(cls, query: str) -> Tuple[str, List[Any]]:
    method _split_by_or (line 157) | def _split_by_or(cls, query: str) -> List[str]:
    method _parse_and_group (line 186) | def _parse_and_group(cls, group: str) -> Tuple[str, List[Any]]:
    method _split_by_and (line 211) | def _split_by_and(cls, query: str) -> List[str]:
    method _parse_condition (line 240) | def _parse_condition(cls, condition: str) -> Tuple[Optional[str], List...
    method _build_like_condition (line 282) | def _build_like_condition(cls, field: str, value: str, is_array: bool)...
    method _build_exact_condition (line 297) | def _build_exact_condition(cls, field: str, value: str, is_array: bool...
    method _build_not_equal_condition (line 312) | def _build_not_equal_condition(cls, field: str, value: str, is_array: ...
  class AssetSearchService (line 329) | class AssetSearchService:
    method search (line 332) | def search(
    method count (line 384) | def count(self, query: str, asset_type: AssetType = 'website', stateme...
    method search_iter (line 415) | def search_iter(

FILE: backend/apps/asset/services/snapshot/directory_snapshots_service.py
  class DirectorySnapshotsService (line 13) | class DirectorySnapshotsService:
    method __init__ (line 16) | def __init__(self):
    method save_and_sync (line 20) | def save_and_sync(self, items: List[DirectorySnapshotDTO]) -> None:
    method get_by_scan (line 77) | def get_by_scan(self, scan_id: int, filter_query: str = None):
    method get_all (line 85) | def get_all(self, filter_query: str = None):
    method iter_directory_urls_by_scan (line 94) | def iter_directory_urls_by_scan(self, scan_id: int, chunk_size: int = ...
    method iter_raw_data_for_csv_export (line 100) | def iter_raw_data_for_csv_export(self, scan_id: int) -> Iterator[dict]:

FILE: backend/apps/asset/services/snapshot/endpoint_snapshots_service.py
  class EndpointSnapshotsService (line 13) | class EndpointSnapshotsService:
    method __init__ (line 16) | def __init__(self):
    method save_and_sync (line 20) | def save_and_sync(self, items: List[EndpointSnapshotDTO]) -> None:
    method get_by_scan (line 80) | def get_by_scan(self, scan_id: int, filter_query: str = None):
    method get_all (line 88) | def get_all(self, filter_query: str = None):
    method iter_endpoint_urls_by_scan (line 97) | def iter_endpoint_urls_by_scan(self, scan_id: int, chunk_size: int = 1...
    method iter_raw_data_for_csv_export (line 103) | def iter_raw_data_for_csv_export(self, scan_id: int) -> Iterator[dict]:

FILE: backend/apps/asset/services/snapshot/host_port_mapping_snapshots_service.py
  class HostPortMappingSnapshotsService (line 13) | class HostPortMappingSnapshotsService:
    method __init__ (line 16) | def __init__(self):
    method save_and_sync (line 20) | def save_and_sync(self, items: List[HostPortMappingSnapshotDTO]) -> None:
    method get_ip_aggregation_by_scan (line 72) | def get_ip_aggregation_by_scan(self, scan_id: int, filter_query: str =...
    method get_all_ip_aggregation (line 75) | def get_all_ip_aggregation(self, filter_query: str = None):
    method iter_ips_by_scan (line 79) | def iter_ips_by_scan(self, scan_id: int, batch_size: int = 1000) -> It...
    method iter_raw_data_for_csv_export (line 83) | def iter_raw_data_for_csv_export(self, scan_id: int) -> Iterator[dict]:

FILE: backend/apps/asset/services/snapshot/subdomain_snapshots_service.py
  class SubdomainSnapshotsService (line 10) | class SubdomainSnapshotsService:
    method __init__ (line 13) | def __init__(self):
    method save_and_sync (line 16) | def save_and_sync(self, items: List[SubdomainSnapshotDTO]) -> None:
    method get_by_scan (line 74) | def get_by_scan(self, scan_id: int, filter_query: str = None):
    method get_all (line 82) | def get_all(self, filter_query: str = None):
    method iter_subdomain_names_by_scan (line 91) | def iter_subdomain_names_by_scan(self, scan_id: int, chunk_size: int =...
    method iter_raw_data_for_csv_export (line 96) | def iter_raw_data_for_csv_export(self, scan_id: int) -> Iterator[dict]:

FILE: backend/apps/asset/services/snapshot/vulnerability_snapshots_service.py
  class VulnerabilitySnapshotsService (line 13) | class VulnerabilitySnapshotsService:
    method __init__ (line 21) | def __init__(self):
    method save_and_sync (line 25) | def save_and_sync(self, items: List[VulnerabilitySnapshotDTO]) -> None:
    method get_by_scan (line 77) | def get_by_scan(self, scan_id: int, filter_query: str = None):
    method get_all (line 86) | def get_all(self, filter_query: str = None):
    method iter_vuln_urls_by_scan (line 95) | def iter_vuln_urls_by_scan(self, scan_id: int, chunk_size: int = 1000)...

FILE: backend/apps/asset/services/snapshot/website_snapshots_service.py
  class WebsiteSnapshotsService (line 13) | class WebsiteSnapshotsService:
    method __init__ (line 16) | def __init__(self):
    method save_and_sync (line 20) | def save_and_sync(self, items: List[WebsiteSnapshotDTO]) -> None:
    method get_by_scan (line 81) | def get_by_scan(self, scan_id: int, filter_query: str = None):
    method get_all (line 89) | def get_all(self, filter_query: str = None):
    method iter_website_urls_by_scan (line 98) | def iter_website_urls_by_scan(self, scan_id: int, chunk_size: int = 10...
    method iter_raw_data_for_csv_export (line 104) | def iter_raw_data_for_csv_export(self, scan_id: int) -> Iterator[dict]:

FILE: backend/apps/asset/services/statistics_service.py
  class AssetStatisticsService (line 23) | class AssetStatisticsService:
    method __init__ (line 32) | def __init__(self):
    method get_statistics (line 35) | def get_statistics(self) -> dict:
    method _get_vuln_by_severity (line 92) | def _get_vuln_by_severity(self) -> dict:
    method refresh_statistics (line 104) | def refresh_statistics(self) -> AssetStatistics:
    method get_statistics_history (line 139) | def get_statistics_history(self, days: int = 7) -> list[dict]:

FILE: backend/apps/asset/views/asset_views.py
  class AssetStatisticsViewSet (line 31) | class AssetStatisticsViewSet(viewsets.ViewSet):
    method __init__ (line 38) | def __init__(self, **kwargs):
    method list (line 42) | def list(self, request):
    method history (line 91) | def history(self, request: Request):
  class SubdomainViewSet (line 126) | class SubdomainViewSet(viewsets.ModelViewSet):
    method __init__ (line 144) | def __init__(self, **kwargs):
    method get_queryset (line 148) | def get_queryset(self):
    method bulk_create (line 158) | def bulk_create(self, request, **kwargs):
    method export (line 240) | def export(self, request, **kwargs):
    method bulk_delete (line 264) | def bulk_delete(self, request, **kwargs):
  class WebSiteViewSet (line 293) | class WebSiteViewSet(viewsets.ModelViewSet):
    method __init__ (line 314) | def __init__(self, **kwargs):
    method get_queryset (line 318) | def get_queryset(self):
    method bulk_create (line 328) | def bulk_create(self, request, **kwargs):
    method export (line 395) | def export(self, request, **kwargs):
    method bulk_delete (line 426) | def bulk_delete(self, request, **kwargs):
  class DirectoryViewSet (line 455) | class DirectoryViewSet(viewsets.ModelViewSet):
    method __init__ (line 473) | def __init__(self, **kwargs):
    method get_queryset (line 477) | def get_queryset(self):
    method bulk_create (line 487) | def bulk_create(self, request, **kwargs):
    method export (line 554) | def export(self, request, **kwargs):
    method bulk_delete (line 583) | def bulk_delete(self, request, **kwargs):
  class EndpointViewSet (line 612) | class EndpointViewSet(viewsets.ModelViewSet):
    method __init__ (line 633) | def __init__(self, **kwargs):
    method get_queryset (line 637) | def get_queryset(self):
    method bulk_create (line 647) | def bulk_create(self, request, **kwargs):
    method export (line 714) | def export(self, request, **kwargs):
    method bulk_delete (line 746) | def bulk_delete(self, request, **kwargs):
  class HostPortMappingViewSet (line 775) | class HostPortMappingViewSet(viewsets.ModelViewSet):
    method __init__ (line 803) | def __init__(self, **kwargs):
    method get_queryset (line 807) | def get_queryset(self):
    method export (line 820) | def export(self, request, **kwargs):
    method bulk_delete (line 848) | def bulk_delete(self, request, **kwargs):
  class VulnerabilityViewSet (line 880) | class VulnerabilityViewSet(viewsets.ModelViewSet):
    method __init__ (line 900) | def __init__(self, **kwargs):
    method get_queryset (line 904) | def get_queryset(self):
  class SubdomainSnapshotViewSet (line 916) | class SubdomainSnapshotViewSet(viewsets.ModelViewSet):
    method __init__ (line 931) | def __init__(self, **kwargs):
    method get_queryset (line 935) | def get_queryset(self):
    method export (line 944) | def export(self, request, **kwargs):
  class WebsiteSnapshotViewSet (line 968) | class WebsiteSnapshotViewSet(viewsets.ModelViewSet):
    method __init__ (line 985) | def __init__(self, **kwargs):
    method get_queryset (line 989) | def get_queryset(self):
    method export (line 998) | def export(self, request, **kwargs):
  class DirectorySnapshotViewSet (line 1029) | class DirectorySnapshotViewSet(viewsets.ModelViewSet):
    method __init__ (line 1043) | def __init__(self, **kwargs):
    method get_queryset (line 1047) | def get_queryset(self):
    method export (line 1056) | def export(self, request, **kwargs):
  class EndpointSnapshotViewSet (line 1085) | class EndpointSnapshotViewSet(viewsets.ModelViewSet):
    method __init__ (line 1102) | def __init__(self, **kwargs):
    method get_queryset (line 1106) | def get_queryset(self):
    method export (line 1115) | def export(self, request, **kwargs):
  class HostPortMappingSnapshotViewSet (line 1147) | class HostPortMappingSnapshotViewSet(viewsets.ModelViewSet):
    method __init__ (line 1161) | def __init__(self, **kwargs):
    method get_queryset (line 1165) | def get_queryset(self):
    method export (line 1174) | def export(self, request, **kwargs):
  class VulnerabilitySnapshotViewSet (line 1202) | class VulnerabilitySnapshotViewSet(viewsets.ModelViewSet):
    method __init__ (line 1217) | def __init__(self, **kwargs):
    method get_queryset (line 1221) | def get_queryset(self):
  class ScreenshotViewSet (line 1232) | class ScreenshotViewSet(viewsets.ModelViewSet):
    method get_queryset (line 1250) | def get_queryset(self):
    method image (line 1268) | def image(self, request, pk=None, **kwargs):
    method bulk_delete (line 1298) | def bulk_delete(self, request, **kwargs):
  class ScreenshotSnapshotViewSet (line 1327) | class ScreenshotSnapshotViewSet(viewsets.ModelViewSet):
    method get_queryset (line 1341) | def get_queryset(self):
    method image (line 1359) | def image(self, request, pk=None, **kwargs):

FILE: backend/apps/asset/views/search_views.py
  class AssetSearchView (line 45) | class AssetSearchView(APIView):
    method __init__ (line 73) | def __init__(self, **kwargs):
    method _parse_headers (line 77) | def _parse_headers(self, headers_data) -> dict:
    method _format_result (line 91) | def _format_result(self, result: dict, vulnerabilities_by_url: dict, a...
    method _get_vulnerabilities_by_url_prefix (line 133) | def _get_vulnerabilities_by_url_prefix(self, website_urls: list) -> dict:
    method get (line 216) | def get(self, request: Request):
  class AssetSearchExportView (line 276) | class AssetSearchExportView(APIView):
    method __init__ (line 290) | def __init__(self, **kwargs):
    method _get_headers_and_formatters (line 294) | def _get_headers_and_formatters(self, asset_type: str):
    method get (line 314) | def get(self, request: Request):

FILE: backend/apps/common/apps.py
  class CommonConfig (line 4) | class CommonConfig(AppConfig):
    method ready (line 8) | def ready(self):

FILE: backend/apps/common/authentication.py
  class CsrfExemptSessionAuthentication (line 4) | class CsrfExemptSessionAuthentication(SessionAuthentication):
    method enforce_csrf (line 11) | def enforce_csrf(self, request):

FILE: backend/apps/common/container_bootstrap.py
  function fetch_config_and_setup_django (line 25) | def fetch_config_and_setup_django():

FILE: backend/apps/common/decorators/db_connection.py
  function ensure_db_connection (line 25) | def ensure_db_connection(method):
  function auto_ensure_db_connection (line 48) | def auto_ensure_db_connection(cls):
  function _check_and_reconnect (line 98) | def _check_and_reconnect(max_retries=5):
  function async_check_and_reconnect (line 152) | async def async_check_and_reconnect(max_retries=5):
  function ensure_db_connection_async (line 156) | def ensure_db_connection_async(method):

FILE: backend/apps/common/definitions.py
  class ScanStatus (line 4) | class ScanStatus(models.TextChoices):
  class VulnSeverity (line 13) | class VulnSeverity(models.TextChoices):

FILE: backend/apps/common/error_codes.py
  class ErrorCodes (line 11) | class ErrorCodes:

FILE: backend/apps/common/exception_handlers.py
  function custom_exception_handler (line 15) | def custom_exception_handler(exc, context):

FILE: backend/apps/common/management/commands/db_health_check.py
  class Command (line 35) | class Command(BaseCommand):
    method add_arguments (line 40) | def add_arguments(self, parser):
    method handle (line 74) | def handle(self, *args, **options):
    method test_database_latency (line 104) | def test_database_latency(self, db_connection, test_count):
    method test_api_performance (line 177) | def test_api_performance(self, test_count):
    method monitor_database_performance (line 249) | def monitor_database_performance(self, db_connection):
    method show_connection_stats (line 403) | def show_connection_stats(self, db_connection):

FILE: backend/apps/common/management/commands/db_monitor.py
  class Command (line 12) | class Command(BaseCommand):
    method add_arguments (line 17) | def add_arguments(self, parser):
    method handle (line 31) | def handle(self, *args, **options):
    method monitor_key_metrics (line 44) | def monitor_key_metrics(self):

FILE: backend/apps/common/management/commands/init_admin.py
  class Command (line 15) | class Command(BaseCommand):
    method add_arguments (line 18) | def add_arguments(self, parser):
    method handle (line 31) | def handle(self, *args, **options):

FILE: backend/apps/common/migrations/0001_initial.py
  class Migration (line 7) | class Migration(migrations.Migration):

FILE: backend/apps/common/models/blacklist.py
  class BlacklistRule (line 5) | class BlacklistRule(models.Model):
    class RuleType (line 12) | class RuleType(models.TextChoices):
    class Scope (line 18) | class Scope(models.TextChoices):
    class Meta (line 54) | class Meta:
    method __str__ (line 68) | def __str__(self):

FILE: backend/apps/common/normalizer.py
  function normalize_domain (line 7) | def normalize_domain(domain: str) -> str:
  function normalize_ip (line 35) | def normalize_ip(ip: str) -> str:
  function normalize_cidr (line 56) | def normalize_cidr(cidr: str) -> str:
  function normalize_target (line 77) | def normalize_target(target: str) -> str:

FILE: backend/apps/common/pagination.py
  class BasePagination (line 8) | class BasePagination(PageNumberPagination):
    method get_paginated_response (line 25) | def get_paginated_response(self, data):

FILE: backend/apps/common/permissions.py
  class IsAuthenticatedOrPublic (line 38) | class IsAuthenticatedOrPublic(BasePermission):
    method has_permission (line 46) | def has_permission(self, request, view):
    method _check_worker_api_key (line 62) | def _check_worker_api_key(self, request):

FILE: backend/apps/common/prefect_django_setup.py
  function setup_django_for_prefect (line 11) | def setup_django_for_prefect():
  function close_old_db_connections (line 47) | def close_old_db_connections():

FILE: backend/apps/common/response_helpers.py
  function success_response (line 14) | def success_response(
  function error_response (line 48) | def error_response(

FILE: backend/apps/common/serializers/blacklist_serializers.py
  class BlacklistRuleSerializer (line 8) | class BlacklistRuleSerializer(serializers.ModelSerializer):
    class Meta (line 11) | class Meta:
    method validate_pattern (line 24) | def validate_pattern(self, value):
    method create (line 30) | def create(self, validated_data):
    method update (line 36) | def update(self, instance, validated_data):
  class GlobalBlacklistRuleSerializer (line 44) | class GlobalBlacklistRuleSerializer(BlacklistRuleSerializer):
    class Meta (line 47) | class Meta(BlacklistRuleSerializer.Meta):
    method create (line 51) | def create(self, validated_data):
  class TargetBlacklistRuleSerializer (line 58) | class TargetBlacklistRuleSerializer(BlacklistRuleSerializer):
    class Meta (line 61) | class Meta(BlacklistRuleSerializer.Meta):
    method create (line 65) | def create(self, validated_data):

FILE: backend/apps/common/services/blacklist_service.py
  function _normalize_patterns (line 24) | def _normalize_patterns(patterns: List[str]) -> List[str]:
  class BlacklistService (line 37) | class BlacklistService:
    method get_global_rules (line 45) | def get_global_rules(self) -> QuerySet:
    method get_target_rules (line 55) | def get_target_rules(self, target_id: int) -> QuerySet:
    method get_rules (line 71) | def get_rules(self, target_id: Optional[int] = None) -> List:
    method replace_global_rules (line 96) | def replace_global_rules(self, patterns: List[str]) -> Dict[str, Any]:
    method replace_target_rules (line 117) | def replace_target_rules(self, target, patterns: List[str]) -> Dict[st...
    method _replace_rules (line 139) | def _replace_rules(self, patterns: List[str], scope: str, target=None)...

FILE: backend/apps/common/services/system_log_service.py
  class LogFileInfo (line 21) | class LogFileInfo(TypedDict):
  class SystemLogService (line 29) | class SystemLogService:
    method __init__ (line 44) | def __init__(self):
    method _categorize_file (line 52) | def _categorize_file(self, filename: str) -> str | None:
    method _validate_filename (line 64) | def _validate_filename(self, filename: str) -> bool:
    method get_log_files (line 83) | def get_log_files(self) -> list[LogFileInfo]:
    method get_logs_content (line 131) | def get_logs_content(self, filename: str | None = None, lines: int | N...

FILE: backend/apps/common/utils/blacklist_filter.py
  function detect_rule_type (line 60) | def detect_rule_type(pattern: str) -> str:
  function extract_host (line 106) | def extract_host(target: str) -> str:
  class BlacklistFilter (line 137) | class BlacklistFilter:
    method __init__ (line 144) | def __init__(self, rules: List):
    method is_allowed (line 185) | def is_allowed(self, target: str) -> bool:
    method _check_domain_rules (line 208) | def _check_domain_rules(self, host: str) -> bool:
    method _check_ip_rules (line 228) | def _check_ip_rules(self, host: str) -> bool:

FILE: backend/apps/common/utils/csv_utils.py
  function generate_csv_rows (line 26) | def generate_csv_rows(
  function format_list_field (line 71) | def format_list_field(values: List, separator: str = ';') -> str:
  function format_datetime (line 97) | def format_datetime(dt: Optional[datetime]) -> str:
  function create_csv_export_response (line 127) | def create_csv_export_response(
  function _create_file_response (line 166) | def _create_file_response(
  function _create_streaming_response (line 230) | def _create_streaming_response(

FILE: backend/apps/common/utils/dedup.py
  function get_unique_fields (line 18) | def get_unique_fields(model: type[models.Model]) -> Optional[Tuple[str, ...
  function deduplicate_for_bulk (line 52) | def deduplicate_for_bulk(items: List[T], model: type[models.Model]) -> L...

FILE: backend/apps/common/utils/filter_utils.py
  class ArrayToString (line 38) | class ArrayToString(Func):
  class LogicalOp (line 45) | class LogicalOp(Enum):
  class ParsedFilter (line 52) | class ParsedFilter:
  class FilterGroup (line 60) | class FilterGroup:
  class QueryParser (line 66) | class QueryParser:
    method parse (line 80) | def parse(cls, query_string: str) -> List[FilterGroup]:
  class QueryBuilder (line 150) | class QueryBuilder:
    method build_query (line 157) | def build_query(
    method _build_single_q (line 228) | def _build_single_q(cls, field: str, operator: str, value: str, is_jso...
    method _try_convert_to_int (line 251) | def _try_convert_to_int(cls, value: str) -> Optional[int]:
    method _build_fuzzy_q (line 259) | def _build_fuzzy_q(cls, field: str, value: str) -> Q:
    method _build_exact_q (line 264) | def _build_exact_q(cls, field: str, value: str) -> Q:
    method _build_not_equal_q (line 272) | def _build_not_equal_q(cls, field: str, value: str) -> Q:
  function apply_filters (line 280) | def apply_filters(

FILE: backend/apps/common/utils/hash.py
  function calc_file_sha256 (line 18) | def calc_file_sha256(file_path: str, chunk_size: int = DEFAULT_CHUNK_SIZ...
  function calc_stream_sha256 (line 39) | def calc_stream_sha256(stream: BinaryIO, chunk_size: int = DEFAULT_CHUNK...
  function safe_calc_file_sha256 (line 55) | def safe_calc_file_sha256(file_path: str) -> Optional[str]:
  function is_file_hash_match (line 74) | def is_file_hash_match(file_path: str, expected_hash: str) -> bool:

FILE: backend/apps/common/validators.py
  function validate_domain (line 11) | def validate_domain(domain: str) -> None:
  function is_valid_domain (line 30) | def is_valid_domain(domain: str) -> bool:
  function validate_ip (line 45) | def validate_ip(ip: str) -> None:
  function is_valid_ip (line 64) | def is_valid_ip(ip: str) -> bool:
  function validate_cidr (line 83) | def validate_cidr(cidr: str) -> None:
  function detect_target_type (line 102) | def detect_target_type(name: str) -> str:
  function validate_port (line 144) | def validate_port(port: any) -> tuple[bool, int | None]:
  function validate_url (line 183) | def validate_url(url: str) -> None:
  function is_valid_url (line 208) | def is_valid_url(url: str, max_length: int = 2000) -> bool:
  function is_url_match_target (line 228) | def is_url_match_target(url: str, target_name: str, target_type: str) ->...
  function detect_input_type (line 272) | def detect_input_type(input_str: str) -> str:

FILE: backend/apps/common/views/auth_views.py
  class LoginView (line 21) | class LoginView(APIView):
    method post (line 29) | def post(self, request):
  class LogoutView (line 65) | class LogoutView(APIView):
    method post (line 73) | def post(self, request):
  class MeView (line 92) | class MeView(APIView):
    method get (line 100) | def get(self, request):
  class ChangePasswordView (line 132) | class ChangePasswordView(APIView):
    method post (line 138) | def post(self, request):

FILE: backend/apps/common/views/blacklist_views.py
  class GlobalBlacklistView (line 14) | class GlobalBlacklistView(APIView):
    method __init__ (line 34) | def __init__(self, **kwargs):
    method get (line 38) | def get(self, request):
    method put (line 51) | def put(self, request):

FILE: backend/apps/common/views/health_views.py
  class HealthCheckView (line 12) | class HealthCheckView(APIView):
    method get (line 23) | def get(self, request):

FILE: backend/apps/common/views/system_log_views.py
  class SystemLogFilesView (line 24) | class SystemLogFilesView(APIView):
    method __init__ (line 45) | def __init__(self, **kwargs):
    method get (line 49) | def get(self, request):
  class SystemLogsView (line 64) | class SystemLogsView(APIView):
    method __init__ (line 81) | def __init__(self, **kwargs):
    method get (line 85) | def get(self, request):

FILE: backend/apps/common/views/version_views.py
  function get_current_version (line 24) | def get_current_version() -> str:
  function compare_versions (line 48) | def compare_versions(current: str, latest: str) -> bool:
  class VersionView (line 73) | class VersionView(APIView):
    method get (line 76) | def get(self, _request: Request) -> Response:
  class CheckUpdateView (line 84) | class CheckUpdateView(APIView):
    method get (line 87) | def get(self, _request: Request) -> Response:

FILE: backend/apps/common/websocket_auth.py
  class AuthenticatedWebsocketConsumer (line 13) | class AuthenticatedWebsocketConsumer(AsyncWebsocketConsumer):
    method connect (line 20) | async def connect(self):
    method on_connect (line 38) | async def on_connect(self):

FILE: backend/apps/engine/apps.py
  class EngineConfig (line 5) | class EngineConfig(AppConfig):
    method ready (line 10) | def ready(self):
    method _is_runserver (line 19) | def _is_runserver(self):
    method _start_scheduler (line 24) | def _start_scheduler(self):

FILE: backend/apps/engine/consumers/worker_deploy_consumer.py
  class WorkerDeployConsumer (line 19) | class WorkerDeployConsumer(AuthenticatedWebsocketConsumer):
    method __init__ (line 26) | def __init__(self, *args, **kwargs):
    method on_connect (line 34) | async def on_connect(self):
    method disconnect (line 47) | async def disconnect(self, close_code):
    method receive (line 65) | async def receive(self, text_data=None, bytes_data=None):
    method _auto_ssh_connect (line 111) | async def _auto_ssh_connect(self):
    method _ssh_connect (line 136) | async def _ssh_connect(self, password: str, cols: int = 80, rows: int ...
    method _read_shell_output (line 196) | async def _read_shell_output(self):
    method _run_deploy_script (line 211) | async def _run_deploy_script(self):
    method _run_uninstall_script (line 353) | async def _run_uninstall_script(self):
    method _attach_deploy_session (line 423) | async def _attach_deploy_session(self):
    method terminal_output (line 447) | async def terminal_output(self, event):
    method deploy_status (line 451) | async def deploy_status(self, event):

FILE: backend/apps/engine/management/commands/init_default_engine.py
  class Command (line 28) | class Command(BaseCommand):
    method add_arguments (line 31) | def add_arguments(self, parser):
    method handle (line 43) | def handle(self, *args, **options):

FILE: backend/apps/engine/management/commands/init_fingerprints.py
  class Command (line 95) | class Command(BaseCommand):
    method handle (line 98) | def handle(self, *args, **options):
    method _extract_fingerprints (line 177) | def _extract_fingerprints(self, json_data, data_key, fp_type):

FILE: backend/apps/engine/management/commands/init_nuclei_templates.py
  function get_local_commit_hash (line 34) | def get_local_commit_hash(local_path: Path) -> str:
  class Command (line 48) | class Command(BaseCommand):
    method add_arguments (line 51) | def add_arguments(self, parser):
    method handle (line 63) | def handle(self, *args, **options):

FILE: backend/apps/engine/management/commands/init_wordlists.py
  class Command (line 38) | class Command(BaseCommand):
    method handle (line 41) | def handle(self, *args, **options):

FILE: backend/apps/engine/migrations/0001_initial.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: backend/apps/engine/models/engine.py
  class WorkerNode (line 9) | class WorkerNode(models.Model):
    class Meta (line 45) | class Meta:
    method __str__ (line 63) | def __str__(self):
  class ScanEngine (line 69) | class ScanEngine(models.Model):
    class Meta (line 78) | class Meta:
    method __str__ (line 87) | def __str__(self):
  class Wordlist (line 91) | class Wordlist(models.Model):
    class Meta (line 104) | class Meta:
    method __str__ (line 113) | def __str__(self) -> str:
  class NucleiTemplateRepo (line 117) | class NucleiTemplateRepo(models.Model):
    class Meta (line 128) | class Meta:
    method __str__ (line 133) | def __str__(self) -> str:  # pragma: no cover - 简单表示

FILE: backend/apps/engine/models/fingerprints.py
  class GobyFingerprint (line 9) | class GobyFingerprint(models.Model):
    class Meta (line 22) | class Meta:
    method __str__ (line 33) | def __str__(self) -> str:
  class EholeFingerprint (line 37) | class EholeFingerprint(models.Model):
    class Meta (line 48) | class Meta:
    method __str__ (line 71) | def __str__(self) -> str:
  class WappalyzerFingerprint (line 75) | class WappalyzerFingerprint(models.Model):
    class Meta (line 95) | class Meta:
    method __str__ (line 107) | def __str__(self) -> str:
  class FingersFingerprint (line 111) | class FingersFingerprint(models.Model):
    class Meta (line 125) | class Meta:
    method __str__ (line 137) | def __str__(self) -> str:
  class FingerPrintHubFingerprint (line 141) | class FingerPrintHubFingerprint(models.Model):
    class Meta (line 157) | class Meta:
    method __str__ (line 170) | def __str__(self) -> str:
  class ARLFingerprint (line 174) | class ARLFingerprint(models.Model):
    class Meta (line 184) | class Meta:
    method __str__ (line 194) | def __str__(self) -> str:

FILE: backend/apps/engine/repositories/django_engine_repository.py
  class DjangoEngineRepository (line 16) | class DjangoEngineRepository:
    method get_all (line 19) | def get_all(self):
    method get_by_id (line 23) | def get_by_id(self, engine_id: int) -> ScanEngine | None:

FILE: backend/apps/engine/repositories/django_wordlist_repository.py
  class DjangoWordlistRepository (line 16) | class DjangoWordlistRepository:
    method get_queryset (line 19) | def get_queryset(self):
    method get_by_id (line 23) | def get_by_id(self, wordlist_id: int) -> Wordlist | None:
    method get_by_name (line 31) | def get_by_name(self, name: str) -> Wordlist | None:
    method create (line 38) | def create(self, **kwargs) -> Wordlist:
    method delete (line 42) | def delete(self, wordlist_id: int) -> bool:

FILE: backend/apps/engine/repositories/django_worker_repository.py
  class DjangoWorkerRepository (line 19) | class DjangoWorkerRepository:
    method get_by_id (line 22) | def get_by_id(self, worker_id: int) -> WorkerNode | None:
    method get_all (line 30) | def get_all(self):
    method update_status (line 34) | def update_status(self, worker_id: int, status: str) -> bool:
    method delete_by_id (line 46) | def delete_by_id(self, worker_id: int) -> bool:
    method get_or_create_by_name (line 56) | def get_or_create_by_name(

FILE: backend/apps/engine/repositories/nuclei_repo_repository.py
  class NucleiTemplateRepository (line 28) | class NucleiTemplateRepository:
    method get_by_id (line 35) | def get_by_id(self, repo_id: int) -> Optional[NucleiTemplateRepo]:
  class TemplateFileRepository (line 50) | class TemplateFileRepository:
    method __init__ (line 60) | def __init__(self, root: Path) -> None:
    method get_tree (line 69) | def get_tree(self) -> List[Dict]:
    method get_file_content (line 148) | def get_file_content(self, rel_path: str) -> Optional[Dict]:

FILE: backend/apps/engine/scheduler.py
  function get_scheduler (line 19) | def get_scheduler() -> BackgroundScheduler:
  function start_scheduler (line 34) | def start_scheduler():
  function shutdown_scheduler (line 50) | def shutdown_scheduler():
  function _register_scheduled_jobs (line 59) | def _register_scheduled_jobs(scheduler: BackgroundScheduler):
  function _trigger_scheduled_scans (line 95) | def _trigger_scheduled_scans():
  function _trigger_statistics_refresh (line 110) | def _trigger_statistics_refresh():
  function _trigger_cleanup (line 124) | def _trigger_cleanup():

FILE: backend/apps/engine/serializers/engine_serializer.py
  class ScanEngineSerializer (line 8) | class ScanEngineSerializer(serializers.ModelSerializer):
    class Meta (line 11) | class Meta:
    method to_representation (line 22) | def to_representation(self, instance):
    method validate_name (line 30) | def validate_name(self, value):
    method validate_configuration (line 36) | def validate_configuration(self, value):

FILE: backend/apps/engine/serializers/fingerprints/arl.py
  class ARLFingerprintSerializer (line 8) | class ARLFingerprintSerializer(serializers.ModelSerializer):
    class Meta (line 16) | class Meta:
    method validate_name (line 21) | def validate_name(self, value):
    method validate_rule (line 27) | def validate_rule(self, value):

FILE: backend/apps/engine/serializers/fingerprints/ehole.py
  class EholeFingerprintSerializer (line 8) | class EholeFingerprintSerializer(serializers.ModelSerializer):
    class Meta (line 11) | class Meta:
    method validate_cms (line 17) | def validate_cms(self, value):
    method validate_keyword (line 23) | def validate_keyword(self, value):

FILE: backend/apps/engine/serializers/fingerprints/fingerprinthub.py
  class FingerPrintHubFingerprintSerializer (line 8) | class FingerPrintHubFingerprintSerializer(serializers.ModelSerializer):
    class Meta (line 22) | class Meta:
    method validate_fp_id (line 28) | def validate_fp_id(self, value):
    method validate_name (line 34) | def validate_name(self, value):
    method validate_http (line 40) | def validate_http(self, value):
    method validate_metadata (line 46) | def validate_metadata(self, value):

FILE: backend/apps/engine/serializers/fingerprints/fingers.py
  class FingersFingerprintSerializer (line 8) | class FingersFingerprintSerializer(serializers.ModelSerializer):
    class Meta (line 20) | class Meta:
    method validate_name (line 26) | def validate_name(self, value):
    method validate_rule (line 32) | def validate_rule(self, value):
    method validate_tag (line 38) | def validate_tag(self, value):
    method validate_default_port (line 44) | def validate_default_port(self, value):

FILE: backend/apps/engine/serializers/fingerprints/goby.py
  class GobyFingerprintSerializer (line 8) | class GobyFingerprintSerializer(serializers.ModelSerializer):
    class Meta (line 11) | class Meta:
    method validate_name (line 16) | def validate_name(self, value):
    method validate_rule (line 22) | def validate_rule(self, value):

FILE: backend/apps/engine/serializers/fingerprints/wappalyzer.py
  class WappalyzerFingerprintSerializer (line 8) | class WappalyzerFingerprintSerializer(serializers.ModelSerializer):
    class Meta (line 11) | class Meta:
    method validate_name (line 20) | def validate_name(self, value):

FILE: backend/apps/engine/serializers/nuclei_template_repo_serializer.py
  class NucleiTemplateRepoSerializer (line 22) | class NucleiTemplateRepoSerializer(serializers.ModelSerializer):
    class Meta (line 28) | class Meta:

FILE: backend/apps/engine/serializers/wordlist_serializer.py
  class WordlistSerializer (line 8) | class WordlistSerializer(serializers.ModelSerializer):
    class Meta (line 11) | class Meta:

FILE: backend/apps/engine/serializers/worker_serializer.py
  class WorkerNodeSerializer (line 8) | class WorkerNodeSerializer(serializers.ModelSerializer):
    class Meta (line 24) | class Meta:
    method _get_load_from_context (line 30) | def _get_load_from_context(self, worker_id: int) -> dict | None:
    method get_status (line 35) | def get_status(self, obj) -> str:
    method get_info (line 57) | def get_info(self, obj) -> dict | None:
    method create (line 81) | def create(self, validated_data):
    method update (line 85) | def update(self, instance, validated_data):

FILE: backend/apps/engine/services/deploy_service.py
  function _read_script (line 23) | def _read_script(filename: str) -> str:
  function get_bootstrap_script (line 32) | def get_bootstrap_script() -> str:
  function get_deploy_script (line 37) | def get_deploy_script() -> str:
  function get_uninstall_script (line 49) | def get_uninstall_script() -> str:
  function get_start_agent_script (line 54) | def get_start_agent_script(

FILE: backend/apps/engine/services/engine_service.py
  class EngineService (line 15) | class EngineService:
    method __init__ (line 18) | def __init__(self):
    method get_engine (line 22) | def get_engine(self, engine_id: int) -> ScanEngine | None:
    method get_all_engines (line 34) | def get_all_engines(self):

FILE: backend/apps/engine/services/fingerprints/arl_service.py
  class ARLFingerprintService (line 16) | class ARLFingerprintService(BaseFingerprintService):
    method validate_fingerprint (line 21) | def validate_fingerprint(self, item: dict) -> bool:
    method to_model_data (line 39) | def to_model_data(self, item: dict) -> dict:
    method get_export_data (line 54) | def get_export_data(self) -> list:
    method export_to_yaml (line 74) | def export_to_yaml(self, output_path: str) -> int:
    method parse_yaml_import (line 91) | def parse_yaml_import(self, yaml_content: str) -> list:

FILE: backend/apps/engine/services/fingerprints/base.py
  class BaseFingerprintService (line 13) | class BaseFingerprintService:
    method validate_fingerprint (line 19) | def validate_fingerprint(self, item: dict) -> bool:
    method validate_fingerprints (line 31) | def validate_fingerprints(self, raw_data: list) -> tuple[list, list]:
    method to_model_data (line 49) | def to_model_data(self, item: dict) -> dict:
    method bulk_create (line 61) | def bulk_create(self, fingerprints: list) -> int:
    method batch_create_fingerprints (line 78) | def batch_create_fingerprints(self, raw_data: list) -> dict:
    method get_export_data (line 103) | def get_export_data(self) -> dict:
    method export_to_file (line 112) | def export_to_file(self, output_path: str) -> int:
    method get_fingerprint_version (line 129) | def get_fingerprint_version(self) -> str:

FILE: backend/apps/engine/services/fingerprints/ehole.py
  class EholeFingerprintService (line 10) | class EholeFingerprintService(BaseFingerprintService):
    method validate_fingerprint (line 15) | def validate_fingerprint(self, item: dict) -> bool:
    method to_model_data (line 33) | def to_model_data(self, item: dict) -> dict:
    method get_export_data (line 55) | def get_export_data(self) -> dict:

FILE: backend/apps/engine/services/fingerprints/fingerprinthub_service.py
  class FingerPrintHubFingerprintService (line 10) | class FingerPrintHubFingerprintService(BaseFingerprintService):
    method validate_fingerprint (line 15) | def validate_fingerprint(self, item: dict) -> bool:
    method to_model_data (line 43) | def to_model_data(self, item: dict) -> dict:
    method get_export_data (line 75) | def get_export_data(self) -> list:

FILE: backend/apps/engine/services/fingerprints/fingers_service.py
  class FingersFingerprintService (line 10) | class FingersFingerprintService(BaseFingerprintService):
    method validate_fingerprint (line 15) | def validate_fingerprint(self, item: dict) -> bool:
    method to_model_data (line 33) | def to_model_data(self, item: dict) -> dict:
    method get_export_data (line 55) | def get_export_data(self) -> list:

FILE: backend/apps/engine/services/fingerprints/goby.py
  class GobyFingerprintService (line 10) | class GobyFingerprintService(BaseFingerprintService):
    method validate_fingerprint (line 15) | def validate_fingerprint(self, item: dict) -> bool:
    method to_model_data (line 39) | def to_model_data(self, item: dict) -> dict:
    method get_export_data (line 68) | def get_export_data(self) -> list:

FILE: backend/apps/engine/services/fingerprints/wappalyzer.py
  class WappalyzerFingerprintService (line 10) | class WappalyzerFingerprintService(BaseFingerprintService):
    method validate_fingerprint (line 15) | def validate_fingerprint(self, item: dict) -> bool:
    method to_model_data (line 31) | def to_model_data(self, item: dict) -> dict:
    method get_export_data (line 59) | def get_export_data(self) -> dict:

FILE: backend/apps/engine/services/nuclei_template_repo_service.py
  class NucleiTemplateRepoService (line 42) | class NucleiTemplateRepoService:
    method __init__ (line 52) | def __init__(self, repository: NucleiTemplateRepository | None = None)...
    method _get_repo_obj (line 62) | def _get_repo_obj(self, repo_id: int):
    method _get_base_dir (line 79) | def _get_base_dir(self) -> Path:
    method remove_local_path_dir (line 93) | def remove_local_path_dir(self, repo_obj) -> None:
    method ensure_local_path (line 123) | def ensure_local_path(self, repo_obj) -> Path:
    method refresh_repo (line 163) | def refresh_repo(self, repo_id: int) -> Dict[str, Any]:
    method _get_fs_repo (line 280) | def _get_fs_repo(self, repo_id: int) -> TemplateFileRepository:
    method get_template_tree (line 298) | def get_template_tree(self, repo_id: int) -> List[Dict[str, Any]]:
    method get_template_content (line 310) | def get_template_content(self, repo_id: int, rel_path: str) -> Optiona...

FILE: backend/apps/engine/services/task_distributor.py
  class TaskDistributor (line 57) | class TaskDistributor:
    method __init__ (line 75) | def __init__(self):
    method get_online_workers (line 83) | def get_online_workers(self) -> list[WorkerNode]:
    method select_best_worker (line 104) | def select_best_worker(self) -> Optional[WorkerNode]:
    method _wait_for_submit_interval (line 233) | def _wait_for_submit_interval(self):
    method _build_docker_command (line 246) | def _build_docker_command(
    method _execute_docker_command (line 327) | def _execute_docker_command(
    method _execute_local_docker (line 352) | def _execute_local_docker(
    method _execute_ssh_docker (line 386) | def _execute_ssh_docker(
    method execute_scan_flow (line 449) | def execute_scan_flow(
    method execute_cleanup_on_all_workers (line 534) | def execute_cleanup_on_all_workers(
    method execute_delete_task (line 597) | def execute_delete_task(
  function get_task_distributor (line 677) | def get_task_distributor() -> TaskDistributor:

FILE: backend/apps/engine/services/wordlist_service.py
  class WordlistService (line 24) | class WordlistService:
    method __init__ (line 27) | def __init__(self) -> None:
    method get_queryset (line 31) | def get_queryset(self):
    method get_wordlist (line 35) | def get_wordlist(self, wordlist_id: int) -> Optional[Wordlist]:
    method get_wordlist_by_name (line 39) | def get_wordlist_by_name(self, name: str) -> Optional[Wordlist]:
    method create_wordlist (line 45) | def create_wordlist(
    method delete_wordlist (line 115) | def delete_wordlist(self, wordlist_id: int) -> bool:
    method _exists_by_name (line 131) | def _exists_by_name(self, name: str) -> bool:
    method get_wordlist_content (line 135) | def get_wordlist_content(self, wordlist_id: int) -> Optional[str]:
    method update_wordlist_content (line 148) | def update_wordlist_content(self, wordlist_id: int, content: str) -> O...

FILE: backend/apps/engine/services/worker_load_service.py
  class WorkerLoadService (line 19) | class WorkerLoadService:
    method __init__ (line 29) | def __init__(self):
    method redis (line 33) | def redis(self) -> redis.Redis:
    method _key (line 44) | def _key(self, worker_id: int) -> str:
    method update_load (line 48) | def update_load(self, worker_id: int, cpu_percent: float, memory_perce...
    method get_load (line 79) | def get_load(self, worker_id: int) -> Optional[Dict[str, Any]]:
    method get_all_loads (line 102) | def get_all_loads(self, worker_ids: list[int]) -> Dict[int, Dict[str, ...
    method delete_load (line 132) | def delete_load(self, worker_id: int) -> bool:
    method is_online (line 141) | def is_online(self, worker_id: int) -> bool:

FILE: backend/apps/engine/services/worker_service.py
  class WorkerService (line 15) | class WorkerService:
    method __init__ (line 18) | def __init__(self) -> None:
    method get_worker (line 24) | def get_worker(self, worker_id: int):
    method get_all_workers (line 28) | def get_all_workers(self):
    method update_status (line 34) | def update_status(self, worker_id: int, status: str) -> bool:
    method delete_worker (line 44) | def delete_worker(self, worker_id: int) -> bool:
    method register_worker (line 50) | def register_worker(self, name: str, is_local: bool = True):
    method remote_uninstall (line 68) | def remote_uninstall(
    method execute_remote_command (line 137) | def execute_remote_command(

FILE: backend/apps/engine/views/engine_views.py
  class ScanEngineViewSet (line 10) | class ScanEngineViewSet(viewsets.ModelViewSet):
    method __init__ (line 25) | def __init__(self, **kwargs):
    method get_queryset (line 29) | def get_queryset(self):

FILE: backend/apps/engine/views/fingerprints/arl.py
  class ARLFingerprintViewSet (line 17) | class ARLFingerprintViewSet(BaseFingerprintViewSet):
    method parse_import_data (line 57) | def parse_import_data(self, json_data) -> list:
    method get_export_filename (line 68) | def get_export_filename(self) -> str:
    method import_file (line 73) | def import_file(self, request):
    method export (line 111) | def export(self, request):

FILE: backend/apps/engine/views/fingerprints/base.py
  class BaseFingerprintViewSet (line 22) | class BaseFingerprintViewSet(viewsets.ModelViewSet):
    method get_queryset (line 66) | def get_queryset(self):
    method get_service (line 79) | def get_service(self):
    method parse_import_data (line 85) | def parse_import_data(self, json_data: dict) -> list:
    method get_export_filename (line 97) | def get_export_filename(self) -> str:
    method batch_create (line 107) | def batch_create(self, request):
    method import_file (line 136) | def import_file(self, request):
    method _parse_json_content (line 165) | def _parse_json_content(self, content: str):
    method bulk_delete (line 201) | def bulk_delete(self, request):
    method delete_all (line 219) | def delete_all(self, request):
    method export (line 230) | def export(self, request):

FILE: backend/apps/engine/views/fingerprints/ehole.py
  class EholeFingerprintViewSet (line 11) | class EholeFingerprintViewSet(BaseFingerprintViewSet):
    method parse_import_data (line 56) | def parse_import_data(self, json_data: dict) -> list:
    method get_export_filename (line 65) | def get_export_filename(self) -> str:

FILE: backend/apps/engine/views/fingerprints/fingerprinthub.py
  class FingerPrintHubFingerprintViewSet (line 11) | class FingerPrintHubFingerprintViewSet(BaseFingerprintViewSet):
    method parse_import_data (line 60) | def parse_import_data(self, json_data) -> list:
    method get_export_filename (line 71) | def get_export_filename(self) -> str:

FILE: backend/apps/engine/views/fingerprints/fingers.py
  class FingersFingerprintViewSet (line 11) | class FingersFingerprintViewSet(BaseFingerprintViewSet):
    method parse_import_data (line 56) | def parse_import_data(self, json_data) -> list:
    method get_export_filename (line 67) | def get_export_filename(self) -> str:

FILE: backend/apps/engine/views/fingerprints/goby.py
  class GobyFingerprintViewSet (line 11) | class GobyFingerprintViewSet(BaseFingerprintViewSet):
    method parse_import_data (line 50) | def parse_import_data(self, json_data) -> list:
    method get_export_filename (line 63) | def get_export_filename(self) -> str:

FILE: backend/apps/engine/views/fingerprints/wappalyzer.py
  class WappalyzerFingerprintViewSet (line 11) | class WappalyzerFingerprintViewSet(BaseFingerprintViewSet):
    method parse_import_data (line 57) | def parse_import_data(self, json_data: dict) -> list:
    method get_export_filename (line 73) | def get_export_filename(self) -> str:

FILE: backend/apps/engine/views/nuclei_template_repo_views.py
  class NucleiTemplateRepoViewSet (line 44) | class NucleiTemplateRepoViewSet(viewsets.ModelViewSet):
    method __init__ (line 66) | def __init__(self, *args, **kwargs) -> None:  # type: ignore[override]
    method perform_create (line 71) | def perform_create(self, serializer) -> None:  # type: ignore[override]
    method perform_destroy (line 81) | def perform_destroy(self, instance: NucleiTemplateRepo) -> None:  # ty...
    method refresh (line 95) | def refresh(self, request: Request, pk: str | None = None) -> Response:
    method templates_tree (line 140) | def templates_tree(self, request: Request, pk: str | None = None) -> R...
    method templates_content (line 185) | def templates_content(self, request: Request, pk: str | None = None) -...

FILE: backend/apps/engine/views/wordlist_views.py
  class WordlistViewSet (line 18) | class WordlistViewSet(viewsets.ViewSet):
    method __init__ (line 23) | def __init__(self, *args, **kwargs):
    method paginator (line 28) | def paginator(self):
    method list (line 37) | def list(self, request):
    method create (line 44) | def create(self, request):
    method destroy (line 73) | def destroy(self, request, pk=None):
    method download (line 94) | def download(self, request):
    method content (line 129) | def content(self, request, pk=None):

FILE: backend/apps/engine/views/worker_views.py
  class WorkerNodeViewSet (line 21) | class WorkerNodeViewSet(viewsets.ModelViewSet):
    method __init__ (line 37) | def __init__(self, **kwargs):
    method get_queryset (line 41) | def get_queryset(self):
    method get_serializer_context (line 45) | def get_serializer_context(self):
    method destroy (line 58) | def destroy(self, request, *args, **kwargs):
    method heartbeat (line 121) | def heartbeat(self, request, pk=None):
    method _trigger_remote_agent_update (line 202) | def _trigger_remote_agent_update(self, worker, target_version: str):
    method _set_worker_status (line 277) | def _set_worker_status(self, worker_id: int, status: str):
    method register (line 286) | def register(self, request):
    method config (line 330) | def config(self, request):

FILE: backend/apps/scan/apps.py
  class ScanConfig (line 4) | class ScanConfig(AppConfig):
    method ready (line 9) | def ready(self):

FILE: backend/apps/scan/configs/command_templates.py
  function get_supported_scan_types (line 284) | def get_supported_scan_types():
  function get_command_template (line 294) | def get_command_template(scan_type: str, tool_name: str) -> dict:

FILE: backend/apps/scan/flows/directory_scan_flow.py
  function calculate_directory_scan_timeout (line 44) | def calculate_directory_scan_timeout(
  function _get_max_workers (line 97) | def _get_max_workers(tool_config: dict, default: int = DEFAULT_MAX_WORKE...
  function _export_site_urls (line 108) | def _export_site_urls(
  function _generate_log_filename (line 144) | def _generate_log_filename(
  function _prepare_tool_wordlist (line 158) | def _prepare_tool_wordlist(tool_name: str, tool_config: dict) -> bool:
  function _build_scan_params (line 173) | def _build_scan_params(
  function _execute_batch (line 210) | def _execute_batch(
  function _run_scans_concurrently (line 265) | def _run_scans_concurrently(
  function directory_scan_flow (line 390) | def directory_scan_flow(

FILE: backend/apps/scan/flows/fingerprint_detect_flow.py
  function calculate_fingerprint_detect_timeout (line 34) | def calculate_fingerprint_detect_timeout(
  function _export_urls (line 59) | def _export_urls(
  function _run_fingerprint_detect (line 95) | def _run_fingerprint_detect(
  function fingerprint_detect_flow (line 219) | def fingerprint_detect_flow(
  function _build_empty_result (line 364) | def _build_empty_result(

FILE: backend/apps/scan/flows/initiate_scan_flow.py
  function _run_subflow_task (line 40) | def _run_subflow_task(scan_type: str, flow_func, flow_kwargs: dict):
  function initiate_scan_flow (line 54) | def initiate_scan_flow(

FILE: backend/apps/scan/flows/port_scan_flow.py
  function calculate_port_scan_timeout (line 34) | def calculate_port_scan_timeout(
  function _parse_port_count (line 76) | def _parse_port_count(tool_config: dict) -> int:
  function _export_hosts (line 135) | def _export_hosts(target_id: int, port_scan_dir: Path) -> tuple[str, int...
  function _run_scans_sequentially (line 173) | def _run_scans_sequentially(
  function port_scan_flow (line 299) | def port_scan_flow(

FILE: backend/apps/scan/flows/screenshot_flow.py
  function _parse_screenshot_config (line 38) | def _parse_screenshot_config(enabled_tools: dict) -> dict:
  function _map_url_sources_to_data_sources (line 47) | def _map_url_sources_to_data_sources(url_sources: list[str]) -> list[str]:
  function _collect_urls_from_provider (line 61) | def _collect_urls_from_provider(provider: TargetProvider) -> tuple[list[...
  function _collect_urls_from_database (line 73) | def _collect_urls_from_database(
  function _build_empty_result (line 83) | def _build_empty_result(scan_id: int, target_name: str) -> dict:
  function screenshot_flow (line 103) | def screenshot_flow(

FILE: backend/apps/scan/flows/site_scan_flow.py
  class ScanContext (line 39) | class ScanContext:
  function _count_file_lines (line 49) | def _count_file_lines(file_path: str) -> int:
  function _calculate_timeout_by_line_count (line 64) | def _calculate_timeout_by_line_count(
  function _export_site_urls (line 90) | def _export_site_urls(
  function _get_tool_timeout (line 127) | def _get_tool_timeout(tool_config: dict, urls_file: str) -> int:
  function _execute_single_tool (line 138) | def _execute_single_tool(
  function _run_scans_sequentially (line 213) | def _run_scans_sequentially(
  function _build_empty_result (line 261) | def _build_empty_result(
  function _aggregate_tool_results (line 293) | def _aggregate_tool_results(tool_stats: dict) -> tuple[int, int, int]:
  function _validate_flow_params (line 307) | def _validate_flow_params(
  function site_scan_flow (line 331) | def site_scan_flow(

FILE: backend/apps/scan/flows/subdomain_discovery_flow.py
  class ScanContext (line 57) | class ScanContext:
  function _validate_and_normalize_target (line 70) | def _validate_and_normalize_target(target_name: str) -> str:
  function _count_lines (line 81) | def _count_lines(file_path: str) -> int:
  function _merge_files (line 91) | def _merge_files(file_list: list, output_file: str) -> str:
  function _calculate_auto_timeout (line 110) | def _calculate_auto_timeout(file_path: str, multiplier: int = 3, default...
  function _run_single_tool (line 120) | def _run_single_tool(
  function _run_scans_parallel (line 168) | def _run_scans_parallel(
  function _generate_provider_config (line 249) | def _generate_provider_config(result_dir: Path, scan_id: int) -> Optiona...
  function _run_stage1_passive (line 265) | def _run_stage1_passive(ctx: ScanContext, enabled_tools: dict, provider_...
  function _run_stage2_bruteforce (line 300) | def _run_stage2_bruteforce(ctx: ScanContext, bruteforce_config: dict):
  function _run_stage3_permutation (line 354) | def _run_stage3_permutation(ctx: ScanContext, permutation_config: dict):
  function _run_stage4_resolve (line 454) | def _run_stage4_resolve(ctx: ScanContext, resolve_config: dict):
  function _save_to_database (line 492) | def _save_to_database(ctx: ScanContext) -> int:
  function _empty_result (line 515) | def _empty_result(scan_id: int, target: str, scan_workspace_dir: str) ->...
  function subdomain_discovery_flow (line 541) | def subdomain_discovery_flow(

FILE: backend/apps/scan/flows/url_fetch/domain_name_url_fetch_flow.py
  function domain_name_url_fetch_flow (line 34) | def domain_name_url_fetch_flow(

FILE: backend/apps/scan/flows/url_fetch/main_flow.py
  function _classify_tools (line 42) | def _classify_tools(enabled_tools: dict) -> tuple[dict, dict, dict, dict]:
  function _merge_and_deduplicate_urls (line 69) | def _merge_and_deduplicate_urls(result_files: list, url_fetch_dir: Path)...
  function _clean_urls_with_uro (line 92) | def _clean_urls_with_uro(
  function _validate_and_stream_save_urls (line 137) | def _validate_and_stream_save_urls(
  function _save_urls_to_database (line 218) | def _save_urls_to_database(merged_file: str, scan_id: int, target_id: in...
  function url_fetch_flow (line 241) | def url_fetch_flow(

FILE: backend/apps/scan/flows/url_fetch/sites_url_fetch_flow.py
  function _export_sites_file (line 22) | def _export_sites_file(target_id: int, scan_id: int, target_name: str, o...
  function sites_url_fetch_flow (line 56) | def sites_url_fetch_flow(

FILE: backend/apps/scan/flows/url_fetch/utils.py
  function calculate_timeout_by_line_count (line 16) | def calculate_timeout_by_line_count(
  function prepare_tool_execution (line 57) | def prepare_tool_execution(
  function run_tools_parallel (line 151) | def run_tools_parallel(

FILE: backend/apps/scan/flows/vuln_scan/endpoints_vuln_scan_flow.py
  function endpoints_vuln_scan_flow (line 35) | def endpoints_vuln_scan_flow(

FILE: backend/apps/scan/flows/vuln_scan/main_flow.py
  function _classify_vuln_tools (line 22) | def _classify_vuln_tools(enabled_tools: Dict[str, dict]) -> Tuple[Dict[s...
  function vuln_scan_flow (line 52) | def vuln_scan_flow(

FILE: backend/apps/scan/flows/vuln_scan/utils.py
  function calculate_timeout_by_line_count (line 11) | def calculate_timeout_by_line_count(

FILE: backend/apps/scan/handlers/initiate_scan_flow_handlers.py
  function on_initiate_scan_flow_running (line 22) | def on_initiate_scan_flow_running(flow: Flow, flow_run: FlowRun, state: ...
  function on_initiate_scan_flow_completed (line 101) | def on_initiate_scan_flow_completed(flow: Flow, flow_run: FlowRun, state...
  function on_initiate_scan_flow_failed (line 199) | def on_initiate_scan_flow_failed(flow: Flow, flow_run: FlowRun, state: S...

FILE: backend/apps/scan/handlers/scan_flow_handlers.py
  function _get_stage_from_flow_name (line 25) | def _get_stage_from_flow_name(flow_name: str) -> str | None:
  function on_scan_flow_running (line 38) | def on_scan_flow_running(flow: Flow, flow_run: FlowRun, state: State) ->...
  function on_scan_flow_completed (line 78) | def on_scan_flow_completed(flow: Flow, flow_run: FlowRun, state: State) ...
  function on_scan_flow_failed (line 132) | def on_scan_flow_failed(flow: Flow, flow_run: FlowRun, state: State) -> ...

FILE: backend/apps/scan/migrations/0001_initial.py
  class Migration (line 8) | class Migration(migrations.Migration):

FILE: backend/apps/scan/migrations/0002_add_cached_screenshots_count.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: backend/apps/scan/migrations/0003_add_wecom_fields.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: backend/apps/scan/models/scan_log_model.py
  class ScanLog (line 6) | class ScanLog(models.Model):
    class Level (line 9) | class Level(models.TextChoices):
    class Meta (line 31) | class Meta:
    method __str__ (line 40) | def __str__(self):

FILE: backend/apps/scan/models/scan_models.py
  class SoftDeleteManager (line 9) | class SoftDeleteManager(models.Manager):
    method get_queryset (line 12) | def get_queryset(self):
  class Scan (line 16) | class Scan(models.Model):
    class Meta (line 95) | class Meta:
    method __str__ (line 106) | def __str__(self):

FILE: backend/apps/scan/models/scheduled_scan_model.py
  class ScheduledScan (line 7) | class ScheduledScan(models.Model):
    class Meta (line 61) | class Meta:
    method __str__ (line 72) | def __str__(self):

FILE: backend/apps/scan/models/subfinder_provider_settings_model.py
  class SubfinderProviderSettings (line 9) | class SubfinderProviderSettings(models.Model):
    class Meta (line 28) | class Meta:
    method save (line 44) | def save(self, *args, **kwargs):
    method get_instance (line 49) | def get_instance(cls) -> 'SubfinderProviderSettings':
    method get_provider_config (line 57) | def get_provider_config(self, provider: str) -> dict:
    method is_provider_enabled (line 61) | def is_provider_enabled(self, provider: str) -> bool:

FILE: backend/apps/scan/notifications/consumers.py
  class NotificationConsumer (line 14) | class NotificationConsumer(AuthenticatedWebsocketConsumer):
    method __init__ (line 23) | def __init__(self, *args, **kwargs):
    method on_connect (line 27) | async def on_connect(self):
    method disconnect (line 55) | async def disconnect(self, close_code):
    method receive (line 76) | async def receive(self, text_data):
    method notification_message (line 98) | async def notification_message(self, event):
    method _heartbeat_loop (line 126) | async def _heartbeat_loop(self):

FILE: backend/apps/scan/notifications/models.py
  class NotificationSettings (line 14) | class NotificationSettings(models.Model):
    class Meta (line 38) | class Meta:
    method save (line 43) | def save(self, *args, **kwargs):
    method get_instance (line 48) | def get_instance(cls) -> 'NotificationSettings':
    method is_category_enabled (line 65) | def is_category_enabled(self, category: str) -> bool:
  class Notification (line 70) | class Notification(models.Model):
    class Meta (line 99) | class Meta:
    method __str__ (line 111) | def __str__(self):
    method cleanup_old_notifications (line 115) | def cleanup_old_notifications(cls) -> int:
    method save (line 121) | def save(self, *args, **kwargs):

FILE: backend/apps/scan/notifications/receivers.py
  function on_vulnerabilities_saved (line 16) | def on_vulnerabilities_saved(sender, items, scan_id, target_id, **kwargs):
  function on_worker_delete_failed (line 74) | def on_worker_delete_failed(sender, worker_name, message, **kwargs):
  function on_all_workers_high_load (line 86) | def on_all_workers_high_load(sender, worker_name, cpu, mem, **kwargs):

FILE: backend/apps/scan/notifications/repositories.py
  class NotificationSettingsData (line 18) | class NotificationSettingsData:
  class NotificationSettingsRepository (line 29) | class NotificationSettingsRepository:
    method get_settings (line 32) | def get_settings(self) -> NotificationSettings:
    method update_settings (line 36) | def update_settings(self, data: NotificationSettingsData) -> Notificat...
    method is_category_enabled (line 47) | def is_category_enabled(self, category: str) -> bool:
  class DjangoNotificationRepository (line 53) | class DjangoNotificationRepository:
    method get_filtered (line 56) | def get_filtered(
    method get_unread_count (line 80) | def get_unread_count(self) -> int:
    method mark_all_as_read (line 84) | def mark_all_as_read(self) -> int:
    method create (line 91) | def create(

FILE: backend/apps/scan/notifications/serializers.py
  class NotificationSerializer (line 8) | class NotificationSerializer(serializers.ModelSerializer):
    class Meta (line 9) | class Meta:

FILE: backend/apps/scan/notifications/services.py
  function push_to_external_channels (line 38) | def push_to_external_channels(notification: Notification) -> None:
  function _send_discord (line 71) | def _send_discord(notification: Notification, webhook_url: str) -> bool:
  function _send_wecom (line 105) | def _send_wecom(notification: Notification, webhook_url: str) -> bool:
  class NotificationSettingsService (line 144) | class NotificationSettingsService:
    method __init__ (line 147) | def __init__(self, repository: NotificationSettingsRepository | None =...
    method get_settings (line 150) | def get_settings(self) -> dict:
    method update_settings (line 165) | def update_settings(self, data: dict) -> dict:
  class NotificationService (line 199) | class NotificationService:
    method __init__ (line 202) | def __init__(self, repository: DjangoNotificationRepository | None = N...
    method get_notifications (line 205) | def get_notifications(self, level: str | None = None, unread: bool | N...
    method get_unread_count (line 208) | def get_unread_count(self) -> int:
    method mark_all_as_read (line 211) | def mark_all_as_read(self) -> int:
  function create_notification (line 215) | def create_notification(
  function _push_to_websocket (line 326) | def _push_to_websocket(notification: Notification) -> None:
  function _push_via_api_callback (line 346) | def _push_via_api_callback(notification: Notification, server_url: str) ...
  function _push_via_channel_layer (line 384) | def _push_via_channel_layer(notification: Notification) -> None:

FILE: backend/apps/scan/notifications/types.py
  class NotificationLevel (line 6) | class NotificationLevel(models.TextChoices):
  class NotificationCategory (line 14) | class NotificationCategory(models.TextChoices):

FILE: backend/apps/scan/notifications/views.py
  function _parse_bool (line 29) | def _parse_bool(value: str | None) -> bool | None:
  class NotificationCollectionView (line 49) | class NotificationCollectionView(APIView):
    method get (line 57) | def get(self, request: Request) -> Response:
  class NotificationUnreadCountView (line 95) | class NotificationUnreadCountView(APIView):
    method get (line 107) | def get(self, request: Request) -> Response:
  class NotificationMarkAllAsReadView (line 114) | class NotificationMarkAllAsReadView(APIView):
    method post (line 127) | def post(self, request: Request) -> Response:
  class NotificationSettingsView (line 134) | class NotificationSettingsView(APIView):
    method get (line 144) | def get(self, request: Request) -> Response:
    method put (line 150) | def put(self, request: Request) -> Response:
  function notification_callback (line 163) | def notification_callback(request):
  function _push_notification_to_websocket (line 208) | def _push_notification_to_websocket(data: dict):

FILE: backend/apps/scan/orchestrators/flow_orchestrator.py
  class FlowOrchestrator (line 23) | class FlowOrchestrator:
    method __init__ (line 30) | def __init__(self, engine_config: str):
    method _parse_config (line 73) | def _parse_config(self, engine_config: str) -> Dict:
    method _detect_scan_types (line 100) | def _detect_scan_types(self) -> List[str]:
    method is_scan_type_enabled (line 127) | def is_scan_type_enabled(self, scan_type: str) -> bool:
    method get_execution_stages (line 158) | def get_execution_stages(self):
    method get_flow_function (line 187) | def get_flow_function(self, scan_type: str) -> Optional[Callable]:

FILE: backend/apps/scan/providers/base.py
  class ProviderContext (line 20) | class ProviderContext:
  class TargetProvider (line 32) | class TargetProvider(ABC):
    method __init__ (line 48) | def __init__(self, context: Optional[ProviderContext] = None):
    method context (line 52) | def context(self) -> ProviderContext:
    method _expand_host (line 57) | def _expand_host(host: str) -> Iterator[str]:
    method iter_hosts (line 87) | def iter_hosts(self) -> Iterator[str]:
    method _iter_raw_hosts (line 93) | def _iter_raw_hosts(self) -> Iterator[str]:
    method iter_urls (line 98) | def iter_urls(self) -> Iterator[str]:
    method get_blacklist_filter (line 103) | def get_blacklist_filter(self) -> Optional['BlacklistFilter']:
    method target_id (line 108) | def target_id(self) -> Optional[int]:
    method scan_id (line 113) | def scan_id(self) -> Optional[int]:

FILE: backend/apps/scan/providers/database_provider.py
  class DatabaseTargetProvider (line 18) | class DatabaseTargetProvider(TargetProvider):
    method __init__ (line 32) | def __init__(self, target_id: int, context: Optional[ProviderContext] ...
    method iter_hosts (line 38) | def iter_hosts(self) -> Iterator[str]:
    method _iter_raw_hosts (line 47) | def _iter_raw_hosts(self) -> Iterator[str]:
    method iter_urls (line 70) | def iter_urls(self) -> Iterator[str]:
    method get_blacklist_filter (line 86) | def get_blacklist_filter(self) -> Optional['BlacklistFilter']:

FILE: backend/apps/scan/providers/list_provider.py
  class ListTargetProvider (line 12) | class ListTargetProvider(TargetProvider):
    method __init__ (line 36) | def __init__(
    method _iter_raw_hosts (line 74) | def _iter_raw_hosts(self) -> Iterator[str]:
    method iter_urls (line 78) | def iter_urls(self) -> Iterator[str]:
    method get_blacklist_filter (line 82) | def get_blacklist_filter(self) -> None:

FILE: backend/apps/scan/providers/pipeline_provider.py
  class StageOutput (line 15) | class StageOutput:
  class PipelineTargetProvider (line 37) | class PipelineTargetProvider(TargetProvider):
    method __init__ (line 58) | def __init__(
    method _iter_raw_hosts (line 76) | def _iter_raw_hosts(self) -> Iterator[str]:
    method iter_urls (line 80) | def iter_urls(self) -> Iterator[str]:
    method get_blacklist_filter (line 84) | def get_blacklist_filter(self) -> None:
    method previous_output (line 89) | def previous_output(self) -> StageOutput:

FILE: backend/apps/scan/providers/snapshot_provider.py
  class SnapshotTargetProvider (line 19) | class SnapshotTargetProvider(TargetProvider):
    method __init__ (line 66) | def __init__(
    method _iter_raw_hosts (line 90) | def _iter_raw_hosts(self) -> Iterator[str]:
    method iter_hosts (line 119) | def iter_hosts(self) -> Iterator[str]:
    method iter_urls (line 136) | def iter_urls(self) -> Iterator[str]:
    method get_blacklist_filter (line 168) | def get_blacklist_filter(self) -> None:
    method snapshot_type (line 173) | def snapshot_type(self) -> SnapshotType:

FILE: backend/apps/scan/providers/tests/test_common_properties.py
  class TestContextPropagation (line 24) | class TestContextPropagation:
    method test_property_4_list_provider_context_propagation (line 39) | def test_property_4_list_provider_context_propagation(self, target_id,...
    method test_property_4_database_provider_context_propagation (line 59) | def test_property_4_database_provider_context_propagation(self, target...
    method test_property_4_pipeline_provider_context_propagation (line 80) | def test_property_4_pipeline_provider_context_propagation(self, target...
    method test_property_4_snapshot_provider_context_propagation (line 101) | def test_property_4_snapshot_provider_context_propagation(self, target...
  class TestNonDatabaseProviderBlacklistFilter (line 122) | class TestNonDatabaseProviderBlacklistFilter:
    method test_property_5_list_provider_no_blacklist (line 134) | def test_property_5_list_provider_no_blacklist(self, targets):
    method test_property_5_pipeline_provider_no_blacklist (line 146) | def test_property_5_pipeline_provider_no_blacklist(self, hosts):
    method test_property_5_snapshot_provider_no_blacklist (line 157) | def test_property_5_snapshot_provider_no_blacklist(self):
  class TestCIDRExpansionConsistency (line 168) | class TestCIDRExpansionConsistency:
    method test_property_7_cidr_expansion_consistency (line 184) | def test_property_7_cidr_expansion_consistency(self, network_prefix, c...
    method test_cidr_expansion_with_multiple_cidrs (line 218) | def test_cidr_expansion_with_multiple_cidrs(self):
    method test_mixed_hosts_and_cidrs (line 242) | def test_mixed_hosts_and_cidrs(self):

FILE: backend/apps/scan/providers/tests/test_database_provider.py
  function valid_domain_strategy (line 20) | def valid_domain_strategy():
  class MockBlacklistFilter (line 33) | class MockBlacklistFilter:
    method __init__ (line 36) | def __init__(self, blocked_patterns: list):
    method is_allowed (line 39) | def is_allowed(self, target: str) -> bool:
  class TestDatabaseTargetProviderProperties (line 47) | class TestDatabaseTargetProviderProperties:
    method test_property_7_blacklist_filters_hosts (line 59) | def test_property_7_blacklist_filters_hosts(self, hosts, blocked_keywo...
  class TestDatabaseTargetProviderUnit (line 104) | class TestDatabaseTargetProviderUnit:
    method test_target_id_in_context (line 107) | def test_target_id_in_context(self):
    method test_context_propagation (line 113) | def test_context_propagation(self):
    method test_blacklist_filter_lazy_loading (line 121) | def test_blacklist_filter_lazy_loading(self):
    method test_nonexistent_target_returns_empty (line 147) | def test_nonexistent_target_returns_empty(self):

FILE: backend/apps/scan/providers/tests/test_list_provider.py
  function valid_domain_strategy (line 19) | def valid_domain_strategy():
  function valid_ip_strategy (line 33) | def valid_ip_strategy():
  function valid_url_strategy (line 45) | def valid_url_strategy():
  class TestListTargetProviderProperties (line 64) | class TestListTargetProviderProperties:
    method test_property_1_hosts_round_trip (line 69) | def test_property_1_hosts_round_trip(self, hosts):
    method test_property_1_urls_round_trip (line 86) | def test_property_1_urls_round_trip(self, urls):
    method test_property_1_combined_round_trip (line 106) | def test_property_1_combined_round_trip(self, hosts, urls):
  class TestListTargetProviderUnit (line 126) | class TestListTargetProviderUnit:
    method test_empty_lists (line 129) | def test_empty_lists(self):
    method test_blacklist_filter_returns_none (line 135) | def test_blacklist_filter_returns_none(self):
    method test_target_id_association (line 140) | def test_target_id_association(self):
    method test_context_propagation (line 146) | def test_context_propagation(self):

FILE: backend/apps/scan/providers/tests/test_pipeline_provider.py
  function valid_domain_strategy (line 19) | def valid_domain_strategy():
  function valid_ip_strategy (line 32) | def valid_ip_strategy():
  function valid_url_strategy (line 44) | def valid_url_strategy():
  class TestPipelineTargetProviderProperties (line 63) | class TestPipelineTargetProviderProperties:
    method test_property_3_hosts_round_trip (line 68) | def test_property_3_hosts_round_trip(self, hosts):
    method test_property_3_urls_round_trip (line 85) | def test_property_3_urls_round_trip(self, urls):
    method test_property_3_combined_round_trip (line 105) | def test_property_3_combined_round_trip(self, hosts, urls):
  class TestPipelineTargetProviderUnit (line 124) | class TestPipelineTargetProviderUnit:
    method test_empty_stage_output (line 127) | def test_empty_stage_output(self):
    method test_blacklist_filter_returns_none (line 135) | def test_blacklist_filter_returns_none(self):
    method test_target_id_association (line 141) | def test_target_id_association(self):
    method test_context_propagation (line 147) | def test_context_propagation(self):
    method test_previous_output_property (line 156) | def test_previous_output_property(self):
    method test_stage_output_with_metadata (line 165) | def test_stage_output_with_metadata(self):

FILE: backend/apps/scan/providers/tests/test_snapshot_provider.py
  class TestSnapshotTargetProvider (line 11) | class TestSnapshotTargetProvider:
    method test_init_with_scan_id_and_type (line 14) | def test_init_with_scan_id_and_type(self):
    method test_init_with_context (line 25) | def test_init_with_context(self):
    method test_iter_hosts_subdomain (line 39) | def test_iter_hosts_subdomain(self, mock_service_class):
    method test_iter_hosts_host_port (line 65) | def test_iter_hosts_host_port(self, mock_service_class):
    method test_iter_urls_website (line 97) | def test_iter_urls_website(self, mock_service_class):
    method test_iter_urls_endpoint (line 123) | def test_iter_urls_endpoint(self, mock_service_class):
    method test_iter_hosts_unsupported_type (line 152) | def test_iter_hosts_unsupported_type(self):
    method test_iter_urls_unsupported_type (line 162) | def test_iter_urls_unsupported_type(self):
    method test_get_blacklist_filter (line 172) | def test_get_blacklist_filter(self):
    method test_context_propagation (line 181) | def test_context_propagation(self):

FILE: backend/apps/scan/repositories/django_scan_repository.py
  class DjangoScanRepository (line 26) | class DjangoScanRepository:
    method get_by_id (line 32) | def get_by_id(self,
    method get_by_id_for_update (line 66) | def get_by_id_for_update(self, scan_id: int) -> Scan | None:
    method exists (line 90) | def exists(self, scan_id: int) -> bool:
    method create (line 103) | def create(self,
    method bulk_create (line 139) | def bulk_create(self, scans: List[Scan]) -> List[Scan]:
    method soft_delete_by_ids (line 154) | def soft_delete_by_ids(self, scan_ids: List[int]) -> int:
    method hard_delete_by_ids (line 184) | def hard_delete_by_ids(self, scan_ids: List[int]) -> Tuple[int, Dict[s...
    method get_all (line 234) | def get_all(self, prefetch_relations: bool = True) -> QuerySet[Scan]:
    method get_statistics (line 250) | def get_statistics(self) -> dict:
    method update_status (line 304) | def update_status(self,
    method append_container_id (line 355) | def append_container_id(self, scan_id: int, container_id: str) -> bool:
    method update_worker (line 395) | def update_worker(self, scan_id: int, worker_id: int) -> bool:
    method update_cached_stats (line 420) | def update_cached_stats(self, scan_id: int) -> dict | None:
    method update_status_if_match (line 506) | def update_status_if_match(self,
    method update_progress (line 569) | def update_progress(

FILE: backend/apps/scan/repositories/scheduled_scan_repository.py
  class ScheduledScanDTO (line 22) | class ScheduledScanDTO:
    method __post_init__ (line 45) | def __post_init__(self):
  class DjangoScheduledScanRepository (line 53) | class DjangoScheduledScanRepository:
    method get_by_id (line 63) | def get_by_id(self, scheduled_scan_id: int) -> Optional[ScheduledScan]:
    method get_queryset (line 70) | def get_queryset(self):
    method get_all (line 79) | def get_all(self, page: int = 1, page_size: int = 10) -> Tuple[List[Sc...
    method get_enabled (line 94) | def get_enabled(self) -> List[ScheduledScan]:
    method create (line 102) | def create(self, dto: ScheduledScanDTO) -> ScheduledScan:
    method update (line 128) | def update(self, scheduled_scan_id: int, dto: ScheduledScanDTO) -> Opt...
    method update_next_run_time (line 179) | def update_next_run_time(self, scheduled_scan_id: int, next_run_time: ...
    method increment_run_count (line 186) | def increment_run_count(self, scheduled_scan_id: int) -> bool:
    method toggle_enabled (line 195) | def toggle_enabled(self, scheduled_scan_id: int, enabled: bool) -> bool:
    method hard_delete (line 202) | def hard_delete(self, scheduled_scan_id: int) -> bool:

FILE: backend/apps/scan/scripts/run_cleanup.py
  function cleanup_results (line 26) | def cleanup_results(results_dir: str, retention_days: int) -> dict:
  function main (line 84) | def main():

FILE: backend/apps/scan/scripts/run_delete_scans.py
  function hard_delete_scans (line 21) | def hard_delete_scans(scan_ids: list[int]) -> dict:
  function main (line 55) | def main():

FILE: backend/apps/scan/scripts/run_initiate_scan.py
  function diagnose_prefect_environment (line 14) | def diagnose_prefect_environment():
  function main (line 117) | def main():

FILE: backend/apps/scan/serializers/mixins.py
  class DuplicateKeyLoader (line 7) | class DuplicateKeyLoader(yaml.SafeLoader):
  function _check_duplicate_keys (line 12) | def _check_duplicate_keys(loader, node, deep=False):
  class ScanConfigValidationMixin (line 32) | class ScanConfigValidationMixin:
    method validate_configuration (line 35) | def validate_configuration(self, value):
    method validate_engine_ids (line 47) | def validate_engine_ids(self, value):
    method validate_engine_names (line 53) | def validate_engine_names(self, value):

FILE: backend/apps/scan/serializers/scan_log_serializers.py
  class ScanLogSerializer (line 8) | class ScanLogSerializer(serializers.ModelSerializer):
    class Meta (line 11) | class Meta:

FILE: backend/apps/scan/serializers/scan_serializers.py
  class ScanSerializer (line 9) | class ScanSerializer(serializers.ModelSerializer):
    class Meta (line 13) | class Meta:
    method get_target_name (line 25) | def get_target_name(self, obj):
  class ScanHistorySerializer (line 29) | class ScanHistorySerializer(serializers.ModelSerializer):
    class Meta (line 39) | class Meta:
    method get_summary (line 47) | def get_summary(self, obj):
  class QuickScanSerializer (line 66) | class QuickScanSerializer(ScanConfigValidationMixin, serializers.Seriali...
    method validate_targets (line 79) | def validate_targets(self, value):
  class InitiateScanSerializer (line 94) | class InitiateScanSerializer(ScanConfigValidationMixin, serializers.Seri...
    method validate (line 103) | def validate(self, data):

FILE: backend/apps/scan/serializers/scheduled_scan_serializers.py
  class ScheduledScanSerializer (line 9) | class ScheduledScanSerializer(serializers.ModelSerializer):
    class Meta (line 18) | class Meta:
    method get_scan_mode (line 37) | def get_scan_mode(self, obj):
  class CreateScheduledScanSerializer (line 41) | class CreateScheduledScanSerializer(ScanConfigValidationMixin, serialize...
    method validate (line 53) | def validate(self, data):
  class UpdateScheduledScanSerializer (line 65) | class UpdateScheduledScanSerializer(serializers.Serializer):
    method validate_engine_ids (line 75) | def validate_engine_ids(self, value):
  class ToggleScheduledScanSerializer (line 81) | class ToggleScheduledScanSerializer(serializers.Serializer):

FILE: backend/apps/scan/serializers/subfinder_provider_settings_serializers.py
  class SubfinderProviderSettingsSerializer (line 6) | class SubfinderProviderSettingsSerializer(serializers.Serializer):
    method to_internal_value (line 23) | def to_internal_value(self, data):
    method to_representation (line 51) | def to_representation(self, instance):

FILE: backend/apps/scan/services/quick_scan_service.py
  class ParsedInputDTO (line 26) | class ParsedInputDTO:
  class QuickScanService (line 43) | class QuickScanService:
    method __init__ (line 46) | def __init__(self):
    method parse_inputs (line 51) | def parse_inputs(self, inputs: List[str]) -> List[ParsedInputDTO]:
    method _parse_url_input (line 93) | def _parse_url_input(self, url_str: str, line_number: int) -> ParsedIn...
    method _parse_target_input (line 129) | def _parse_target_input(
    method process_quick_scan (line 170) | def process_quick_scan(
    method create_assets_from_parsed_inputs (line 217) | def create_assets_from_parsed_inputs(

FILE: backend/apps/scan/services/scan_control_service.py
  class ScanControlService (line 23) | class ScanControlService:
    method __init__ (line 33) | def __init__(self):
    method _stop_containers (line 39) | def _stop_containers(
    method delete_scans_two_phase (line 116) | def delete_scans_two_phase(self, scan_ids: List[int]) -> dict:
    method _async_cleanup_and_hard_delete (line 165) | def _async_cleanup_and_hard_delete(
    method stop_scan (line 207) | def stop_scan(self, scan_id: int) -> tuple[bool, int]:

FILE: backend/apps/scan/services/scan_creation_service.py
  class ScanCreationService (line 34) | class ScanCreationService:
    method __init__ (line 45) | def __init__(self):
    method prepare_initiate_scan (line 60) | def prepare_initiate_scan(
    method prepare_initiate_scan_multi_engine (line 146) | def prepare_initiate_scan_multi_engine(
    method _generate_scan_workspace_dir (line 246) | def _generate_scan_workspace_dir(self) -> str:
    method create_scans (line 280) | def create_scans(
    method _distribute_scans_to_workers (line 371) | def _distribute_scans_to_workers(self, scan_data: List[dict]):

FILE: backend/apps/scan/services/scan_service.py
  class ScanService (line 30) | class ScanService:
    method __init__ (line 51) | def __init__(self):
    method get_scan (line 69) | def get_scan(self, scan_id: int, prefetch_relations: bool) -> Scan | N...
    method get_all_scans (line 83) | def get_all_scans(self, prefetch_relations: bool = True):
    method prepare_initiate_scan (line 86) | def prepare_initiate_scan(
    method prepare_initiate_scan_multi_engine (line 99) | def prepare_initiate_scan_multi_engine(
    method create_scans (line 115) | def create_scans(
    method update_status (line 130) | def update_status(
    method update_status_if_match (line 142) | def update_status_if_match(
    method update_cached_stats (line 154) | def update_cached_stats(self, scan_id: int) -> dict | None:
    method init_stage_progress (line 160) | def init_stage_progress(self, scan_id: int, stages: list[str]) -> bool:
    method start_stage (line 164) | def start_stage(self, scan_id: int, stage: str) -> bool:
    method complete_stage (line 168) | def complete_stage(self, scan_id: int, stage: str, detail: str | None ...
    method fail_stage (line 172) | def fail_stage(self, scan_id: int, stage: str, error: str | None = Non...
    method cancel_running_stages (line 176) | def cancel_running_stages(self, scan_id: int, final_status: str = "can...
    method add_command_to_scan (line 181) | def add_command_to_scan(self, scan_id: int, stage_name: str, tool_name...
    method delete_scans_two_phase (line 227) | def delete_scans_two_phase(self, scan_ids: List[int]) -> dict:
    method stop_scan (line 231) | def stop_scan(self, scan_id: int) -> tuple[bool, int]:
    method hard_delete_scans (line 235) | def hard_delete_scans(self, scan_ids: List[int]) -> tuple[int, Dict[st...
    method get_statistics (line 251) | def get_statistics(self) -> dict:

FILE: backend/apps/scan/services/scan_state_service.py
  class ScanStateService (line 21) | class ScanStateService:
    method __init__ (line 32) | def __init__(self):
    method update_status (line 38) | def update_status(
    method update_status_if_match (line 81) | def update_status_if_match(
    method update_cached_stats (line 135) | def update_cached_stats(self, scan_id: int) -> dict | None:
    method update_progress (line 163) | def update_progress(
    method init_stage_progress (line 203) | def init_stage_progress(self, scan_id: int, stages: list[str]) -> bool:
    method start_stage (line 225) | def start_stage(self, scan_id: int, stage: str) -> bool:
    method complete_stage (line 273) | def complete_stage(
    method fail_stage (line 343) | def fail_stage(
    method cancel_running_stages (line 390) | def cancel_running_stages(self, scan_id: int, final_status: str = "can...

FILE: backend/apps/scan/services/scan_stats_service.py
  class ScanStatsService (line 17) | class ScanStatsService:
    method __init__ (line 26) | def __init__(self):
    method get_statistics (line 32) | def get_statistics(self) -> dict:

FILE: backend/apps/scan/services/scheduled_scan_service.py
  class ScheduledScanService (line 25) | class ScheduledScanService:
    method __init__ (line 34) | def __init__(self):
    method get_by_id (line 41) | def get_by_id(self, scheduled_scan_id: int) -> Optional[ScheduledScan]:
    method get_queryset (line 45) | def get_queryset(self):
    method get_all (line 49) | def get_all(self, page: int = 1, page_size: int = 10) -> Tuple[List[Sc...
    method create (line 55) | def create(self, dto: ScheduledScanDTO) -> ScheduledScan:
    method create_with_configuration (line 110) | def create_with_configuration(self, dto: ScheduledScanDTO) -> Schedule...
    method _validate_create_dto (line 149) | def _validate_create_dto(self, dto: ScheduledScanDTO) -> None:
    method _validate_create_dto_with_configuration (line 162) | def _validate_create_dto_with_configuration(self, dto: ScheduledScanDT...
    method _validate_base_dto (line 170) | def _validate_base_dto(self, dto: ScheduledScanDTO) -> None:
    method update (line 203) | def update(self, scheduled_scan_id: int, dto: ScheduledScanDTO) -> Opt...
    method toggle_enabled (line 258) | def toggle_enabled(self, scheduled_scan_id: int, enabled: bool) -> bool:
    method record_run (line 287) | def record_run(self, scheduled_scan_id: int) -> bool:
    method delete (line 312) | def delete(self, scheduled_scan_id: int) -> bool:
    method trigger_due_scans (line 326) | def trigger_due_scans(self) -> int:
    method _trigger_scan_now (line 379) | def _trigger_scan_now(self, scheduled_scan: ScheduledScan) -> int:
    method _calculate_next_run_time (line 414) | def _calculate_next_run_time(self, scheduled_scan: ScheduledScan) -> O...

FILE: backend/apps/scan/services/subfinder_provider_config_service.py
  class SubfinderProviderConfigService (line 18) | class SubfinderProviderConfigService:
    method generate (line 33) | def generate(self, output_dir: str) -> Optional[str]:
    method _build_provider_value (line 82) | def _build_provider_value(self, provider: str, config: dict) -> Option...
    method cleanup (line 127) | def cleanup(self, config_path: str) -> None:

FILE: backend/apps/scan/services/target_export_service.py
  class DataSource (line 24) | class DataSource:
  function create_export_service (line 32) | def create_export_service(target_id: int) -> 'TargetExportService':
  function _iter_default_urls_from_target (line 49) | def _iter_default_urls_from_target(
  function _iter_urls_with_fallback (line 113) | def _iter_urls_with_fallback(
  function get_urls_with_fallback (line 195) | def get_urls_with_fallback(
  function export_urls_with_fallback (line 246) | def export_urls_with_fallback(
  class TargetExportService (line 308) | class TargetExportService:
    method __init__ (line 327) | def __init__(self, blacklist_filter: Optional[BlacklistFilter] = None):
    method export_urls (line 336) | def export_urls(
    method generate_default_urls (line 411) | def generate_default_urls(
    method export_hosts (line 459) | def export_hosts(
    method _export_domains (line 536) | def _export_domains(
    method _export_ip (line 576) | def _export_ip(self, target_name: str, output_path: Path) -> int:
    method _export_cidr (line 584) | def _export_cidr(self, target_name: str, output_path: Path) -> int:
    method _should_write_target (line 609) | def _should_write_target(self, target: str) -> bool:

FILE: backend/apps/scan/tasks/directory_scan/export_sites_task.py
  function export_sites_task (line 25) | def export_sites_task(
  function _export_with_provider (line 90) | def _export_with_provider(output_file: str, provider: TargetProvider) ->...

FILE: backend/apps/scan/tasks/directory_scan/run_and_stream_save_directories_task.py
  class ServiceSet (line 44) | class ServiceSet:
    method create_default (line 53) | def create_default(cls) -> "ServiceSet":
  function _parse_and_validate_line (line 61) | def _parse_and_validate_line(line: str) -> Optional[dict]:
  function _parse_ffuf_stream_output (line 110) | def _parse_ffuf_stream_output(
  function _save_batch_with_retry (line 174) | def _save_batch_with_retry(
  function _save_batch (line 253) | def _save_batch(
  function run_and_stream_save_directories_task (line 313) | def run_and_stream_save_directories_task(

FILE: backend/apps/scan/tasks/fingerprint_detect/export_urls_task.py
  function export_urls_for_fingerprint_task (line 27) | def export_urls_for_fingerprint_task(
  function _export_with_provider (line 86) | def _export_with_provider(output_file: str, provider: TargetProvider) ->...

FILE: backend/apps/scan/tasks/fingerprint_detect/run_xingfinger_task.py
  function parse_xingfinger_line (line 23) | def parse_xingfinger_line(line: str) -> dict | None:
  function bulk_merge_website_fields (line 58) | def bulk_merge_website_fields(
  function _parse_xingfinger_stream_output (line 147) | def _parse_xingfinger_stream_output(
  function run_xingfinger_and_stream_update_tech_task (line 193) | def run_xingfinger_and_stream_update_tech_task(
  function _process_batch (line 311) | def _process_batch(

FILE: backend/apps/scan/tasks/port_scan/export_hosts_task.py
  function export_hosts_task (line 25) | def export_hosts_task(

FILE: backend/apps/scan/tasks/port_scan/run_and_stream_save_ports_task.py
  class ServiceSet (line 49) | class ServiceSet:
    method create_default (line 58) | def create_default(cls) -> "ServiceSet":
  function _save_batch_with_retry (line 66) | def _save_batch_with_retry(
  function _save_batch (line 119) | def _save_batch(
  function _parse_and_validate_line (line 193) | def _parse_and_validate_line(line: str) -> Optional[PortScanRecord]:
  function _parse_naabu_stream_output (line 257) | def _parse_naabu_stream_output(
  function _validate_task_parameters (line 354) | def _validate_task_parameters(cmd: str, target_id: int, scan_id: int, cw...
  function _accumulate_batch_stats (line 381) | def _accumulate_batch_stats(total_stats: dict, batch_result: dict) -> None:
  function _process_batch (line 395) | def _process_batch(
  function _process_records_in_batches (line 431) | def _process_records_in_batches(
  function _build_final_result (line 518) | def _build_final_result(stats: dict) -> dict:
  function _cleanup_resources (line 549) | def _cleanup_resources(data_generator) -> None:
  function run_and_stream_save_ports_task (line 590) | def run_and_stream_save_ports_task(
  function _raise_if_cancelled (line 693) | def _raise_if_cancelled(scan_id: int) -> None:

FILE: backend/apps/scan/tasks/port_scan/types.py
  class PortScanRecord (line 10) | class PortScanRecord(TypedDict):

FILE: backend/apps/scan/tasks/screenshot/capture_screenshots_task.py
  function _run_async (line 14) | def _run_async(coro):
  function _save_screenshot_with_retry (line 33) | def _save_screenshot_with_retry(
  function _capture_and_save_screenshots (line 81) | async def _capture_and_save_screenshots(
  function capture_screenshots_task (line 144) | def capture_screenshots_task(

FILE: backend/apps/scan/tasks/site_scan/export_site_urls_task.py
  function _generate_urls_from_port (line 27) | def _generate_urls_from_port(host: str, port: int) -> list[str]:
  function export_site_urls_task (line 44) | def export_site_urls_task(
  function _export_site_urls_legacy (line 126) | def _export_site_urls_legacy(target_id: int, output_file: str, batch_siz...

FILE: backend/apps/scan/tasks/site_scan/run_and_stream_save_websites_task.py
  class ServiceSet (line 47) | class ServiceSet:
    method create_default (line 56) | def create_default(cls) -> 'ServiceSet':
  function _sanitize_string (line 64) | def _sanitize_string(value: str) -> str:
  function normalize_url (line 76) | def normalize_url(url: str) -> str:
  function _extract_hostname (line 131) | def _extract_hostname(url: str) -> str:
  class HttpxRecord (line 154) | class HttpxRecord:
    method __init__ (line 157) | def __init__(self, data: Dict[str, Any]):
  function _save_batch_with_retry (line 177) | def _save_batch_with_retry(
  function _save_batch (line 254) | def _save_batch(
  function _parse_and_validate_line (line 358) | def _parse_and_validate_line(line: str) -> Optional[HttpxRecord]:
  function _parse_httpx_stream_output (line 406) | def _parse_httpx_stream_output(
  function _validate_task_parameters (line 470) | def _validate_task_parameters(cmd: str, target_id: int, scan_id: int, cw...
  function _accumulate_batch_stats (line 497) | def _accumulate_batch_stats(total_stats: dict, batch_result: dict) -> None:
  function _process_batch (line 509) | def _process_batch(
  function _process_records_in_batches (line 545) | def _process_records_in_batches(
  function _build_final_result (line 615) | def _build_final_result(stats: dict) -> dict:
  function _cleanup_resources (line 644) | def _cleanup_resources(data_generator) -> None:
  function run_and_stream_save_websites_task (line 663) | def run_and_stream_save_websites_task(

FILE: backend/apps/scan/tasks/subdomain_discovery/merge_and_validate_task.py
  function _count_file_lines (line 34) | def _count_file_lines(file_path: str) -> int:
  function _calculate_timeout (line 48) | def _calculate_timeout(total_lines: int) -> int:
  function _validate_input_files (line 55) | def _validate_input_files(result_files: List[str]) -> List[str]:
  function merge_and_validate_task (line 68) | def merge_and_validate_task(result_files: List[str], result_dir: str) ->...

FILE: backend/apps/scan/tasks/subdomain_discovery/run_subdomain_discovery_task.py
  function run_subdomain_discovery_task (line 20) | def run_subdomain_discovery_task(

FILE: backend/apps/scan/tasks/subdomain_discovery/save_domains_task.py
  class ServiceSet (line 22) | class ServiceSet:
    method create_default (line 31) | def create_default(cls) -> 'ServiceSet':
  function save_domains_task (line 43) | def save_domains_task(
  function _save_batch_with_retry (line 170) | def _save_batch_with_retry(

FILE: backend/apps/scan/tasks/tests/test_task_backward_compatibility.py
  function valid_domain_strategy (line 23) | def valid_domain_strategy():
  class TestExportHostsTaskBackwardCompatibility (line 36) | class TestExportHostsTaskBackwardCompatibility:
    method test_property_8_legacy_mode_creates_database_provider (line 44) | def test_property_8_legacy_mode_creates_database_provider(self, target...
    method test_legacy_mode_with_provider_parameter (line 98) | def test_legacy_mode_with_provider_parameter(self):
    method test_error_when_no_parameters (line 127) | def test_error_when_no_parameters(self):
  class TestExportSiteUrlsTaskBackwardCompatibility (line 140) | class TestExportSiteUrlsTaskBackwardCompatibility:
    method test_property_8_legacy_mode_uses_traditional_logic (line 143) | def test_property_8_legacy_mode_uses_traditional_logic(self):
    method test_provider_mode_uses_provider_logic (line 201) | def test_provider_mode_uses_provider_logic(self):
    method test_error_when_no_parameters (line 230) | def test_error_when_no_parameters(self):

FILE: backend/apps/scan/tasks/url_fetch/clean_urls_task.py
  function clean_urls_task (line 27) | def clean_urls_task(

FILE: backend/apps/scan/tasks/url_fetch/export_sites_task.py
  function export_sites_task (line 30) | def export_sites_task(
  function _export_with_provider (line 95) | def _export_with_provider(output_file: str, provider: TargetProvider) ->...

FILE: backend/apps/scan/tasks/url_fetch/merge_and_deduplicate_urls_task.py
  function merge_and_deduplicate_urls_task (line 24) | def merge_and_deduplicate_urls_task(

FILE: backend/apps/scan/tasks/url_fetch/run_and_stream_save_urls_task.py
  class ServiceSet (line 39) | class ServiceSet:
    method create_default (line 48) | def create_default(cls) -> "ServiceSet":
  function _sanitize_string (line 55) | def _sanitize_string(value: str) -> str:
  function _extract_hostname (line 67) | def _extract_hostname(url: str) -> str:
  class HttpxRecord (line 90) | class HttpxRecord:
    method __init__ (line 93) | def __init__(self, data: Dict[str, Any]):
  function _parse_and_validate_line (line 113) | def _parse_and_validate_line(line: str) -> Optional[HttpxRecord]:
  function _parse_httpx_stream_output (line 153) | def _parse_httpx_stream_output(
  function _validate_task_parameters (line 221) | def _validate_task_parameters(cmd: str, target_id: int, scan_id: int, cw...
  function _build_final_result (line 248) | def _build_final_result(stats: dict) -> dict:
  function _cleanup_resources (line 277) | def _cleanup_resources(data_generator) -> None:
  function _save_batch_with_retry (line 293) | def _save_batch_with_retry(
  function _save_batch (line 370) | def _save_batch(
  function _accumulate_batch_stats (line 467) | def _accumulate_batch_stats(total_stats: dict, batch_result: dict) -> None:
  function _process_batch (line 479) | def _process_batch(
  function _process_records_in_batches (line 515) | def _process_records_in_batches(
  function run_and_stream_save_urls_task (line 586) | def run_and_stream_save_urls_task(

FILE: backend/apps/scan/tasks/url_fetch/run_url_fetcher_task.py
  function run_url_fetcher_task (line 24) | def run_url_fetcher_task(

FILE: backend/apps/scan/tasks/url_fetch/save_urls_task.py
  class ParsedURL (line 19) | class ParsedURL:
  function _parse_url (line 28) | def _parse_url(url: str) -> Optional[ParsedURL]:
  function save_urls_task (line 78) | def save_urls_task(

FILE: backend/apps/scan/tasks/vuln_scan/export_endpoints_task.py
  function export_endpoints_task (line 29) | def export_endpoints_task(
  function _export_with_provider (line 91) | def _export_with_provider(output_file: str, provider: TargetProvider) ->...

FILE: backend/apps/scan/tasks/vuln_scan/run_and_stream_save_dalfox_vulns_task.py
  class ServiceSet (line 44) | class ServiceSet:
    method create_default (line 53) | def create_default(cls) -> "ServiceSet":
  function _validate_task_parameters (line 60) | def _validate_task_parameters(cmd: str, target_id: int, scan_id: int, cw...
  function _map_severity (line 75) | def _map_severity(raw: Optional[str]) -> str:
  function _parse_and_validate_line (line 90) | def _parse_and_validate_line(line: str) -> Optional[dict]:
  function _parse_dalfox_stream_output (line 154) | def _parse_dalfox_stream_output(
  function _save_batch (line 207) | def _save_batch(
  function _save_batch_with_retry (line 245) | def _save_batch_with_retry(
  function _accumulate_batch_stats (line 285) | def _accumulate_batch_stats(total_stats: dict, batch_result: dict) -> None:
  function _process_batch (line 290) | def _process_batch(
  function _process_records_in_batches (line 312) | def _process_records_in_batches(
  function _build_final_result (line 364) | def _build_final_result(stats: dict) -> dict:
  function _cleanup_resources (line 384) | def _cleanup_resources(data_generator) -> None:
  function run_and_stream_save_dalfox_vulns_task (line 401) | def run_and_stream_save_dalfox_vulns_task(
  function _raise_if_cancelled (line 470) | def _raise_if_cancelled(scan_id: int) -> None:

FILE: backend/apps/scan/tasks/vuln_scan/run_and_stream_save_nuclei_vulns_task.py
  class ServiceSet (line 41) | class ServiceSet:
    method create_default (line 47) | def create_default(cls) -> "ServiceSet":
  function _validate_task_parameters (line 54) | def _validate_task_parameters(cmd: str, target_id: int, scan_id: int, cw...
  function _map_severity (line 69) | def _map_severity(raw: Optional[str]) -> str:
  function _parse_and_validate_line (line 83) | def _parse_and_validate_line(line: str) -> Optional[dict]:
  function _parse_nuclei_stream_output (line 156) | def _parse_nuclei_stream_output(
  function _save_batch (line 209) | def _save_batch(
  function _save_batch_with_retry (line 247) | def _save_batch_with_retry(
  function _accumulate_batch_stats (line 287) | def _accumulate_batch_stats(total_stats: dict, batch_result: dict) -> None:
  function _process_batch (line 292) | def _process_batch(
  function _process_records_in_batches (line 314) | def _process_records_in_batches(
  function _build_final_result (line 366) | def _build_final_result(stats: dict) -> dict:
  function _cleanup_resources (line 386) | def _cleanup_resources(data_generator) -> None:
  function run_and_stream_save_nuclei_vulns_task (line 403) | def run_and_stream_save_nuclei_vulns_task(
  function _raise_if_cancelled (line 472) | def _raise_if_cancelled(scan_id: int) -> None:

FILE: backend/apps/scan/tasks/vuln_scan/run_vuln_tool_task.py
  function run_vuln_tool_task (line 25) | def run_vuln_tool_task(

FILE: backend/apps/scan/utils/command_builder.py
  function build_scan_command (line 12) | def build_scan_command(

FILE: backend/apps/scan/utils/command_executor.py
  function _get_command_tracker (line 36) | def _get_command_tracker(tool_name: str, command: str):
  function _wait_for_system_load (line 60) | def _wait_for_system_load() -> None:
  class CommandExecutor (line 88) | class CommandExecutor:
    method _write_command_start_header (line 97) | def _write_command_start_header(self, log_file: Path, tool_name: str, ...
    method _write_command_end_footer (line 117) | def _write_command_end_footer(self, log_file: Path, tool_name: str, du...
    method _clean_output_line (line 137) | def _clean_output_line(self, line: str, suffix_char: Optional[str] = N...
    method _kill_process_tree (line 203) | def _kill_process_tree(self, process: subprocess.Popen) -> None:
    method execute_and_wait (line 226) | def execute_and_wait(
    method execute_stream (line 435) | def execute_stream(
    method _read_log_tail (line 672) | def _read_log_tail(self, log_file: Path, max_lines: int = MAX_LOG_TAIL...
  function execute_and_wait (line 749) | def execute_and_wait(
  function execute_stream (line 775) | def execute_stream(

FILE: backend/apps/scan/utils/config_merger.py
  class ConfigConflictError (line 12) | class ConfigConflictError(Exception):
    method __init__ (line 18) | def __init__(self, conflicts: List[Tuple[str, str, str]]):
  function merge_engine_configs (line 28) | def merge_engine_configs(engines: List[Tuple[str, str]]) -> str:

FILE: backend/apps/scan/utils/config_parser.py
  function _normalize_config_keys (line 25) | def _normalize_config_keys(config: Dict[str, Any]) -> Dict[str, Any]:
  function _parse_subdomain_discovery_config (line 53) | def _parse_subdomain_discovery_config(scan_config: Dict[str, Any]) -> Di...
  function parse_enabled_tools_from_dict (line 125) | def parse_enabled_tools_from_dict(

FILE: backend/apps/scan/utils/directory_cleanup.py
  function remove_directory (line 14) | def remove_directory(directory: str) -> bool:

FILE: backend/apps/scan/utils/fingerprint_helpers.py
  function ensure_ehole_fingerprint_local (line 27) | def ensure_ehole_fingerprint_local() -> str:
  function ensure_goby_fingerprint_local (line 85) | def ensure_goby_fingerprint_local() -> str:
  function ensure_wappalyzer_fingerprint_local (line 138) | def ensure_wappalyzer_fingerprint_local() -> str:
  function get_fingerprint_paths (line 191) | def get_fingerprint_paths(lib_names: list) -> dict:
  function ensure_fingers_fingerprint_local (line 227) | def ensure_fingers_fingerprint_local() -> str:
  function ensure_fingerprinthub_fingerprint_local (line 279) | def ensure_fingerprinthub_fingerprint_local() -> str:
  function ensure_arl_fingerprint_local (line 331) | def ensure_arl_fingerprint_local() -> str:

FILE: backend/apps/scan/utils/nuclei_helpers.py
  function get_local_commit_hash (line 27) | def get_local_commit_hash(local_path: Path) -> Optional[str]:
  function git_clone (line 51) | def git_clone(repo_url: str, local_path: Path) -> bool:
  function git_fetch_and_checkout (line 75) | def git_fetch_and_checkout(local_path: Path, commit_hash: str) -> bool:
  function ensure_nuclei_templates_local (line 122) | def ensure_nuclei_templates_local(repo_name: str) -> str:

FILE: backend/apps/scan/utils/performance.py
  function _get_system_stats (line 37) | def _get_system_stats() -> dict:
  class FlowPerformanceMetrics (line 62) | class FlowPerformanceMetrics:
  class FlowPerformanceTracker (line 90) | class FlowPerformanceTracker:
    method __init__ (line 106) | def __init__(self, flow_name: str, scan_id: int):
    method start (line 115) | def start(
    method _sample_loop (line 153) | def _sample_loop(self) -> None:
    method finish (line 186) | def finish(
  function _get_process_stats (line 251) | def _get_process_stats(pid: int) -> dict:
  class CommandPerformanceTracker (line 309) | class CommandPerformanceTracker:
    method __init__ (line 326) | def __init__(self, tool_name: str, command: str = ""):
    method start (line 340) | def start(self) -> None:
    method set_pid (line 360) | def set_pid(self, pid: int) -> None:
    method sample (line 384) | def sample(self) -> dict:
    method finish (line 406) | def finish(

FILE: backend/apps/scan/utils/system_load.py
  function _get_current_load (line 23) | def _get_current_load() -> tuple[float, float]:
  function wait_for_system_load (line 28) | def wait_for_system_load(
  function check_system_load (line 58) | def check_system_load(

FILE: backend/apps/scan/utils/user_logger.py
  function user_log (line 32) | def user_log(scan_id: int, stage: str, message: str, level: str = "info"):

FILE: backend/apps/scan/utils/wordlist_helpers.py
  function ensure_wordlist_local (line 23) | def ensure_wordlist_local(wordlist_name: str) -> str:

FILE: backend/apps/scan/utils/workspace_utils.py
  function setup_scan_workspace (line 13) | def setup_scan_workspace(scan_workspace_dir: str) -> Path:
  function setup_scan_directory (line 40) | def setup_scan_directory(scan_workspace_dir: str, subdir: str) -> Path:
  function _verify_writable (line 68) | def _verify_writable(path: Path) -> None:

FILE: backend/apps/scan/views/scan_log_views.py
  class ScanLogListView (line 14) | class ScanLogListView(APIView):
    method get (line 29) | def get(self, request, scan_id: int):

FILE: backend/apps/scan/views/scan_views.py
  class ScanViewSet (line 33) | class ScanViewSet(viewsets.ModelViewSet):
    method get_queryset (line 41) | def get_queryset(self):
    method get_serializer_class (line 61) | def get_serializer_class(self):
    method destroy (line 72) | def destroy(self, request, *args, **kwargs):
    method quick (line 111) | def quick(self, request):
    method initiate (line 213) | def initiate(self, request):
    method bulk_delete (line 326) | def bulk_delete(self, request):
    method statistics (line 399) | def statistics(self, request):
    method stop (line 443) | def stop(self, request, pk=None):  # pylint: disable=unused-argument

FILE: backend/apps/scan/views/scheduled_scan_views.py
  class ScheduledScanViewSet (line 29) | class ScheduledScanViewSet(viewsets.ModelViewSet):
    method __init__ (line 53) | def __init__(self, *args, **kwargs):
    method get_queryset (line 57) | def get_queryset(self):
    method get_serializer_class (line 70) | def get_serializer_class(self):
    method create (line 80) | def create(self, request, *args, **kwargs):
    method update (line 112) | def update(self, request, *args, **kwargs):
    method destroy (line 150) | def destroy(self, request, *args, **kwargs):
    method toggle (line 165) | def toggle(self, request, pk=None):

FILE: backend/apps/scan/views/subfinder_provider_settings_views.py
  class SubfinderProviderSettingsView (line 14) | class SubfinderProviderSettingsView(APIView):
    method get (line 21) | def get(self, request):
    method put (line 27) | def put(self, request):

FILE: backend/apps/targets/apps.py
  class TargetsConfig (line 4) | class TargetsConfig(AppConfig):

FILE: backend/apps/targets/migrations/0001_initial.py
  class Migration (line 6) | class Migration(migrations.Migration):

FILE: backend/apps/targets/models.py
  class SoftDeleteManager (line 5) | class SoftDeleteManager(models.Manager):
    method get_queryset (line 8) | def get_queryset(self):
  class Organization (line 12) | class Organization(models.Model):
    class Meta (line 34) | class Meta:
    method __str__ (line 53) | def __str__(self):
  class Target (line 57) | class Target(models.Model):
    class TargetType (line 65) | class TargetType(models.TextChoices):
    class Meta (line 93) | class Meta:
    method __str__ (line 114) | def __str__(self):

FILE: backend/apps/targets/repositories/django_organization_repository.py
  class DjangoOrganizationRepository (line 20) | class DjangoOrganizationRepository:
    method bulk_add_targets (line 23) | def bulk_add_targets(self, organization_id: int, targets: List[Target]...
    method get_by_id (line 63) | def get_by_id(self, organization_id: int) -> Organization | None:
    method get_names_by_ids (line 79) | def get_names_by_ids(self, organization_ids: List[int]) -> List[Tuple[...
    method soft_delete_by_ids (line 95) | def soft_delete_by_ids(self, organization_ids: List[int]) -> int:
    method get_targets (line 130) | def get_targets(self, organization_id: int) -> List[Target]:
    method get_all (line 145) | def get_all(self):
    method get_all_with_stats (line 154) | def get_all_with_stats(self):
    method get_by_ids (line 167) | def get_by_ids(self, organization_ids: List[int]) -> List[Organization]:
    method hard_delete_by_ids (line 179) | def hard_delete_by_ids(self, organization_ids: List[int]) -> Tuple[int...

FILE: backend/apps/targets/repositories/django_target_repository.py
  class DjangoTargetRepository (line 20) | class DjangoTargetRepository:
    method count_by_ids (line 23) | def count_by_ids(self, target_ids: List[int]) -> int:
    method get_by_ids (line 37) | def get_by_ids(self, target_ids: List[int]) -> List[Target]:
    method bulk_create_ignore_conflicts (line 51) | def bulk_create_ignore_conflicts(self, targets: List[Target]) -> None:
    method get_by_names (line 72) | def get_by_names(self, names: List[str]) -> List[Target]:
    method get_by_id (line 86) | def get_by_id(self, target_id: int) -> Target | None:
    method get_names_by_ids (line 102) | def get_names_by_ids(self, target_ids: List[int]) -> List[Tuple[int, s...
    method soft_delete_by_ids (line 118) | def soft_delete_by_ids(self, target_ids: List[int]) -> int:
    method get_all (line 152) | def get_all(self):
    method get_or_create (line 161) | def get_or_create(self, name: str, target_type: str):
    method hard_delete_by_ids (line 177) | def hard_delete_by_ids(self, target_ids: List[int]) -> Tuple[int, Dict...
    method update_last_scanned_at (line 235) | def update_last_scanned_at(self, target_id: int, scanned_at) -> bool:

FILE: backend/apps/targets/scripts/run_delete_organizations.py
  function hard_delete_organizations (line 21) | def hard_delete_organizations(organization_ids: list[int]) -> dict:
  function main (line 55) | def main():

FILE: backend/apps/targets/scripts/run_delete_targets.py
  function hard_delete_targets (line 21) | def hard_delete_targets(target_ids: list[int]) -> dict:
  function main (line 55) | def main():

FILE: backend/apps/targets/serializers.py
  class SimpleOrganizationSerializer (line 10) | class SimpleOrganizationSerializer(serializers.ModelSerializer):
    class Meta (line 22) | class Meta:
  class TargetSerializer (line 27) | class TargetSerializer(serializers.ModelSerializer):
    class Meta (line 44) | class Meta:
    method create (line 49) | def create(self, validated_data):
    method update (line 70) | def update(self, instance, validated_data):
  class TargetDetailSerializer (line 94) | class TargetDetailSerializer(serializers.ModelSerializer):
    class Meta (line 108) | class Meta:
    method get_summary (line 113) | def get_summary(self, obj):
  class OrganizationSerializer (line 172) | class OrganizationSerializer(serializers.ModelSerializer):
    class Meta (line 177) | class Meta:
  class BatchCreateTargetSerializer (line 183) | class BatchCreateTargetSerializer(serializers.Serializer):
    method validate_targets (line 208) | def validate_targets(self, value):

FILE: backend/apps/targets/services/organization_service.py
  class OrganizationService (line 16) | class OrganizationService:
    method __init__ (line 19) | def __init__(self):
    method get_organization (line 25) | def get_organization(self, organization_id: int) -> Organization | None:
    method get_all (line 38) | def get_all(self):
    method get_all_with_stats (line 47) | def get_all_with_stats(self):
    method bulk_add_targets (line 58) | def bulk_add_targets(self, organization_id: int, targets: List) -> None:
    method delete_organizations_two_phase (line 71) | def delete_organizations_two_phase(self, organization_ids: List[int]) ...
    method soft_delete_organizations (line 129) | def soft_delete_organizations(self, organization_ids: List[int]) -> int:
    method hard_delete_organizations (line 153) | def hard_delete_organizations(self, organization_ids: List[int]) -> Tu...

FILE: backend/apps/targets/services/target_service.py
  class TargetService (line 18) | class TargetService:
    method __init__ (line 21) | def __init__(self):
    method count_existing_ids (line 27) | def count_existing_ids(self, target_ids: List[int]) -> int:
    method get_target (line 41) | def get_target(self, target_id: int) -> Target | None:
    method get_by_id (line 53) | def get_by_id(self, target_id: int) -> Target | None:
    method get_all (line 66) | def get_all(self):
    method get_targets_by_names (line 75) | def get_targets_by_names(self, names: List[str]) -> List[Target]:
    method update_last_scanned_at (line 87) | def update_last_scanned_at(self, target_id: int) -> bool:
    method batch_create_targets (line 102) | def batch_create_targets(
    method delete_targets_two_phase (line 199) | def delete_targets_two_phase(self, target_ids: List[int]) -> Dict:
    method soft_delete_targets (line 257) | def soft_delete_targets(self, target_ids: List[int]) -> int:
    method hard_delete_targets (line 281) | def hard_delete_targets(self, target_ids: List[int]) -> Tuple[int, Dic...

FILE: backend/apps/targets/views.py
  class OrganizationViewSet (line 20) | class OrganizationViewSet(viewsets.ModelViewSet):
    method __init__ (line 28) | def __init__(self, **kwargs):
    method get_queryset (line 32) | def get_queryset(self):
    method targets (line 37) | def targets(self, request, pk=None):
    method unlink_targets (line 59) | def unlink_targets(self, request, pk=None):
    method destroy (line 104) | def destroy(self, request, *args, **kwargs):
    method bulk_delete (line 145) | def bulk_delete(self, request):
  class TargetViewSet (line 197) | class TargetViewSet(viewsets.ModelViewSet):
    method __init__ (line 217) | def __init__(self, **kwargs):
    method get_queryset (line 221) | def get_queryset(self):
    method get_serializer_class (line 240) | def get_serializer_class(self):
    method destroy (line 250) | def destroy(self, request, *args, **kwargs):
    method bulk_delete (line 289) | def bulk_delete(self, request):
    method batch_create (line 344) | def batch_create(self, request):
    method blacklist (line 414) | def blacklist(self, request, pk=None):

FILE: backend/config/logging_config.py
  function get_logging_config (line 45) | def get_logging_config(debug: bool = False):

FILE: backend/config/settings.py
  function get_bool_env (line 21) | def get_bool_env(key: str, default: bool = False) -> bool:

FILE: backend/manage.py
  function main (line 7) | def main():

FILE: backend/scripts/generate_test_data_sql.py
  function generate_fixed_length_url (line 36) | def generate_fixed_length_url(target_name: str, length: int = 245, path_...
  function generate_fixed_length_text (line 87) | def generate_fixed_length_text(length: int = 300, text_type: str = 'desc...
  function load_env_file (line 147) | def load_env_file(env_path: str) -> dict:
  function get_db_config (line 160) | def get_db_config() -> dict:
  function generate_raw_response_headers (line 183) | def generate_raw_response_headers(headers_dict: dict) -> str:
  class TestDataGenerator (line 208) | class TestDataGenerator:
    method __init__ (line 209) | def __init__(self, clear: bool = False):
    method run (line 214) | def run(self):
    method clear_data (line 260) | def clear_data(self):
    method create_workers (line 307) | def create_workers(self) -> list:
    method create_engines (line 353) | def create_engines(self) -> list:
    method create_organizations (line 402) | def create_organizations(self) -> list:
    method create_targets (line 456) | def create_targets(self, org_ids: list) -> list:
    method create_scans (line 576) | def create_scans(self, target_ids: list, engine_ids: list, worker_ids:...
    method create_scheduled_scans (line 666) | def create_scheduled_scans(self, org_ids: list, target_ids: list, engi...
    method create_subdomains (line 767) | def create_subdomains(self, target_ids: list):
    method create_websites (line 841) | def create_websites(self, target_ids: list) -> list:
    method create_endpoints (line 949) | def create_endpoints(self, target_ids: list):
    method create_directories (line 1134) | def create_directories(self, target_ids: list, website_ids: list):
    method create_host_port_mappings (line 1207) | def create_host_port_mappings(self, target_ids: list):
    method create_vulnerabilities (line 1276) | def create_vulnerabilities(self, target_ids: list):
    method create_subdomain_snapshots (line 1389) | def create_subdomain_snapshots(self, scan_ids: list):
    method create_website_snapshots (line 1451) | def create_website_snapshots(self, scan_ids: list):
    method create_endpoint_snapshots (line 1528) | def create_endpoint_snapshots(self, scan_ids: list):
    method create_directory_snapshots (line 1636) | def create_directory_snapshots(self, scan_ids: list):
    method create_host_port_mapping_snapshots (line 1697) | def create_host_port_mapping_snapshots(self, scan_ids: list):
    method create_vulnerability_snapshots (line 1743) | def create_vulnerability_snapshots(self, scan_ids: list):
    method create_ehole_fingerprints (line 1824) | def create_ehole_fingerprints(self):
    method create_goby_fingerprints (line 1911) | def create_goby_fingerprints(self):
    method create_wappalyzer_fingerprints (line 1990) | def create_wappalyzer_fingerprints(self):
    method create_fingers_fingerprints (line 2107) | def create_fingers_fingerprints(self):
    method create_fingerprinthub_fingerprints (line 2223) | def create_fingerprinthub_fingerprints(self):
    method create_arl_fingerprints (line 2384) | def create_arl_fingerprints(self):
  class MillionDataGenerator (line 2473) | class MillionDataGenerator:
    method __init__ (line 2487) | def __init__(self, clear: bool = False):
    method run (line 2492) | def run(self):
    method clear_data (line 2519) | def clear_data(self):
    method create_targets (line 2538) | def create_targets(self) -> list:
    method create_subdomains (line 2567) | def create_subdomains(self, target_ids: list):
    method create_websites (line 2624) | def create_websites(self, target_ids: list):
    method create_endpoints (line 2677) | def create_endpoints(self, target_ids: list):
    method create_host_port_mappings (line 2767) | def create_host_port_mappings(self, target_ids: list):
    method create_vulnerabilities (line 2813) | def create_vulnerabilities(self, target_ids: list):
    method create_statistics_history (line 2906) | def create_statistics_history(self):
    method update_asset_statistics (line 2955) | def update_asset_statistics(self):
  function main (line 3025) | def main():

FILE: frontend/app/[locale]/dashboard/page.tsx
  function Page (line 11) | function Page() {

FILE: frontend/app/[locale]/layout.tsx
  function generateMetadata (line 35) | async function generateMetadata({ params }: { params: Promise<{ locale: ...
  function generateStaticParams (line 67) | function generateStaticParams() {
  type Props (line 71) | interface Props {
  function LocaleLayout (line 80) | async function LocaleLayout({

FILE: frontend/app/[locale]/login/layout.tsx
  type Props (line 4) | type Props = {
  function generateMetadata (line 8) | async function generateMetadata({ params }: Props): Promise<Metadata> {
  function LoginLayout (line 22) | function LoginLayout({

FILE: frontend/app/[locale]/login/page.tsx
  function LoginPage (line 20) | function LoginPage() {

FILE: frontend/app/[locale]/organization/[id]/page.tsx
  function OrganizationDetailPage (line 10) | function OrganizationDetailPage({

FILE: frontend/app/[locale]/organization/page.tsx
  function OrganizationPage (line 13) | function OrganizationPage() {

FILE: frontend/app/[locale]/page.tsx
  function Home (line 4) | function Home() {

FILE: frontend/app/[locale]/scan/engine/page.tsx
  constant FEATURE_LIST (line 31) | const FEATURE_LIST = [
  type FeatureKey (line 42) | type FeatureKey = typeof FEATURE_LIST[number]["key"]
  function parseEngineFeatures (line 45) | function parseEngineFeatures(engine: ScanEngine): Record<FeatureKey, boo...
  function countEnabledFeatures (line 79) | function countEnabledFeatures(engine: ScanEngine) {
  function ScanEnginePage (line 87) | function ScanEnginePage() {

FILE: frontend/app/[locale]/scan/history/[id]/directories/page.tsx
  function ScanDirectoriesPage (line 6) | function ScanDirectoriesPage() {

FILE: frontend/app/[locale]/scan/history/[id]/endpoints/page.tsx
  function ScanHistoryEndpointsPage (line 7) | function ScanHistoryEndpointsPage() {

FILE: frontend/app/[locale]/scan/history/[id]/ip-addresses/page.tsx
  function ScanHistoryIPsPage (line 7) | function ScanHistoryIPsPage() {

FILE: frontend/app/[locale]/scan/history/[id]/layout.tsx
  function ScanHistoryLayout (line 13) | function ScanHistoryLayout({

FILE: frontend/app/[locale]/scan/history/[id]/overview/page.tsx
  function ScanOverviewPage (line 10) | function ScanOverviewPage() {

FILE: frontend/app/[locale]/scan/history/[id]/page.tsx
  function ScanHistoryDetailPage (line 6) | function ScanHistoryDetailPage() {

FILE: frontend/app/[locale]/scan/history/[id]/screenshots/page.tsx
  function ScanScreenshotsPage (line 6) | function ScanScreenshotsPage() {

FILE: frontend/app/[locale]/scan/history/[id]/subdomain/page.tsx
  function ScanHistorySubdomainPage (line 7) | function ScanHistorySubdomainPage() {

FILE: frontend/app/[locale]/scan/history/[id]/vulnerabilities/page.tsx
  function ScanHistoryVulnerabilitiesPage (line 7) | function ScanHistoryVulnerabilitiesPage() {

FILE: frontend/app/[locale]/scan/history/[id]/websites/page.tsx
  function ScanWebSitesPage (line 6) | function ScanWebSitesPage() {

FILE: frontend/app/[locale]/scan/history/page.tsx
  function ScanHistoryPage (line 12) | function ScanHistoryPage() {

FILE: frontend/app/[locale]/scan/scheduled/page.tsx
  function ScheduledScanPage (line 31) | function ScheduledScanPage() {

FILE: frontend/app/[locale]/search/page.tsx
  function Search (line 3) | function Search() {

FILE: frontend/app/[locale]/settings/api-keys/page.tsx
  function PasswordInput (line 17) | function PasswordInput({ value, onChange, placeholder, disabled }: {
  constant PROVIDERS (line 46) | const PROVIDERS = [
  constant DEFAULT_SETTINGS (line 148) | const DEFAULT_SETTINGS: ApiKeySettings = {
  function ApiKeysSettingsPage (line 159) | function ApiKeysSettingsPage() {

FILE: frontend/app/[locale]/settings/blacklist/page.tsx
  function GlobalBlacklistPage (line 15) | function GlobalBlacklistPage() {

FILE: frontend/app/[locale]/settings/notifications/page.tsx
  function NotificationSettingsPage (line 20) | function NotificationSettingsPage() {

FILE: frontend/app/[locale]/settings/system-logs/page.tsx
  function SystemLogsPage (line 5) | function SystemLogsPage() {

FILE: frontend/app/[locale]/settings/workers/page.tsx
  function WorkersPage (line 6) | function WorkersPage() {

FILE: frontend/app/[locale]/target/[id]/details/page.tsx
  function TargetDetailsPage (line 10) | function TargetDetailsPage() {

FILE: frontend/app/[locale]/target/[id]/directories/page.tsx
  function TargetDirectoriesPage (line 6) | function TargetDirectoriesPage() {

FILE: frontend/app/[locale]/target/[id]/endpoints/page.tsx
  function TargetEndpointsPage (line 11) | function TargetEndpointsPage() {

FILE: frontend/app/[locale]/target/[id]/ip-addresses/page.tsx
  function TargetIPsPage (line 7) | function TargetIPsPage() {

FILE: frontend/app/[locale]/target/[id]/layout.tsx
  function TargetLayout (line 18) | function TargetLayout({

FILE: frontend/app/[locale]/target/[id]/overview/page.tsx
  function TargetOverviewPage (line 10) | function TargetOverviewPage() {

FILE: frontend/app/[locale]/target/[id]/page.tsx
  function TargetDetailPage (line 10) | function TargetDetailPage() {

FILE: frontend/app/[locale]/target/[id]/screenshots/page.tsx
  function ScreenshotsPage (line 6) | function ScreenshotsPage() {

FILE: frontend/app/[locale]/target/[id]/settings/page.tsx
  function TargetSettingsPage (line 10) | function TargetSettingsPage() {

FILE: frontend/app/[locale]/target/[id]/subdomain/page.tsx
  function TargetSubdomainPage (line 7) | function TargetSubdomainPage() {

FILE: frontend/app/[locale]/target/[id]/vulnerabilities/page.tsx
  function TargetVulnerabilitiesPage (line 11) | function TargetVulnerabilitiesPage() {

FILE: frontend/app/[locale]/target/[id]/websites/page.tsx
  function WebSitesPage (line 6) | function WebSitesPage() {

FILE: frontend/app/[locale]/target/page.tsx
  function AllTargetsPage (line 7) | function AllTargetsPage() {

FILE: frontend/app/[locale]/tools/config/custom/page.tsx
  function CustomToolsPage (line 5) | function CustomToolsPage() {

FILE: frontend/app/[locale]/tools/config/opensource/page.tsx
  function OpensourceToolsPage (line 5) | function OpensourceToolsPage() {

FILE: frontend/app/[locale]/tools/config/page.tsx
  function ToolConfigPage (line 7) | function ToolConfigPage() {

FILE: frontend/app/[locale]/tools/fingerprints/arl/page.tsx
  function ARLFingerprintPage (line 6) | function ARLFingerprintPage() {

FILE: frontend/app/[locale]/tools/fingerprints/ehole/page.tsx
  function EholeFingerprintPage (line 6) | function EholeFingerprintPage() {

FILE: frontend/app/[locale]/tools/fingerprints/fingerprinthub/page.tsx
  function FingerPrintHubFingerprintPage (line 6) | function FingerPrintHubFingerprintPage() {

FILE: frontend/app/[locale]/tools/fingerprints/fingers/page.tsx
  function FingersFingerprintPage (line 6) | function FingersFingerprintPage() {

FILE: frontend/app/[locale]/tools/fingerprints/goby/page.tsx
  function GobyFingerprintsPage (line 5) | function GobyFingerprintsPage() {

FILE: frontend/app/[locale]/tools/fingerprints/layout.tsx
  function FingerprintsLayout (line 23) | function FingerprintsLayout({

FILE: frontend/app/[locale]/tools/fingerprints/page.tsx
  function FingerprintsPage (line 8) | function FingerprintsPage() {

FILE: frontend/app/[locale]/tools/fingerprints/wappalyzer/page.tsx
  function WappalyzerFingerprintsPage (line 5) | function WappalyzerFingerprintsPage() {

FILE: frontend/app/[locale]/tools/nuclei/[repoId]/page.tsx
  type FlattenedNode (line 35) | interface FlattenedNode extends NucleiTemplateTreeNode {
  function parseTemplateInfo (line 40) | function parseTemplateInfo(content: string) {
  function getSeverityColor (line 69) | function getSeverityColor(severity?: string) {
  function NucleiRepoDetailPage (line 86) | function NucleiRepoDetailPage() {

FILE: frontend/app/[locale]/tools/nuclei/page.tsx
  function formatDateTime (line 37) | function formatDateTime(isoString: string | null, locale: string) {
  function NucleiReposPage (line 46) | function NucleiReposPage() {

FILE: frontend/app/[locale]/tools/page.tsx
  function ToolsPage (line 13) | function ToolsPage() {

FILE: frontend/app/[locale]/tools/wordlists/page.tsx
  function WordlistsPage (line 29) | function WordlistsPage() {

FILE: frontend/app/[locale]/vulnerabilities/page.tsx
  function VulnerabilitiesPage (line 11) | function VulnerabilitiesPage() {

FILE: frontend/app/layout.tsx
  function RootLayout (line 7) | function RootLayout({

FILE: frontend/components/about-dialog.tsx
  type AboutDialogProps (line 32) | interface AboutDialogProps {
  function AboutDialog (line 36) | function AboutDialog({ children }: AboutDialogProps) {

FILE: frontend/components/app-sidebar.tsx
  function AppSidebar (line 61) | function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {

FILE: frontend/components/auth/auth-guard.tsx
  constant PUBLIC_ROUTES (line 9) | const PUBLIC_ROUTES = ["/login"]
  constant SKIP_AUTH (line 11) | const SKIP_AUTH = process.env.NEXT_PUBLIC_SKIP_AUTH === 'true'
  type AuthGuardProps (line 13) | interface AuthGuardProps {
  function AuthGuard (line 21) | function AuthGuard({ children }: AuthGuardProps) {

FILE: frontend/components/auth/auth-layout.tsx
  constant PUBLIC_ROUTES (line 16) | const PUBLIC_ROUTES = ["/login"]
  type AuthLayoutProps (line 18) | interface AuthLayoutProps {
  function isPublicPath (line 26) | function isPublicPath(pathname: string): boolean {
  function AuthLayout (line 38) | function AuthLayout({ children }: AuthLayoutProps) {

FILE: frontend/components/auth/change-password-dialog.tsx
  type ChangePasswordDialogProps (line 19) | interface ChangePasswordDialogProps {
  function ChangePasswordDialog (line 24) | function ChangePasswordDialog({ open, onOpenChange }: ChangePasswordDial...

FILE: frontend/components/color-theme-switcher.tsx
  function ColorThemeSwitcher (line 17) | function ColorThemeSwitcher() {

FILE: frontend/components/common/bulk-add-urls-dialog.tsx
  type AssetType (line 25) | type AssetType = 'endpoint' | 'website' | 'directory'
  type BulkAddUrlsDialogProps (line 27) | interface BulkAddUrlsDialogProps {
  constant ASSET_TYPE_LABELS (line 37) | const ASSET_TYPE_LABELS: Record<AssetType, { title: string; description:...
  function BulkAddUrlsDialog (line 73) | function BulkAddUrlsDialog({

FILE: frontend/components/common/smart-filter-input.tsx
  type FilterField (line 23) | interface FilterField {
  constant PREDEFINED_FIELDS (line 30) | const PREDEFINED_FIELDS: Record<string, FilterField> = {
  function getTranslatedFields (line 46) | function getTranslatedFields(t: (key: string) => string): Record<string,...
  constant DEFAULT_FIELDS (line 64) | const DEFAULT_FIELDS: FilterField[] = [
  constant FILTER_HISTORY_KEY (line 71) | const FILTER_HISTORY_KEY = 'smart_filter_history'
  constant MAX_HISTORY_PER_FIELD (line 72) | const MAX_HISTORY_PER_FIELD = 10
  function getFieldHistory (line 75) | function getFieldHistory(field: string): string[] {
  function saveFieldHistory (line 86) | function saveFieldHistory(field: string, value: string) {
  function saveQueryHistory (line 100) | function saveQueryHistory(query: string) {
  type ParsedFilter (line 110) | interface ParsedFilter {
  function parseFilterExpression (line 117) | function parseFilterExpression(input: string): ParsedFilter[] {
  type SmartFilterInputProps (line 133) | interface SmartFilterInputProps {
  function SmartFilterInput (line 145) | function SmartFilterInput({

FILE: frontend/components/dashboard/asset-distribution-chart.tsx
  constant COLORS (line 23) | const COLORS = {
  function AssetDistributionChart (line 30) | function AssetDistributionChart() {

FILE: frontend/components/dashboard/asset-trend-chart.tsx
  function fillMissingDates (line 13) | function fillMissingDates(data: StatisticsHistoryItem[] | undefined, day...
  type SeriesKey (line 66) | type SeriesKey = 'totalSubdomains' | 'totalIps' | 'totalEndpoints' | 'to...
  constant ALL_SERIES (line 69) | const ALL_SERIES: SeriesKey[] = ['totalSubdomains', 'totalIps', 'totalEn...
  function AssetTrendChart (line 71) | function AssetTrendChart() {

FILE: frontend/components/dashboard/dashboard-activity-tabs.tsx
  function DashboardActivityTabs (line 8) | function DashboardActivityTabs() {

FILE: frontend/components/dashboard/dashboard-data-table.tsx
  function DashboardDataTable (line 35) | function DashboardDataTable() {

FILE: frontend/components/dashboard/dashboard-scan-history.tsx
  function DashboardScanHistory (line 14) | function DashboardScanHistory() {

FILE: frontend/components/dashboard/dashboard-scheduled-scans.tsx
  function DashboardScheduledScans (line 12) | function DashboardScheduledScans() {

FILE: frontend/components/dashboard/dashboard-stat-cards.tsx
  function TrendBadge (line 11) | function TrendBadge({ change }: { change: number }) {
  function StatCard (line 29) | function StatCard({
  function formatUpdateTime (line 71) | function formatUpdateTime(dateStr: string | null, locale: string, noData...
  function DashboardStatCards (line 82) | function DashboardStatCards() {

FILE: frontend/components/dashboard/recent-vulnerabilities.tsx
  function RecentVulnerabilities (line 39) | function RecentVulnerabilities() {

FILE: frontend/components/dashboard/vuln-severity-chart.tsx
  constant SEVERITY_COLORS (line 23) | const SEVERITY_COLORS = {
  function VulnSeverityChart (line 31) | function VulnSeverityChart() {

FILE: frontend/components/directories/directories-columns.tsx
  type DirectoryTranslations (line 12) | interface DirectoryTranslations {
  type CreateColumnsProps (line 29) | interface CreateColumnsProps {
  function StatusBadge (line 37) | function StatusBadge({ status }: { status: number | null }) {
  function formatDuration (line 62) | function formatDuration(nanoseconds: number | null): string {
  function createDirectoryColumns (line 71) | function createDirectoryColumns({

FILE: frontend/components/directories/directories-data-table.tsx
  constant DIRECTORY_FILTER_FIELDS (line 13) | const DIRECTORY_FILTER_FIELDS: FilterField[] = [
  constant DIRECTORY_FILTER_EXAMPLES (line 19) | const DIRECTORY_FILTER_EXAMPLES = [
  type DirectoriesDataTableProps (line 25) | interface DirectoriesDataTableProps {
  function DirectoriesDataTable (line 44) | function DirectoriesDataTable({

FILE: frontend/components/directories/directories-view.tsx
  function DirectoriesView (line 20) | function DirectoriesView({

FILE: frontend/components/disk/disk-stat-cards.tsx
  function StatCard (line 10) | function StatCard({ title, value, icon, loading }: { title: string; valu...
  function DiskStatCards (line 30) | function DiskStatCards() {

FILE: frontend/components/endpoints/endpoints-columns.tsx
  type EndpointTranslations (line 12) | interface EndpointTranslations {
  type CreateColumnsProps (line 36) | interface CreateColumnsProps {
  function HttpStatusBadge (line 41) | function HttpStatusBadge({ statusCode }: { statusCode: number | null | u...
  function createEndpointColumns (line 73) | function createEndpointColumns({

FILE: frontend/components/endpoints/endpoints-data-table.tsx
  constant ENDPOINT_FILTER_FIELDS (line 12) | const ENDPOINT_FILTER_FIELDS: FilterField[] = [
  constant ENDPOINT_FILTER_EXAMPLES (line 22) | const ENDPOINT_FILTER_EXAMPLES = [
  type EndpointsDataTableProps (line 29) | interface EndpointsDataTableProps<TData extends { id: number | string },...
  function EndpointsDataTable (line 49) | function EndpointsDataTable<TData extends { id: number | string }, TValu...

FILE: frontend/components/endpoints/endpoints-detail-view.tsx
  function EndpointsDetailView (line 34) | function EndpointsDetailView({

FILE: frontend/components/fingerprints/arl-fingerprint-columns.tsx
  type ColumnOptions (line 10) | interface ColumnOptions {
  function createARLFingerprintColumns (line 18) | function createARLFingerprintColumns({

FILE: frontend/components/fingerprints/arl-fingerprint-data-table.tsx
  constant ARL_FILTER_EXAMPLES (line 37) | const ARL_FILTER_EXAMPLES = [
  type ARLFingerprintDataTableProps (line 43) | interface ARLFingerprintDataTableProps {
  function ARLFingerprintDataTable (line 61) | function ARLFingerprintDataTable({

FILE: frontend/components/fingerprints/arl-fingerprint-dialog.tsx
  type ARLFingerprintDialogProps (line 25) | interface ARLFingerprintDialogProps {
  type FormData (line 32) | interface FormData {
  function ARLFingerprintDialog (line 37) | function ARLFingerprintDialog({

FILE: frontend/components/fingerprints/arl-fingerprint-view.tsx
  function ARLFingerprintView (line 21) | function ARLFingerprintView() {

FILE: frontend/components/fingerprints/ehole-fingerprint-columns.tsx
  type ColumnOptions (line 13) | interface ColumnOptions {
  function KeywordListCell (line 20) | function KeywordListCell({ keywords }: { keywords: string[] }) {
  function createEholeFingerprintColumns (line 63) | function createEholeFingerprintColumns({

FILE: frontend/components/fingerprints/ehole-fingerprint-data-table.tsx
  constant EHOLE_FILTER_EXAMPLES (line 37) | const EHOLE_FILTER_EXAMPLES = [
  type EholeFingerprintDataTableProps (line 44) | interface EholeFingerprintDataTableProps {
  function EholeFingerprintDataTable (line 62) | function EholeFingerprintDataTable({

FILE: frontend/components/fingerprints/ehole-fingerprint-dialog.tsx
  type EholeFingerprintDialogProps (line 32) | interface EholeFingerprintDialogProps {
  type FormData (line 39) | interface FormData {
  constant METHOD_OPTIONS (line 48) | const METHOD_OPTIONS = [
  constant LOCATION_OPTIONS (line 55) | const LOCATION_OPTIONS = [
  function EholeFingerprintDialog (line 61) | function EholeFingerprintDialog({

FILE: frontend/components/fingerprints/ehole-fingerprint-view.tsx
  function EholeFingerprintView (line 21) | function EholeFingerprintView() {

FILE: frontend/components/fingerprints/fingerprinthub-fingerprint-columns.tsx
  type ColumnOptions (line 13) | interface ColumnOptions {
  function SeverityBadge (line 20) | function SeverityBadge({ severity }: { severity: string }) {
  function TagListCell (line 41) | function TagListCell({ tags }: { tags: string }) {
  function MetadataCell (line 85) | function MetadataCell({ metadata }: { metadata: any }) {
  function HttpMatchersCell (line 135) | function HttpMatchersCell({ http }: { http: any[] }) {
  function createFingerPrintHubFingerprintColumns (line 188) | function createFingerPrintHubFingerprintColumns({

FILE: frontend/components/fingerprints/fingerprinthub-fingerprint-data-table.tsx
  constant FINGERPRINTHUB_FILTER_EXAMPLES (line 37) | const FINGERPRINTHUB_FILTER_EXAMPLES = [
  type FingerPrintHubFingerprintDataTableProps (line 44) | interface FingerPrintHubFingerprintDataTableProps {
  function FingerPrintHubFingerprintDataTable (line 62) | function FingerPrintHubFingerprintDataTable({

FILE: frontend/components/fingerprints/fingerprinthub-fingerprint-dialog.tsx
  type FingerPrintHubFingerprintDialogProps (line 32) | interface FingerPrintHubFingerprintDialogProps {
  type FormData (line 39) | interface FormData {
  constant SEVERITY_OPTIONS (line 50) | const SEVERITY_OPTIONS = [
  function FingerPrintHubFingerprintDialog (line 58) | function FingerPrintHubFingerprintDialog({

FILE: frontend/components/fingerprints/fingerprinthub-fingerprint-view.tsx
  function FingerPrintHubFingerprintView (line 21) | function FingerPrintHubFingerprintView() {

FILE: frontend/components/fingerprints/fingers-fingerprint-columns.tsx
  type ColumnOptions (line 13) | interface ColumnOptions {
  function TagListCell (line 20) | function TagListCell({ tags }: { tags: string[] }) {
  type RuleItem (line 63) | interface RuleItem {
  function extractRuleItems (line 68) | function extractRuleItems(rules: any[]): RuleItem[] {
  function RulesCell (line 85) | function RulesCell({ rules }: { rules: any[] }) {
  function PortListCell (line 131) | function PortListCell({ ports }: { ports: number[] }) {
  function createFingersFingerprintColumns (line 152) | function createFingersFingerprintColumns({

FILE: frontend/components/fingerprints/fingers-fingerprint-data-table.tsx
  constant FINGERS_FILTER_EXAMPLES (line 37) | const FINGERS_FILTER_EXAMPLES = [
  type FingersFingerprintDataTableProps (line 44) | interface FingersFingerprintDataTableProps {
  function FingersFingerprintDataTable (line 62) | function FingersFingerprintDataTable({

FILE: frontend/components/fingerprints/fingers-fingerprint-dialog.tsx
  type FingersFingerprintDialogProps (line 26) | interface FingersFingerprintDialogProps {
  type FormData (line 33) | interface FormData {
  function FingersFingerprintDialog (line 42) | function FingersFingerprintDialog({

FILE: frontend/components/fingerprints/fingers-fingerprint-view.tsx
  function FingersFingerprintView (line 21) | function FingersFingerprintView() {

FILE: frontend/components/fingerprints/goby-fingerprint-columns.tsx
  type ColumnOptions (line 12) | interface ColumnOptions {
  function RuleDetailsCell (line 19) | function RuleDetailsCell({ rules }: { rules: any[] }) {
  function createGobyFingerprintColumns (line 62) | function createGobyFingerprintColumns({

FILE: frontend/components/fingerprints/goby-fingerprint-data-table.tsx
  constant GOBY_FILTER_EXAMPLES (line 37) | const GOBY_FILTER_EXAMPLES = [
  type GobyFingerprintDataTableProps (line 43) | interface GobyFingerprintDataTableProps {
  function GobyFingerprintDataTable (line 61) | function GobyFingerprintDataTable({

FILE: frontend/components/fingerprints/goby-fingerprint-dialog.tsx
  type GobyFingerprintDialogProps (line 33) | interface GobyFingerprintDialogProps {
  type FormData (line 40) | interface FormData {
  constant LABEL_OPTIONS (line 46) | const LABEL_OPTIONS = [
  function GobyFingerprintDialog (line 57) | function GobyFingerprintDialog({

FILE: frontend/components/fingerprints/goby-fingerprint-view.tsx
  function GobyFingerprintView (line 21) | function GobyFingerprintView() {

FILE: frontend/components/fingerprints/import-fingerprint-dialog.tsx
  type FingerprintType (line 29) | type FingerprintType = "ehole" | "goby" | "wappalyzer" | "fingers" | "fi...
  type ImportFingerprintDialogProps (line 31) | interface ImportFingerprintDialogProps {
  function ImportFingerprintDialog (line 39) | function ImportFingerprintDialog({

FILE: frontend/components/fingerprints/wappalyzer-fingerprint-columns.tsx
  type ColumnOptions (line 12) | interface ColumnOptions {
  type RuleItem (line 16) | interface RuleItem {
  function extractRules (line 24) | function extractRules(fp: WappalyzerFingerprint): RuleItem[] {
  function RulesCell (line 41) | function RulesCell({ fp }: { fp: WappalyzerFingerprint }) {
  function createWappalyzerFingerprintColumns (line 87) | function createWappalyzerFingerprintColumns({

FILE: frontend/components/fingerprints/wappalyzer-fingerprint-data-table.tsx
  constant WAPPALYZER_FILTER_EXAMPLES (line 37) | const WAPPALYZER_FILTER_EXAMPLES = [
  type WappalyzerFingerprintDataTableProps (line 44) | interface WappalyzerFingerprintDataTableProps {
  function WappalyzerFingerprintDataTable (line 62) | function WappalyzerFingerprintDataTable({

FILE: frontend/components/fingerprints/wappalyzer-fingerprint-dialog.tsx
  type WappalyzerFingerprintDialogProps (line 25) | interface WappalyzerFingerprintDialogProps {
  type FormData (line 32) | interface FormData {
  function WappalyzerFingerprintDialog (line 47) | function WappalyzerFingerprintDialog({

FILE: frontend/components/fingerprints/wappalyzer-fingerprint-view.tsx
  function WappalyzerFingerprintView (line 21) | function WappalyzerFingerprintView() {

FILE: frontend/components/ip-addresses/ip-addresses-columns.tsx
  type IPAddressTranslations (line 17) | interface IPAddressTranslations {
  type CreateColumnsProps (line 34) | interface CreateColumnsProps {
  function createIPAddressColumns (line 39) | function createIPAddressColumns({

FILE: frontend/components/ip-addresses/ip-addresses-data-table.tsx
  constant IP_ADDRESS_FILTER_FIELDS (line 13) | const IP_ADDRESS_FILTER_FIELDS: FilterField[] = [
  constant IP_ADDRESS_FILTER_EXAMPLES (line 20) | const IP_ADDRESS_FILTER_EXAMPLES = [
  type IPAddressesDataTableProps (line 26) | interface IPAddressesDataTableProps {
  function IPAddressesDataTable (line 41) | function IPAddressesDataTable({

FILE: frontend/components/ip-addresses/ip-addresses-view.tsx
  function IPAddressesView (line 17) | function IPAddressesView({

FILE: frontend/components/language-switcher.tsx
  function LanguageSwitcher (line 19) | function LanguageSwitcher() {

FILE: frontend/components/loading-spinner.tsx
  type LoadingSpinnerProps (line 7) | interface LoadingSpinnerProps {
  function LoadingSpinner (line 20) | function LoadingSpinner({ size = "sm", className }: LoadingSpinnerProps) {
  type LoadingStateProps (line 30) | interface LoadingStateProps {
  function LoadingState (line 41) | function LoadingState({
  type LoadingOverlayProps (line 63) | interface LoadingOverlayProps {
  function LoadingOverlay (line 74) | function LoadingOverlay({

FILE: frontend/components/nav-secondary.tsx
  function NavSecondary (line 28) | function NavSecondary({

FILE: frontend/components/nav-system.tsx
  function NavSystem (line 16) | function NavSystem({

FILE: frontend/components/nav-user.tsx
  function NavUser (line 46) | function NavUser({

FILE: frontend/components/notifications/notification-drawer.tsx
  function ConnectionStatus (line 28) | function ConnectionStatus({ isConnected, t }: { isConnected: boolean, t:...
  function NotificationSkeleton (line 48) | function NotificationSkeleton() {
  function getTimeGroup (line 68) | function getTimeGroup(dateStr?: string): 'today' | 'yesterday' | 'earlie...
  function NotificationDrawer (line 80) | function NotificationDrawer() {

FILE: frontend/components/organization/add-organization-dialog.tsx
  type AddOrganizationDialogProps (line 39) | interface AddOrganizationDialogProps {
  function AddOrganizationDialog (line 45) | function AddOrganizationDialog({

FILE: frontend/components/organization/edit-organization-dialog.tsx
  type EditOrganizationDialogProps (line 36) | interface EditOrganizationDialogProps {
  function EditOrganizationDialog (line 43) | function EditOrganizationDialog({

FILE: frontend/components/organization/organization-columns.tsx
  type OrganizationTranslations (line 28) | interface OrganizationTranslations {
  type CreateColumnsProps (line 50) | interface CreateColumnsProps {
  function OrganizationRowActions (line 63) | function OrganizationRowActions({

FILE: frontend/components/organization/organization-data-table.tsx
  function OrganizationDataTable (line 11) | function OrganizationDataTable({

FILE: frontend/components/organization/organization-detail-view.tsx
  function OrganizationDetailView (line 32) | function OrganizationDetailView({

FILE: frontend/components/organization/organization-list.tsx
  function OrganizationList (line 55) | function OrganizationList() {
  function OrganizationListSkeleton (line 402) | function OrganizationListSkeleton() {

FILE: frontend/components/organization/targets/link-target-dialog.tsx
  type LinkTargetDialogProps (line 42) | interface LinkTargetDialogProps {
  function LinkTargetDialog (line 60) | function LinkTargetDialog({

FILE: frontend/components/organization/targets/targets-columns.tsx
  type OrgTargetsTranslations (line 20) | interface OrgTargetsTranslations {
  type CreateColumnsProps (line 42) | interface CreateColumnsProps {
  function TargetRowActions (line 52) | function TargetRowActions({
  function TargetNameCell (line 107) | function TargetNameCell({

FILE: frontend/components/organization/targets/targets-data-table.tsx
  type TargetsDataTableProps (line 13) | interface TargetsDataTableProps {
  function TargetsDataTable (line 35) | function TargetsDataTable({

FILE: frontend/components/organization/targets/targets-detail-view.tsx
  function OrganizationTargetsDetailView (line 33) | function OrganizationTargetsDetailView({

FILE: frontend/components/providers/query-provider.tsx
  type QueryProviderProps (line 47) | interface QueryProviderProps {
  function QueryProvider (line 59) | function QueryProvider({ children }: QueryProviderProps) {

FILE: frontend/components/providers/theme-provider.tsx
  function ThemeProvider (line 11) | function ThemeProvider({

FILE: frontend/components/providers/ui-i18n-provider.tsx
  function UiI18nProvider (line 10) | function UiI18nProvider({ children }: { children: React.ReactNode }) {

FILE: frontend/components/route-prefetch.tsx
  function RoutePrefetch (line 10) | function RoutePrefetch() {

FILE: frontend/components/route-progress.tsx
  function RouteProgress (line 12) | function RouteProgress() {

FILE: frontend/components/scan/engine-preset-selector.tsx
  type EnginePreset (line 14) | interface EnginePreset {
  type EnginePresetSelectorProps (line 22) | interface EnginePresetSelectorProps {
  function EnginePresetSelector (line 33) | function EnginePresetSelector({

FILE: frontend/components/scan/engine/engine-columns.tsx
  type EngineTranslations (line 26) | interface EngineTranslations {
  function parseEngineFeatures (line 52) | function parseEngineFeatures(engine: ScanEngine) {
  function FeatureStatus (line 88) | function FeatureStatus({ enabled }: { enabled?: boolean }) {
  type CreateColumnsProps (line 103) | interface CreateColumnsProps {
  function EngineRowActions (line 112) | function EngineRowActions({

FILE: frontend/components/scan/engine/engine-create-dialog.tsx
  type EngineCreateDialogProps (line 22) | interface EngineCreateDialogProps {
  function EngineCreateDialog (line 31) | function EngineCreateDialog({

FILE: frontend/components/scan/engine/engine-data-table.tsx
  type EngineDataTableProps (line 11) | interface EngineDataTableProps {
  function EngineDataTable (line 24) | function EngineDataTable({

FILE: frontend/components/scan/engine/engine-edit-dialog.tsx
  type EngineEditDialogProps (line 22) | interface EngineEditDialogProps {
  function EngineEditDialog (line 33) | function EngineEditDialog({

FILE: frontend/components/scan/history/scan-history-columns.tsx
  type ScanHistoryTranslations (line 42) | interface ScanHistoryTranslations {
  function StatusBadge (line 83) | function StatusBadge({
  type CreateColumnsProps (line 157) | interface CreateColumnsProps {

FILE: frontend/components/scan/history/scan-history-data-table.tsx
  type ScanHistoryDataTableProps (line 13) | interface ScanHistoryDataTableProps {
  function ScanHistoryDataTable (line 37) | function ScanHistoryDataTable({

FILE: frontend/components/scan/history/scan-history-list.tsx
  type ScanHistoryListProps (line 32) | interface ScanHistoryListProps {
  function ScanHistoryList (line 41) | function ScanHistoryList({ hideToolbar = false, targetId, pageSize: cust...

FILE: frontend/components/scan/history/scan-history-stat-cards.tsx
  function StatCard (line 15) | function StatCard({
  function ScanHistoryStatCards (line 55) | function ScanHistoryStatCards() {

FILE: frontend/components/scan/history/scan-overview.tsx
  type ScanOverviewProps (line 43) | interface ScanOverviewProps {
  function PulsingDot (line 52) | function PulsingDot({ className }: { className?: string }) {
  function StageStatusIcon (line 62) | function StageStatusIcon({ status }: { status: StageStatus }) {
  function formatStageDuration (line 78) | function formatStageDuration(seconds?: number): string | undefined {
  constant STAGE_STATUS_PRIORITY (line 88) | const STAGE_STATUS_PRIORITY: Record<StageStatus, number> = {
  function ScanOverview (line 96) | function ScanOverview({ scanId }: ScanOverviewProps) {

FILE: frontend/components/scan/initiate-scan-dialog.tsx
  type InitiateScanDialogProps (line 35) | interface InitiateScanDialogProps {
  function InitiateScanDialog (line 45) | function InitiateScanDialog({

FILE: frontend/components/scan/quick-scan-dialog.tsx
  type QuickScanDialogProps (line 36) | interface QuickScanDialogProps {
  function QuickScanDialog (line 40) | function QuickScanDialog({ trigger }: QuickScanDialogProps) {

FILE: frontend/components/scan/scan-config-editor.tsx
  type ScanConfigEditorProps (line 13) | interface ScanConfigEditorProps {
  function ScanConfigEditor (line 25) | function ScanConfigEditor({

FILE: frontend/components/scan/scan-log-list.tsx
  type ScanLogListProps (line 7) | interface ScanLogListProps {
  function formatTime (line 15) | function formatTime(isoString: string): string {
  function ScanLogList (line 31) | function ScanLogList({ logs, loading }: ScanLogListProps) {

FILE: frontend/components/scan/scan-progress-dialog.tsx
  type StageDetail (line 30) | interface StageDetail {
  type ScanProgressData (line 41) | interface ScanProgressData {
  type ScanProgressDialogProps (line 53) | interface ScanProgressDialogProps {
  constant SCAN_STATUS_STYLES (line 60) | const SCAN_STATUS_STYLES: Record<string, string> = {
  function PulsingDot (line 71) | function PulsingDot({ className }: { className?: string }) {
  function ScanStatusIcon (line 83) | function ScanStatusIcon({ status }: { status: string }) {
  function ScanStatusBadge (line 103) | function ScanStatusBadge({ status, t }: { status: string; t: (key: strin...
  function StageStatusIcon (line 116) | function StageStatusIcon({ status }: { status: StageStatus }) {
  function StageRow (line 134) | function StageRow({ stage, t }: { stage: StageDetail; t: (key: string) =...
  function ScanProgressDialog (line 190) | function ScanProgressDialog({
  function formatDuration (line 295) | function formatDuration(seconds?: number): string | undefined {
  function formatDateTime (line 307) | function formatDateTime(isoString?: string, locale: string = "zh"): stri...
  function getStageResultCount (line 326) | function getStageResultCount(stageName: string, summary: ScanRecord["sum...
  constant STATUS_PRIORITY (line 356) | const STATUS_PRIORITY: Record<StageStatus, number> = {
  function buildScanProgressData (line 364) | function buildScanProgressData(scan: ScanRecord): ScanProgressData {

FILE: frontend/components/scan/scheduled/create-scheduled-scan-dialog.tsx
  type CreateScheduledScanDialogProps (line 65) | interface CreateScheduledScanDialogProps {
  type SelectionMode (line 75) | type SelectionMode = "organization" | "target"
  function CreateScheduledScanDialog (line 77) | function CreateScheduledScanDialog({

FILE: frontend/components/scan/scheduled/edit-scheduled-scan-dialog.tsx
  type EditScheduledScanDialogProps (line 28) | interface EditScheduledScanDialogProps {
  function EditScheduledScanDialog (line 35) | function EditScheduledScanDialog({

FILE: frontend/components/scan/scheduled/scheduled-scan-columns.tsx
  type ScheduledScanTranslations (line 26) | interface ScheduledScanTranslations {
  type CreateColumnsProps (line 58) | interface CreateColumnsProps {
  function parseCronExpression (line 69) | function parseCronExpression(cron: string, t: ScheduledScanTranslations)...
  function ScheduledScanRowActions (line 112) | function ScheduledScanRowActions({

FILE: frontend/components/scan/scheduled/scheduled-scan-data-table.tsx
  type ScheduledScanDataTableProps (line 13) | interface ScheduledScanDataTableProps {
  function ScheduledScanDataTable (line 35) | function ScheduledScanDataTable({

FILE: frontend/components/screenshots/screenshots-gallery.tsx
  constant PAGE_SIZE_OPTIONS (line 29) | const PAGE_SIZE_OPTIONS = [12, 24, 48]
  type Screenshot (line 31) | interface Screenshot {
  type ScreenshotsGalleryProps (line 38) | interface ScreenshotsGalleryProps {
  function ScreenshotsGallery (line 43) | function ScreenshotsGallery({ targetId, scanId }: ScreenshotsGalleryProp...

FILE: frontend/components/search/search-page.tsx
  constant WEBSITE_SEARCH_EXAMPLES (line 26) | const WEBSITE_SEARCH_EXAMPLES = [
  constant ENDPOINT_SEARCH_EXAMPLES (line 38) | const ENDPOINT_SEARCH_EXAMPLES = [
  constant QUICK_SEARCH_TAGS (line 50) | const QUICK_SEARCH_TAGS = [
  constant RECENT_SEARCHES_KEY (line 60) | const RECENT_SEARCHES_KEY = 'xingrin_recent_searches'
  constant MAX_RECENT_SEARCHES (line 61) | const MAX_RECENT_SEARCHES = 5
  function getRecentSearches (line 64) | function getRecentSearches(): string[] {
  function saveRecentSearch (line 75) | function saveRecentSearch(query: string) {
  function removeRecentSearch (line 90) | function removeRecentSearch(query: string) {
  function SearchPage (line 100) | function SearchPage() {

FILE: frontend/components/search/search-pagination.tsx
  type SearchPaginationProps (line 21) | interface SearchPaginationProps {
  constant DEFAULT_PAGE_SIZE_OPTIONS (line 31) | const DEFAULT_PAGE_SIZE_OPTIONS = [10, 20, 50, 100]
  function SearchPagination (line 36) | function SearchPagination({

FILE: frontend/components/search/search-result-card.tsx
  function isWebsiteResult (line 31) | function isWebsiteResult(result: SearchResult): result is WebsiteSearchR...
  type SearchResultCardProps (line 35) | interface SearchResultCardProps {
  function getStatusVariant (line 50) | function getStatusVariant(status: number | null): "default" | "secondary...
  function SearchResultCard (line 58) | function SearchResultCard({ result, onViewVulnerability }: SearchResultC...

FILE: frontend/components/search/search-results-table.tsx
  type SearchResultsTableProps (line 11) | interface SearchResultsTableProps {
  function SearchResultsTable (line 17) | function SearchResultsTable({ results, assetType }: SearchResultsTablePr...

FILE: frontend/components/settings/system-logs/ansi-log-viewer.tsx
  type AnsiLogViewerProps (line 7) | interface AnsiLogViewerProps {
  constant LOG_LEVEL_COLORS (line 15) | const LOG_LEVEL_COLORS: Record<string, string> = {
  function hasAnsiCodes (line 51) | function hasAnsiCodes(text: string): boolean {
  function colorizeLogContent (line 57) | function colorizeLogContent(content: string): string {
  function highlightSearch (line 88) | function highlightSearch(html: string, query: string): string {
  constant LOG_LEVEL_PATTERNS (line 114) | const LOG_LEVEL_PATTERNS = [
  constant NEW_ENTRY_PATTERNS (line 128) | const NEW_ENTRY_PATTERNS = [
  function extractLogLevel (line 139) | function extractLogLevel(line: string): string | null {
  function isNewEntryStart (line 150) | function isNewEntryStart(line: string): boolean {
  function normalizeLevel (line 155) | function normalizeLevel(l: string): string {
  function filterByLevel (line 164) | function filterByLevel(content: string, level: LogLevel): string {
  function AnsiLogViewer (line 192) | function AnsiLogViewer({ content, className, searchQuery = "", logLevel ...

FILE: frontend/components/settings/system-logs/log-toolbar.tsx
  constant LINE_OPTIONS (line 19) | const LINE_OPTIONS = [100, 200, 500, 1000, 5000, 10000] as const
  type LogLevel (line 21) | type LogLevel = "all" | "DEBUG" | "INFO" | "WARN" | "ERROR"
  constant LOG_LEVELS (line 22) | const LOG_LEVELS: LogLevel[] = ["all", "DEBUG", "INFO", "WARN", "ERROR"]
  type LogToolbarProps (line 24) | interface LogToolbarProps {
  function LogToolbar (line 36) | function LogToolbar({

FILE: frontend/components/settings/system-logs/system-logs-view.tsx
  constant DEFAULT_FILE (line 15) | const DEFAULT_FILE = "xingrin.log"
  constant DEFAULT_LINES (line 16) | const DEFAULT_LINES = 500
  function SystemLogsView (line 18) | function SystemLogsView() {

FILE: frontend/components/settings/workers/deploy-terminal-dialog.tsx
  type DeployTerminalDialogProps (line 23) | interface DeployTerminalDialogProps {
  function DeployTerminalDialog (line 45) | function DeployTerminalDialog({

FILE: frontend/components/settings/workers/worker-dialog.tsx
  type FormValues (line 31) | type FormValues = {
  type WorkerDialogProps (line 39) | interface WorkerDialogProps {
  function WorkerDialog (line 45) | function WorkerDialog({ open, onOpenChange, worker }: WorkerDialogProps) {

FILE: frontend/components/settings/workers/worker-list.tsx
  constant STATUS_MAP (line 50) | const STATUS_MAP: Record<WorkerStatus, 'online' | 'offline' | 'maintenan...
  function StatsCards (line 60) | function StatsCards({ workers, t }: { workers: WorkerNode[], t: ReturnTy...
  function QuickStartBanner (line 93) | function QuickStartBanner({ t }: { t: ReturnType<typeof useTranslations>...
  function WorkerCardView (line 154) | function WorkerCardView({
  function EmptyState (line 243) | function EmptyState({ onAdd, t }: { onAdd: () => void, t: ReturnType<typ...
  function WorkerList (line 261) | function WorkerList() {

FILE: frontend/components/site-header.tsx
  function SiteHeader (line 19) | function SiteHeader() {

FILE: frontend/components/subdomains/bulk-add-subdomains-dialog.tsx
  type BulkAddSubdomainsDialogProps (line 23) | interface BulkAddSubdomainsDialogProps {
  function BulkAddSubdomainsDialog (line 37) | function BulkAddSubdomainsDialog({

FILE: frontend/components/subdomains/subdomains-columns.tsx
  type SubdomainTranslations (line 10) | interface SubdomainTranslations {
  type CreateColumnsProps (line 21) | interface CreateColumnsProps {

FILE: frontend/components/subdomains/subdomains-data-table.tsx
  constant SUBDOMAIN_FILTER_FIELDS (line 13) | const SUBDOMAIN_FILTER_FIELDS: FilterField[] = [
  constant SUBDOMAIN_FILTER_EXAMPLES (line 18) | const SUBDOMAIN_FILTER_EXAMPLES = [
  type SubdomainsDataTableProps (line 24) | interface SubdomainsDataTableProps {
  function SubdomainsDataTable (line 52) | function SubdomainsDataTable({

FILE: frontend/components/subdomains/subdomains-detail-view.tsx
  function SubdomainsDetailView (line 28) | function SubdomainsDetailView({

FILE: frontend/components/target/add-target-dialog.tsx
  type AddTargetDialogProps (line 48) | interface AddTargetDialogProps {
  function AddTargetDialog (line 65) | function AddTargetDialog({

FILE: frontend/components/target/all-targets-columns.tsx
  type AllTargetsTranslations (line 27) | interface AllTargetsTranslations {
  function copyToClipboard (line 52) | async function copyToClipboard(text: string): Promise<boolean> {
  type CreateColumnsProps (line 74) | interface CreateColumnsProps {
  function TargetNameCell (line 86) | function TargetNameCell({
  function TargetRowActions (line 152) | function TargetRowActions({

FILE: frontend/components/target/all-targets-detail-view.tsx
  function AllTargetsDetailView (line 32) | function AllTargetsDetailView() {

FILE: frontend/components/target/target-overview.tsx
  type TargetOverviewProps (line 31) | interface TargetOverviewProps {
  function TargetOverview (line 39) | function TargetOverview({ targetId }: TargetOverviewProps) {

FILE: frontend/components/target/target-settings.tsx
  type TargetSettingsProps (line 29) | interface TargetSettingsProps {
  function TargetSettings (line 37) | function TargetSettings({ targetId }: TargetSettingsProps) {

FILE: frontend/components/target/targets-data-table.tsx
  type TargetsDataTableProps (line 13) | interface TargetsDataTableProps {
  function TargetsDataTable (line 36) | function TargetsDataTable({

FILE: frontend/components/theme-toggle.tsx
  function ThemeToggle (line 13) | function ThemeToggle() {

FILE: frontend/components/tools/commands/commands-columns.tsx
  type CommandTranslations (line 22) | interface CommandTranslations {
  type CreateColumnsProps (line 44) | interface CreateColumnsProps {
  function createCommandColumns (line 52) | function createCommandColumns({

FILE: frontend/components/tools/commands/commands-data-table.tsx
  type CommandsDataTableProps (line 9) | interface CommandsDataTableProps<TData extends { id: number }> {
  function CommandsDataTable (line 16) | function CommandsDataTable<TData extends { id: number }>({

FILE: frontend/components/tools/config/add-custom-tool-dialog.tsx
  type AddCustomToolDialogProps (line 29) | interface AddCustomToolDialogProps {
  function AddCustomToolDialog (line 39) | function AddCustomToolDialog({

FILE: frontend/components/tools/config/add-tool-dialog.tsx
  type AddToolDialogProps (line 45) | interface AddToolDialogProps {
  function generateVersionCommand (line 55) | function generateVersionCommand(toolName: string, installCommand: string...
  function AddToolDialog (line 78) | function AddToolDialog({

FILE: frontend/components/tools/config/custom-tools-list.tsx
  function CustomToolsList (line 30) | function CustomToolsList() {

FILE: frontend/components/tools/config/opensource-tools-list.tsx
  function OpensourceToolsList (line 26) | function OpensourceToolsList() {

FILE: frontend/components/tools/config/tool-card.tsx
  type ToolCardProps (line 12) | interface ToolCardProps {
  function HighlightedDescription (line 24) | function HighlightedDescription({ description }: { description: string }) {
  function ToolCard (line 32) | function ToolCard({ tool, onCheckUpdate, onEdit, onDelete, isChecking = ...

FILE: frontend/components/tools/wordlist-edit-dialog.tsx
  type WordlistEditDialogProps (line 21) | interface WordlistEditDialogProps {
  function WordlistEditDialog (line 27) | function WordlistEditDialog({

FILE: frontend/components/tools/wordlist-upload-dialog.tsx
  type WordlistUploadDialogProps (line 21) | interface WordlistUploadDialogProps {
  function WordlistUploadDialog (line 25) | function WordlistUploadDialog({ trigger }: WordlistUploadDialogProps) {

FILE: frontend/components/ui/alert-dialog.tsx
  function AlertDialog (line 9) | function AlertDialog({
  function AlertDialogTrigger (line 15) | function AlertDialogTrigger({
  function AlertDialogPortal (line 23) | function AlertDialogPortal({
  function AlertDialogOverlay (line 31) | function AlertDialogOverlay({
  function AlertDialogContent (line 47) | function AlertDialogContent({
  function AlertDialogHeader (line 66) | function AlertDialogHeader({
  function AlertDialogFooter (line 79) | function AlertDialogFooter({
  function AlertDialogTitle (line 95) | function AlertDialogTitle({
  function AlertDialogDescription (line 108) | function AlertDialogDescription({
  function AlertDialogAction (line 121) | function AlertDialogAction({
  function AlertDialogCancel (line 133) | function AlertDialogCancel({

FILE: frontend/components/ui/avatar.tsx
  function Avatar (line 8) | function Avatar({
  function AvatarImage (line 24) | function AvatarImage({
  function AvatarFallback (line 37) | function AvatarFallback({

FILE: frontend/components/ui/badge.tsx
  function Badge (line 28) | function Badge({

FILE: frontend/components/ui/button.tsx
  function Button (line 39) | function Button({

FILE: frontend/components/ui/calendar.tsx
  function Calendar (line 14) | function Calendar({
  function CalendarDayButton (line 178) | function CalendarDayButton({

FILE: frontend/components/ui/card-grid-skeleton.tsx
  type CardGridSkeletonProps (line 4) | interface CardGridSkeletonProps {
  function CardGridSkeleton (line 16) | function CardGridSkeleton({

FILE: frontend/components/ui/card.tsx
  function Card (line 5) | function Card({ className, ...props }: React.ComponentProps<"div">) {
  function CardHeader (line 18) | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
  function CardTitle (line 31) | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
  function CardDescription (line 41) | function CardDescription({ className, ...props }: React.ComponentProps<"...
  function CardAction (line 51) | function CardAction({ className, ...props }: React.ComponentProps<"div">) {
  function CardContent (line 64) | function CardContent({ className, ...props }: React.ComponentProps<"div"...
  function CardFooter (line 74) | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {

FILE: frontend/components/ui/chart.tsx
  constant THEMES (line 9) | const THEMES = { light: "", dark: ".dark" } as const
  type ChartConfig (line 11) | type ChartConfig = {
  type ChartContextProps (line 21) | type ChartContextProps = {
  function useChart (line 27) | function useChart() {
  function ChartContainer (line 37) | function ChartContainer({
  function ChartTooltipContent (line 107) | function ChartTooltipContent({
  function ChartLegendContent (line 255) | function ChartLegendContent({
  function getPayloadConfigFromPayload (line 312) | function getPayloadConfigFromPayload(

FILE: frontend/components/ui/checkbox.tsx
  function Checkbox (line 9) | function Checkbox({

FILE: frontend/components/ui/collapsible.tsx
  function Collapsible (line 5) | function Collapsible({
  function CollapsibleTrigger (line 11) | function CollapsibleTrigger({
  function CollapsibleContent (line 22) | function CollapsibleContent({

FILE: frontend/components/ui/command.tsx
  type CommandDialogProps (line 27) | interface CommandDialogProps extends DialogProps {

FILE: frontend/components/ui/confirm-dialog.tsx
  type ConfirmDialogProps (line 15) | interface ConfirmDialogProps {
  function ConfirmDialog (line 27) | function ConfirmDialog({

FILE: frontend/components/ui/copyable-popover-content.tsx
  function CopyablePopoverContent (line 13) | function CopyablePopoverContent({

FILE: frontend/components/ui/data-table-skeleton.tsx
  type DataTableSkeletonProps (line 4) | interface DataTableSkeletonProps {
  function DataTableSkeleton (line 19) | function DataTableSkeleton({

FILE: frontend/components/ui/data-table/column-header.tsx
  type DataTableColumnHeaderProps (line 8) | interface DataTableColumnHeaderProps<TData, TValue> {
  function DataTableColumnHeader (line 22) | function DataTableColumnHeader<TData, TValue>({

FILE: frontend/components/ui/data-table/column-resizer.tsx
  type ColumnResizerProps (line 6) | interface ColumnResizerProps<TData> {
  function ColumnResizer (line 24) | function ColumnResizer<TData>({ header, className }: ColumnResizerProps<...

FILE: frontend/components/ui/data-table/expandable-cell.tsx
  type ExpandableI18n (line 12) | interface ExpandableI18n {
  function ExpandableI18nProvider (line 28) | function ExpandableI18nProvider({
  function useExpandableI18n (line 45) | function useExpandableI18n() {
  type ExpandableCellProps (line 53) | interface ExpandableCellProps {
  function ExpandableCell (line 79) | function ExpandableCell({
  function ExpandableUrlCell (line 168) | function ExpandableUrlCell(props: Omit<ExpandableCellProps, "variant">) {
  function ExpandableMonoCell (line 175) | function ExpandableMonoCell(props: Omit<ExpandableCellProps, "variant">) {
  type BadgeItem (line 183) | interface BadgeItem {
  type ExpandableBadgeListProps (line 188) | interface ExpandableBadgeListProps {
  function ExpandableBadgeList (line 212) | function ExpandableBadgeList({
  type ExpandableTagListProps (line 272) | interface ExpandableTagListProps {
  function ExpandableTagList (line 293) | function ExpandableTagList({

FILE: frontend/components/ui/data-table/pagination.tsx
  type DataTablePaginationProps (line 24) | interface DataTablePaginationProps<TData> {
  constant DEFAULT_PAGE_SIZE_OPTIONS (line 31) | const DEFAULT_PAGE_SIZE_OPTIONS = [10, 20, 50, 100, 200, 500, 1000]
  function DataTablePagination (line 39) | function DataTablePagination<TData>({

FILE: frontend/components/ui/data-table/toolbar.tsx
  type DataTableToolbarProps (line 12) | interface DataTableToolbarProps {
  function DataTableToolbar (line 40) | function DataTableToolbar({

FILE: frontend/components/ui/data-table/unified-data-table.tsx
  function UnifiedDataTable (line 77) | function UnifiedDataTable<TData>({

FILE: frontend/components/ui/datetime-picker.tsx
  type DateTimePickerProps (line 16) | interface DateTimePickerProps {
  function DateTimePicker (line 24) | function DateTimePicker({

FILE: frontend/components/ui/dialog.tsx
  function Dialog (line 9) | function Dialog({
  function DialogTrigger (line 15) | function DialogTrigger({
  function DialogPortal (line 21) | function DialogPortal({
  function DialogClose (line 27) | function DialogClose({
  function DialogOverlay (line 33) | function DialogOverlay({
  function DialogContent (line 49) | function DialogContent({
  function DialogHeader (line 83) | function DialogHeader({ className, ...props }: React.ComponentProps<"div...
  function DialogFooter (line 93) | function DialogFooter({ className, ...props }: React.ComponentProps<"div...
  function DialogTitle (line 106) | function DialogTitle({
  function DialogDescription (line 119) | function DialogDescription({

FILE: frontend/components/ui/drawer.tsx
  function Drawer (line 8) | function Drawer({
  function DrawerTrigger (line 14) | function DrawerTrigger({
  function DrawerPortal (line 20) | function DrawerPortal({
  function DrawerClose (line 26) | function DrawerClose({
  function DrawerOverlay (line 32) | function DrawerOverlay({
  function DrawerContent (line 48) | function DrawerContent({
  function DrawerHeader (line 75) | function DrawerHeader({ className, ...props }: React.ComponentProps<"div...
  function DrawerFooter (line 88) | function DrawerFooter({ className, ...props }: React.ComponentProps<"div...
  function DrawerTitle (line 98) | function DrawerTitle({
  function DrawerDescription (line 111) | function DrawerDescription({

FILE: frontend/components/ui/dropdown-menu.tsx
  function DropdownMenu (line 9) | function DropdownMenu({
  function DropdownMenuPortal (line 15) | function DropdownMenuPortal({
  function DropdownMenuTrigger (line 23) | function DropdownMenuTrigger({
  function DropdownMenuContent (line 34) | function DropdownMenuContent({
  function DropdownMenuGroup (line 54) | function DropdownMenuGroup({
  function DropdownMenuItem (line 62) | function DropdownMenuItem({
  function DropdownMenuCheckboxItem (line 85) | function DropdownMenuCheckboxItem({
  function DropdownMenuRadioGroup (line 111) | function DropdownMenuRadioGroup({
  function DropdownMenuRadioItem (line 122) | function DropdownMenuRadioItem({
  function DropdownMenuLabel (line 146) | function DropdownMenuLabel({
  function DropdownMenuSeparator (line 166) | function DropdownMenuSeparator({
  function DropdownMenuShortcut (line 179) | function DropdownMenuShortcut({
  function DropdownMenuSub (line 195) | function DropdownMenuSub({
  function DropdownMenuSubTrigger (line 201) | function DropdownMenuSubTrigger({
  function DropdownMenuSubContent (line 225) | function DropdownMenuSubContent({

FILE: frontend/components/ui/dropzone.tsx
  type DropzoneContextType (line 12) | type DropzoneContextType = {
  type DropzoneProps (line 37) | type DropzoneProps = Omit<DropzoneOptions, 'onDrop'> & {
  type DropzoneContentProps (line 113) | type DropzoneContentProps = {
  type DropzoneEmptyStateProps (line 152) | type DropzoneEmptyStateProps = {

FILE: frontend/components/ui/field.tsx
  function FieldSet (line 10) | function FieldSet({ className, ...props }: React.ComponentProps<"fieldse...
  function FieldLegend (line 24) | function FieldLegend({
  function FieldGroup (line 44) | function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
  function Field (line 81) | function Field({
  function FieldContent (line 97) | function FieldContent({ className, ...props }: React.ComponentProps<"div...
  function FieldLabel (line 110) | function FieldLabel({
  function FieldTitle (line 128) | function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
  function FieldDescription (line 141) | function FieldDescription({ className, ...props }: React.ComponentProps<...
  function FieldSeparato
Copy disabled (too large) Download .json
Condensed preview — 828 files, each showing path, character count, and a content snippet. Download the .json file for the full structured content (13,360K chars).
[
  {
    "path": ".dockerignore",
    "chars": 219,
    "preview": "# Node modules(前端本地开发产物,Docker 构建时会重新安装)\nfrontend/node_modules\nfrontend/.next\n\n# Python 虚拟环境\n.venv\n__pycache__\n*.pyc\n\n# "
  },
  {
    "path": ".github/workflows/docker-build.yml",
    "chars": 8955,
    "preview": "name: Build and Push Docker Images\n\non:\n  push:\n    tags:\n      - 'v*'  # 只在推送 v 开头的 tag 时触发(如 v1.0.0)\n  workflow_dispat"
  },
  {
    "path": ".gitignore",
    "chars": 1933,
    "preview": "# ============================\n# 操作系统相关文件\n# ============================\n.DS_Store\n.DS_Store?\n._*\n.Spotlight-V100\n.Trash"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 658,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\n我们作为成员、贡献者和维护者,承诺让社区中的每个人都能在没有骚扰的环境中参与项目,不论年龄、体型、可见或不可见残障、族裔、性别特征"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 589,
    "preview": "# Contributing to XingRin\n\n感谢你愿意为 XingRin 做出贡献。\n\n## 贡献方式\n\n- 提交 Bug 报告\n- 提出功能建议或产品想法\n- 改进文档、安装流程和示例\n- 提交代码修复或新功能 Pull Req"
  },
  {
    "path": "LICENSE",
    "chars": 1068,
    "preview": "MIT License\n\nCopyright (c) 2025-2026 yyhuni\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
  },
  {
    "path": "README.md",
    "chars": 8549,
    "preview": "<h1 align=\"center\">XingRin - 星环</h1>\n\n<p align=\"center\">\n  <b>开源攻击面管理平台 (ASM) | 面向授权安全测试与防御研究的资产发现与安全自动化系统</b>\n</p>\n\n<p "
  },
  {
    "path": "SECURITY.md",
    "chars": 479,
    "preview": "# Security Policy\n\n## Supported Versions\n\n安全修复优先面向以下范围:\n\n| Version | Supported |\n| --- | --- |\n| `main` 最新提交 | ✅ |\n| 最新稳"
  },
  {
    "path": "VERSION",
    "chars": 7,
    "preview": "v1.5.8\n"
  },
  {
    "path": "backend/.gitignore",
    "chars": 500,
    "preview": "# Python\n__pycache__/\n*.py[cod]\n*$py.class\n*.so\n.Python\n*.egg-info/\ndist/\nbuild/\n.hypothesis/  # Hypothesis 属性测试缓存\n\n# 虚拟"
  },
  {
    "path": "backend/apps/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "backend/apps/asset/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "backend/apps/asset/apps.py",
    "chars": 147,
    "preview": "from django.apps import AppConfig\n\n\nclass AssetConfig(AppConfig):\n    default_auto_field = 'django.db.models.BigAutoFiel"
  },
  {
    "path": "backend/apps/asset/dtos/__init__.py",
    "chars": 404,
    "preview": "\"\"\"Asset DTOs - 数据传输对象\"\"\"\n\n# 资产模块 DTOs\nfrom .asset import (\n    SubdomainDTO,\n    WebSiteDTO,\n    IPAddressDTO,\n    Dire"
  },
  {
    "path": "backend/apps/asset/dtos/asset/__init__.py",
    "chars": 531,
    "preview": "\"\"\"Asset DTOs - 数据传输对象\"\"\"\n\nfrom .subdomain_dto import SubdomainDTO\nfrom .ip_address_dto import IPAddressDTO\nfrom .port_d"
  },
  {
    "path": "backend/apps/asset/dtos/asset/directory_dto.py",
    "chars": 367,
    "preview": "\"\"\"Directory DTO\"\"\"\n\nfrom dataclasses import dataclass\nfrom typing import Optional\n\n\n@dataclass\nclass DirectoryDTO:\n    "
  },
  {
    "path": "backend/apps/asset/dtos/asset/endpoint_dto.py",
    "chars": 816,
    "preview": "\"\"\"Endpoint DTO\"\"\"\n\nfrom dataclasses import dataclass\nfrom typing import Optional, List\n\n\n@dataclass\nclass EndpointDTO:\n"
  },
  {
    "path": "backend/apps/asset/dtos/asset/host_port_mapping_dto.py",
    "chars": 185,
    "preview": "\"\"\"HostPortMapping DTO\"\"\"\n\nfrom dataclasses import dataclass\n\n\n@dataclass\nclass HostPortMappingDTO:\n    \"\"\"主机端口映射 DTO(资产"
  },
  {
    "path": "backend/apps/asset/dtos/asset/ip_address_dto.py",
    "chars": 293,
    "preview": "\"\"\"IPAddress DTO\"\"\"\n\nfrom dataclasses import dataclass\n\n\n@dataclass\nclass IPAddressDTO:\n    \"\"\"\n    IP地址数据传输对象\n    \n    "
  },
  {
    "path": "backend/apps/asset/dtos/asset/port_dto.py",
    "chars": 213,
    "preview": "\"\"\"Port DTO\"\"\"\n\nfrom dataclasses import dataclass\n\n\n@dataclass\nclass PortDTO:\n    \"\"\"端口数据传输对象\"\"\"\n    ip_address_id: int\n"
  },
  {
    "path": "backend/apps/asset/dtos/asset/subdomain_dto.py",
    "chars": 225,
    "preview": "\"\"\"Subdomain DTO\"\"\"\n\nfrom dataclasses import dataclass\n\n\n@dataclass\nclass SubdomainDTO:\n    \"\"\"\n    子域名 DTO(纯资产表)\n    \n "
  },
  {
    "path": "backend/apps/asset/dtos/asset/vulnerability_dto.py",
    "chars": 413,
    "preview": "\"\"\"Vulnerability DTO\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import Optional, Dict, Any\nfrom decimal im"
  },
  {
    "path": "backend/apps/asset/dtos/asset/website_dto.py",
    "chars": 598,
    "preview": "\"\"\"WebSite DTO\"\"\"\n\nfrom dataclasses import dataclass\nfrom typing import List, Optional\n\n\n@dataclass\nclass WebSiteDTO:\n  "
  },
  {
    "path": "backend/apps/asset/dtos/snapshot/__init__.py",
    "chars": 569,
    "preview": "\"\"\"Snapshot DTOs\"\"\"\n\nfrom .subdomain_snapshot_dto import SubdomainSnapshotDTO\nfrom .host_port_mapping_snapshot_dto impor"
  },
  {
    "path": "backend/apps/asset/dtos/snapshot/directory_snapshot_dto.py",
    "chars": 1102,
    "preview": "\"\"\"Directory Snapshot DTO\"\"\"\n\nfrom dataclasses import dataclass\nfrom typing import Optional\nfrom apps.asset.dtos.asset i"
  },
  {
    "path": "backend/apps/asset/dtos/snapshot/endpoint_snapshot_dto.py",
    "chars": 1701,
    "preview": "\"\"\"EndpointSnapshot DTO\"\"\"\n\nfrom dataclasses import dataclass\nfrom typing import List, Optional\n\n\n@dataclass\nclass Endpo"
  },
  {
    "path": "backend/apps/asset/dtos/snapshot/host_port_mapping_snapshot_dto.py",
    "chars": 776,
    "preview": "\"\"\"HostPortMappingSnapshot DTO\"\"\"\n\nfrom dataclasses import dataclass\nfrom typing import Optional\n\n\n@dataclass\nclass Host"
  },
  {
    "path": "backend/apps/asset/dtos/snapshot/subdomain_snapshot_dto.py",
    "chars": 779,
    "preview": "\"\"\"SubdomainSnapshot DTO\"\"\"\n\nfrom dataclasses import dataclass\nfrom typing import TYPE_CHECKING\n\nif TYPE_CHECKING:\n    f"
  },
  {
    "path": "backend/apps/asset/dtos/snapshot/vulnerability_snapshot_dto.py",
    "chars": 1092,
    "preview": "\"\"\"VulnerabilitySnapshot DTO\"\"\"\n\nfrom dataclasses import dataclass, field\nfrom typing import Optional, Dict, Any\nfrom de"
  },
  {
    "path": "backend/apps/asset/dtos/snapshot/website_snapshot_dto.py",
    "chars": 1541,
    "preview": "\"\"\"WebsiteSnapshot DTO\"\"\"\n\nfrom dataclasses import dataclass\nfrom typing import List, Optional\n\n\n@dataclass\nclass Websit"
  },
  {
    "path": "backend/apps/asset/migrations/0001_initial.py",
    "chars": 29430,
    "preview": "# Generated by Django 5.2.7 on 2026-01-06 00:55\n\nimport django.contrib.postgres.fields\nimport django.contrib.postgres.in"
  },
  {
    "path": "backend/apps/asset/migrations/0002_create_search_views.py",
    "chars": 4221,
    "preview": "\"\"\"\n创建资产搜索物化视图(使用 pg_ivm 增量维护)\n\n这些视图用于资产搜索功能,提供高性能的全文搜索能力。\n\"\"\"\n\nfrom django.db import migrations\n\n\nclass Migration(migra"
  },
  {
    "path": "backend/apps/asset/migrations/0003_add_screenshot_models.py",
    "chars": 2626,
    "preview": "# Generated by Django 5.2.7 on 2026-01-07 02:21\n\nimport django.db.models.deletion\nfrom django.db import migrations, mode"
  },
  {
    "path": "backend/apps/asset/migrations/0004_add_status_code_to_screenshot.py",
    "chars": 642,
    "preview": "# Generated by Django 5.2.7 on 2026-01-07 13:29\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "backend/apps/asset/migrations/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "backend/apps/asset/models/__init__.py",
    "chars": 934,
    "preview": "# 导入所有模型,确保Django能发现它们\n\n# 业务模型\nfrom .asset_models import (\n    Subdomain,\n    WebSite,\n    Endpoint,\n    Directory,\n    "
  },
  {
    "path": "backend/apps/asset/models/asset_models.py",
    "chars": 14514,
    "preview": "\nfrom django.db import models\nfrom django.contrib.postgres.fields import ArrayField\nfrom django.contrib.postgres.indexes"
  },
  {
    "path": "backend/apps/asset/models/screenshot_models.py",
    "chars": 2323,
    "preview": "from django.db import models\n\n\nclass ScreenshotSnapshot(models.Model):\n    \"\"\"\n    截图快照\n    \n    记录:某次扫描中捕获的网站截图\n    \"\"\""
  },
  {
    "path": "backend/apps/asset/models/snapshot_models.py",
    "chars": 13242,
    "preview": "from django.db import models\nfrom django.contrib.postgres.fields import ArrayField\nfrom django.contrib.postgres.indexes "
  },
  {
    "path": "backend/apps/asset/models/statistics_models.py",
    "chars": 2984,
    "preview": "from django.db import models\n\n\nclass AssetStatistics(models.Model):\n    \"\"\"\n    资产统计表\n    \n    存储预聚合的全局统计数据,避免仪表盘实时 COUN"
  },
  {
    "path": "backend/apps/asset/repositories/__init__.py",
    "chars": 1019,
    "preview": "\"\"\"Asset Repositories - 数据访问层\"\"\"\n\n# 资产模块 Repositories\nfrom .asset import (\n    DjangoSubdomainRepository,\n    DjangoWebS"
  },
  {
    "path": "backend/apps/asset/repositories/asset/__init__.py",
    "chars": 525,
    "preview": "\"\"\"Asset Repositories - 数据访问层\"\"\"\n\nfrom .subdomain_repository import DjangoSubdomainRepository\nfrom .website_repository i"
  },
  {
    "path": "backend/apps/asset/repositories/asset/directory_repository.py",
    "chars": 5401,
    "preview": "\"\"\"\nDjango ORM 实现的 Directory Repository\n\"\"\"\n\nimport logging\nfrom typing import List, Iterator\nfrom django.db import tran"
  },
  {
    "path": "backend/apps/asset/repositories/asset/endpoint_repository.py",
    "chars": 6113,
    "preview": "\"\"\"Endpoint Repository - Django ORM 实现\"\"\"\n\nimport logging\nfrom typing import List, Iterator\n\nfrom apps.asset.models impo"
  },
  {
    "path": "backend/apps/asset/repositories/asset/host_port_mapping_repository.py",
    "chars": 4079,
    "preview": "\"\"\"HostPortMapping Repository - Django ORM 实现\"\"\"\n\nimport logging\nfrom typing import List, Iterator, Dict, Optional\n\nfrom"
  },
  {
    "path": "backend/apps/asset/repositories/asset/subdomain_repository.py",
    "chars": 3083,
    "preview": "\"\"\"Subdomain Repository - Django ORM 实现\"\"\"\n\nimport logging\nfrom typing import List, Iterator\n\nfrom django.db import tran"
  },
  {
    "path": "backend/apps/asset/repositories/asset/website_repository.py",
    "chars": 6188,
    "preview": "\"\"\"\nDjango ORM 实现的 WebSite Repository\n\"\"\"\n\nimport logging\nfrom typing import List, Generator, Optional, Iterator\nfrom dj"
  },
  {
    "path": "backend/apps/asset/repositories/snapshot/__init__.py",
    "chars": 784,
    "preview": "\"\"\"Snapshot Repositories - 数据访问层\"\"\"\n\nfrom .subdomain_snapshot_repository import DjangoSubdomainSnapshotRepository\nfrom ."
  },
  {
    "path": "backend/apps/asset/repositories/snapshot/directory_snapshot_repository.py",
    "chars": 3135,
    "preview": "\"\"\"Directory Snapshot Repository - 目录快照数据访问层\"\"\"\n\nimport logging\nfrom typing import List, Iterator\nfrom django.db import "
  },
  {
    "path": "backend/apps/asset/repositories/snapshot/endpoint_snapshot_repository.py",
    "chars": 3512,
    "preview": "\"\"\"EndpointSnapshot Repository - Django ORM 实现\"\"\"\n\nimport logging\nfrom typing import List, Iterator\n\nfrom apps.asset.mod"
  },
  {
    "path": "backend/apps/asset/repositories/snapshot/host_port_mapping_snapshot_repository.py",
    "chars": 5773,
    "preview": "\"\"\"HostPortMappingSnapshot Repository - Django ORM 实现\"\"\"\n\nimport logging\nfrom typing import List, Iterator\n\nfrom apps.as"
  },
  {
    "path": "backend/apps/asset/repositories/snapshot/subdomain_snapshot_repository.py",
    "chars": 2605,
    "preview": "\"\"\"Django ORM 实现的 SubdomainSnapshot Repository\"\"\"\n\nimport logging\nfrom typing import List, Iterator\n\nfrom apps.asset.mod"
  },
  {
    "path": "backend/apps/asset/repositories/snapshot/vulnerability_snapshot_repository.py",
    "chars": 2192,
    "preview": "\"\"\"Vulnerability Snapshot Repository - 漏洞快照数据访问层\"\"\"\n\nimport logging\nfrom typing import List\n\nfrom django.db import trans"
  },
  {
    "path": "backend/apps/asset/repositories/snapshot/website_snapshot_repository.py",
    "chars": 3367,
    "preview": "\"\"\"WebsiteSnapshot Repository - Django ORM 实现\"\"\"\n\nimport logging\nfrom typing import List, Iterator\n\nfrom apps.asset.mode"
  },
  {
    "path": "backend/apps/asset/repositories/statistics_repository.py",
    "chars": 3720,
    "preview": "\"\"\"资产统计 Repository\"\"\"\nimport logging\nfrom datetime import date, timedelta\nfrom typing import Optional, List\n\nfrom apps.a"
  },
  {
    "path": "backend/apps/asset/serializers.py",
    "chars": 8167,
    "preview": "from rest_framework import serializers\nfrom .models import Subdomain, WebSite, Directory, HostPortMapping, Endpoint, Vul"
  },
  {
    "path": "backend/apps/asset/services/__init__.py",
    "chars": 949,
    "preview": "\"\"\"Asset Services - 业务逻辑层\"\"\"\n\n# 资产模块 Services\nfrom .asset import (\n    SubdomainService,\n    WebSiteService,\n    Directo"
  },
  {
    "path": "backend/apps/asset/services/asset/__init__.py",
    "chars": 505,
    "preview": "\"\"\"Asset Services - 资产模块的业务逻辑层\"\"\"\n\nfrom .subdomain_service import SubdomainService\nfrom .website_service import WebSiteS"
  },
  {
    "path": "backend/apps/asset/services/asset/directory_service.py",
    "chars": 3852,
    "preview": "\"\"\"Directory Service - 目录业务逻辑层\"\"\"\n\nimport logging\nfrom typing import List, Iterator, Optional\n\nfrom apps.asset.repositor"
  },
  {
    "path": "backend/apps/asset/services/asset/endpoint_service.py",
    "chars": 4322,
    "preview": "\"\"\"\nEndpoint 服务层\n\n处理 URL/端点相关的业务逻辑\n\"\"\"\n\nimport logging\nfrom typing import List, Iterator, Optional\n\nfrom apps.asset.dtos"
  },
  {
    "path": "backend/apps/asset/services/asset/host_port_mapping_service.py",
    "chars": 4787,
    "preview": "\"\"\"HostPortMapping Service - 业务逻辑层\"\"\"\n\nimport logging\nfrom typing import List, Iterator, Optional, Dict\n\nfrom django.db."
  },
  {
    "path": "backend/apps/asset/services/asset/subdomain_service.py",
    "chars": 6846,
    "preview": "import logging\nfrom typing import List, Dict, Optional\nfrom dataclasses import dataclass\n\nfrom apps.asset.repositories i"
  },
  {
    "path": "backend/apps/asset/services/asset/vulnerability_service.py",
    "chars": 3213,
    "preview": "\"\"\"Vulnerability Service - 漏洞资产业务逻辑层\"\"\"\n\nimport logging\nfrom typing import List, Optional\n\nfrom apps.asset.models import"
  },
  {
    "path": "backend/apps/asset/services/asset/website_service.py",
    "chars": 4115,
    "preview": "\"\"\"WebSite Service - 网站业务逻辑层\"\"\"\n\nimport logging\nfrom typing import List, Iterator, Optional\n\nfrom apps.asset.repositorie"
  },
  {
    "path": "backend/apps/asset/services/playwright_screenshot_service.py",
    "chars": 5591,
    "preview": "\"\"\"\nPlaywright 截图服务\n\n使用 Playwright 异步批量捕获网站截图\n\"\"\"\nimport asyncio\nimport logging\nfrom typing import Optional, AsyncGenera"
  },
  {
    "path": "backend/apps/asset/services/screenshot_service.py",
    "chars": 5095,
    "preview": "\"\"\"\n截图服务\n\n负责截图的压缩、保存和同步\n\"\"\"\nimport io\nimport logging\nimport os\nfrom typing import Optional\n\nfrom PIL import Image\n\nlogge"
  },
  {
    "path": "backend/apps/asset/services/search_service.py",
    "chars": 13913,
    "preview": "\"\"\"\n资产搜索服务\n\n提供资产搜索的核心业务逻辑:\n- 从物化视图查询数据\n- 支持表达式语法解析\n- 支持 =(模糊)、==(精确)、!=(不等于)操作符\n- 支持 && (AND) 和 || (OR) 逻辑组合\n- 支持 Websit"
  },
  {
    "path": "backend/apps/asset/services/snapshot/__init__.py",
    "chars": 670,
    "preview": "\"\"\"Snapshot Services - 快照服务\"\"\"\n\nfrom .subdomain_snapshots_service import SubdomainSnapshotsService\nfrom .host_port_mappi"
  },
  {
    "path": "backend/apps/asset/services/snapshot/directory_snapshots_service.py",
    "chars": 3484,
    "preview": "\"\"\"Directory Snapshots Service - 业务逻辑层\"\"\"\n\nimport logging\nfrom typing import List, Iterator\n\nfrom apps.asset.repositorie"
  },
  {
    "path": "backend/apps/asset/services/snapshot/endpoint_snapshots_service.py",
    "chars": 3530,
    "preview": "\"\"\"Endpoint Snapshots Service - 业务逻辑层\"\"\"\n\nimport logging\nfrom typing import List, Iterator\n\nfrom apps.asset.repositories"
  },
  {
    "path": "backend/apps/asset/services/snapshot/host_port_mapping_snapshots_service.py",
    "chars": 3152,
    "preview": "\"\"\"HostPortMapping Snapshots Service - 业务逻辑层\"\"\"\n\nimport logging\nfrom typing import List, Iterator\n\nfrom apps.asset.repos"
  },
  {
    "path": "backend/apps/asset/services/snapshot/subdomain_snapshots_service.py",
    "chars": 3480,
    "preview": "import logging\nfrom typing import List, Iterator\n\nfrom apps.asset.dtos import SubdomainSnapshotDTO\nfrom apps.asset.repos"
  },
  {
    "path": "backend/apps/asset/services/snapshot/vulnerability_snapshots_service.py",
    "chars": 3378,
    "preview": "\"\"\"Vulnerability Snapshots Service - 业务逻辑层\"\"\"\n\nimport logging\nfrom typing import List, Iterator\n\nfrom apps.asset.reposit"
  },
  {
    "path": "backend/apps/asset/services/snapshot/website_snapshots_service.py",
    "chars": 3606,
    "preview": "\"\"\"Website Snapshots Service - 业务逻辑层\"\"\"\n\nimport logging\nfrom typing import List, Iterator\n\nfrom apps.asset.repositories."
  },
  {
    "path": "backend/apps/asset/services/statistics_service.py",
    "chars": 5157,
    "preview": "\"\"\"资产统计 Service\"\"\"\nimport logging\nfrom typing import Optional\n\nfrom django.db.models import Count\n\nfrom apps.asset.repos"
  },
  {
    "path": "backend/apps/asset/urls.py",
    "chars": 1258,
    "preview": "\"\"\"\nAsset 应用 URL 配置\n\"\"\"\n\nfrom django.urls import path, include\nfrom rest_framework.routers import DefaultRouter\nfrom .vi"
  },
  {
    "path": "backend/apps/asset/views/__init__.py",
    "chars": 1047,
    "preview": "\"\"\"\nAsset 应用视图模块\n\n重新导出所有视图类以保持向后兼容\n\"\"\"\n\nfrom .asset_views import (\n    AssetStatisticsViewSet,\n    SubdomainViewSet,\n   "
  },
  {
    "path": "backend/apps/asset/views/asset_views.py",
    "chars": 47442,
    "preview": "import logging\nfrom rest_framework import viewsets, status, filters\nfrom rest_framework.decorators import action\nfrom re"
  },
  {
    "path": "backend/apps/asset/views/search_views.py",
    "chars": 12560,
    "preview": "\"\"\"\n资产搜索 API 视图\n\n提供资产搜索的 REST API 接口:\n- GET /api/assets/search/ - 搜索资产\n- GET /api/assets/search/export/ - 导出搜索结果为 CSV\n\n搜"
  },
  {
    "path": "backend/apps/common/__init__.py",
    "chars": 430,
    "preview": "\"\"\"\n通用工具模块\n\n提供各种共享的工具类和函数\n\"\"\"\n\nfrom .normalizer import normalize_domain, normalize_ip, normalize_cidr, normalize_target\n"
  },
  {
    "path": "backend/apps/common/apps.py",
    "chars": 227,
    "preview": "from django.apps import AppConfig\n\n\nclass CommonConfig(AppConfig):\n    default_auto_field = 'django.db.models.BigAutoFie"
  },
  {
    "path": "backend/apps/common/authentication.py",
    "chars": 318,
    "preview": "from rest_framework.authentication import SessionAuthentication\n\n\nclass CsrfExemptSessionAuthentication(SessionAuthentic"
  },
  {
    "path": "backend/apps/common/container_bootstrap.py",
    "chars": 2722,
    "preview": "\"\"\"\n容器启动引导模块\n\n提供动态任务容器的通用初始化功能:\n- 从 Server 配置中心获取配置\n- 设置环境变量\n- 初始化 Django 环境\n\n使用方式:\n    from apps.common.container_boots"
  },
  {
    "path": "backend/apps/common/decorators/__init__.py",
    "chars": 328,
    "preview": "\"\"\"\n通用装饰器模块\n\n提供可在整个项目中复用的装饰器\n\"\"\"\n\nfrom .db_connection import (\n    ensure_db_connection,\n    auto_ensure_db_connection,\n"
  },
  {
    "path": "backend/apps/common/decorators/db_connection.py",
    "chars": 3998,
    "preview": "\"\"\"\n数据库连接装饰器\n\n提供自动数据库连接健康检查的装饰器,确保长时间运行的任务中数据库连接不会失效。\n\n主要功能:\n- @auto_ensure_db_connection: 类装饰器,自动为所有公共方法添加连接检查\n- @ensur"
  },
  {
    "path": "backend/apps/common/definitions.py",
    "chars": 469,
    "preview": "from django.db import models\n\n\nclass ScanStatus(models.TextChoices):\n    \"\"\"扫描任务状态枚举\"\"\"\n    CANCELLED = 'cancelled', '已取"
  },
  {
    "path": "backend/apps/common/error_codes.py",
    "chars": 749,
    "preview": "\"\"\"\n标准化错误码定义\n\n采用简化方案(参考 Stripe、GitHub 等大厂做法):\n- 只定义 5-10 个通用错误码\n- 未知错误使用通用错误码\n- 错误码格式:大写字母和下划线组成\n\"\"\"\n\n\nclass ErrorCodes:"
  },
  {
    "path": "backend/apps/common/exception_handlers.py",
    "chars": 1433,
    "preview": "\"\"\"\n自定义异常处理器\n\n统一处理 DRF 异常,确保错误响应格式一致\n\"\"\"\n\nfrom rest_framework.views import exception_handler\nfrom rest_framework import "
  },
  {
    "path": "backend/apps/common/management/commands/db_health_check.py",
    "chars": 17091,
    "preview": "\"\"\"\n数据库健康检查管理命令\n\n使用方法:\npython manage.py db_health_check                              # 基础延迟测试(5次)\npython manage.py db_he"
  },
  {
    "path": "backend/apps/common/management/commands/db_monitor.py",
    "chars": 6884,
    "preview": "\"\"\"\n简化的数据库性能监控命令\n\n专注于可能导致查询延迟的关键指标\n\"\"\"\n\nimport time\nfrom django.core.management.base import BaseCommand\nfrom django.db i"
  },
  {
    "path": "backend/apps/common/management/commands/init_admin.py",
    "chars": 1891,
    "preview": "\"\"\"\n初始化 admin 用户\n用法: python manage.py init_admin [--password <password>]\n\"\"\"\nimport os\nfrom django.core.management.base "
  },
  {
    "path": "backend/apps/common/migrations/0001_initial.py",
    "chars": 1765,
    "preview": "# Generated by Django 5.2.7 on 2026-01-06 00:55\n\nimport django.db.models.deletion\nfrom django.db import migrations, mode"
  },
  {
    "path": "backend/apps/common/migrations/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "backend/apps/common/models/__init__.py",
    "chars": 104,
    "preview": "\"\"\"Common models\"\"\"\nfrom apps.common.models.blacklist import BlacklistRule\n\n__all__ = ['BlacklistRule']\n"
  },
  {
    "path": "backend/apps/common/models/blacklist.py",
    "chars": 1966,
    "preview": "\"\"\"黑名单规则模型\"\"\"\nfrom django.db import models\n\n\nclass BlacklistRule(models.Model):\n    \"\"\"黑名单规则模型\n    \n    用于存储黑名单过滤规则,支持域名"
  },
  {
    "path": "backend/apps/common/normalizer.py",
    "chars": 1829,
    "preview": "import re\n\n# 预编译正则表达式,避免每次调用时重新编译\nIP_PATTERN = re.compile(r'^[\\d.:]+$')\n\n\ndef normalize_domain(domain: str) -> str:\n    "
  },
  {
    "path": "backend/apps/common/pagination.py",
    "chars": 862,
    "preview": "\"\"\"\n自定义分页器,匹配前端响应格式\n\"\"\"\nfrom rest_framework.pagination import PageNumberPagination\nfrom rest_framework.response import R"
  },
  {
    "path": "backend/apps/common/permissions.py",
    "chars": 2077,
    "preview": "\"\"\"\n集中式权限管理\n\n实现三类端点的认证逻辑:\n1. 公开端点(无需认证):登录、登出、获取当前用户状态\n2. Worker 端点(API Key 认证):注册、配置、心跳、回调、资源同步\n3. 业务端点(Session 认证):其他所"
  },
  {
    "path": "backend/apps/common/prefect_django_setup.py",
    "chars": 1328,
    "preview": "\"\"\"\nPrefect Flow Django 环境初始化模块\n\n在所有 Prefect Flow 文件开头导入此模块即可自动配置 Django 环境\n\"\"\"\n\nimport os\nimport sys\n\n\ndef setup_django"
  },
  {
    "path": "backend/apps/common/response_helpers.py",
    "chars": 2243,
    "preview": "\"\"\"\n标准化 API 响应辅助函数\n\n遵循行业标准(RFC 9457 Problem Details)和大厂实践(Google、Stripe、GitHub):\n- 成功响应只包含数据,不包含 message 字段\n- 错误响应使用机器可读"
  },
  {
    "path": "backend/apps/common/serializers/__init__.py",
    "chars": 283,
    "preview": "\"\"\"Common serializers\"\"\"\nfrom .blacklist_serializers import (\n    BlacklistRuleSerializer,\n    GlobalBlacklistRuleSerial"
  },
  {
    "path": "backend/apps/common/serializers/blacklist_serializers.py",
    "chars": 2218,
    "preview": "\"\"\"黑名单规则序列化器\"\"\"\nfrom rest_framework import serializers\n\nfrom apps.common.models import BlacklistRule\nfrom apps.common.ut"
  },
  {
    "path": "backend/apps/common/services/__init__.py",
    "chars": 366,
    "preview": "\"\"\"\n通用服务模块\n\n提供系统级别的公共服务,包括:\n- SystemLogService: 系统日志读取服务\n- BlacklistService: 黑名单过滤服务\n\n注意:FilterService 已移至 apps.common.u"
  },
  {
    "path": "backend/apps/common/services/blacklist_service.py",
    "chars": 4704,
    "preview": "\"\"\"\n黑名单规则管理服务\n\n负责黑名单规则的 CRUD 操作(数据库层面)。\n过滤逻辑请使用 apps.common.utils.BlacklistFilter。\n\n架构说明:\n- Model: BlacklistRule (apps.c"
  },
  {
    "path": "backend/apps/common/services/system_log_service.py",
    "chars": 5034,
    "preview": "\"\"\"\n系统日志服务模块\n\n提供系统日志的读取功能,支持:\n- 从日志目录读取日志文件\n- 限制返回行数,防止内存溢出\n- 列出可用的日志文件\n\"\"\"\n\nimport fnmatch\nimport logging\nimport os\nimp"
  },
  {
    "path": "backend/apps/common/signals.py",
    "chars": 726,
    "preview": "\"\"\"通用信号定义\n\n定义项目中使用的自定义信号,用于解耦各模块之间的通信。\n\n使用方式:\n1. 发布信号:signal.send(sender=SomeClass, **kwargs)\n2. 接收信号:@receiver(signal) "
  },
  {
    "path": "backend/apps/common/urls.py",
    "chars": 1247,
    "preview": "\"\"\"\n通用模块 URL 配置\n\n路由说明:\n- /api/health/       健康检查接口(无需认证)\n- /api/auth/*        认证相关接口(登录、登出、用户信息)\n- /api/system/*      系统"
  },
  {
    "path": "backend/apps/common/utils/__init__.py",
    "chars": 800,
    "preview": "\"\"\"Common utilities\"\"\"\n\nfrom .dedup import deduplicate_for_bulk, get_unique_fields\nfrom .hash import (\n    calc_file_sha"
  },
  {
    "path": "backend/apps/common/utils/blacklist_filter.py",
    "chars": 6234,
    "preview": "\"\"\"\n黑名单过滤工具\n\n提供域名、IP、CIDR、关键词的黑名单匹配功能。\n纯工具类,不涉及数据库操作。\n\n支持的规则类型:\n    1. 域名精确匹配: example.com\n       - 规则: example.com\n    "
  },
  {
    "path": "backend/apps/common/utils/csv_utils.py",
    "chars": 6368,
    "preview": "\"\"\"CSV 导出工具模块\n\n提供流式 CSV 生成功能,支持:\n- UTF-8 BOM(Excel 兼容)\n- RFC 4180 规范转义\n- 流式生成(内存友好)\n- 带 Content-Length 的文件响应(支持浏览器下载进度显示"
  },
  {
    "path": "backend/apps/common/utils/dedup.py",
    "chars": 2588,
    "preview": "\"\"\"\n批量数据去重工具\n\n用于 bulk_create 前的批次内去重,避免 PostgreSQL ON CONFLICT 错误。\n自动从 Django 模型读取唯一约束字段,无需手动指定。\n\"\"\"\n\nimport logging\nfro"
  },
  {
    "path": "backend/apps/common/utils/filter_utils.py",
    "chars": 10079,
    "preview": "\"\"\"智能过滤工具 - 通用查询语法解析和 Django ORM 查询构建\n\n支持的语法:\n- field=\"value\"     模糊匹配(包含)\n- field==\"value\"    精确匹配\n- field!=\"value\"    "
  },
  {
    "path": "backend/apps/common/utils/hash.py",
    "chars": 2130,
    "preview": "\"\"\"通用文件 hash 计算与校验工具\n\n提供 SHA-256 哈希计算和校验功能,用于:\n- 字典文件上传时计算 hash\n- Worker 侧本地缓存校验\n\"\"\"\n\nimport hashlib\nimport logging\nfrom"
  },
  {
    "path": "backend/apps/common/validators.py",
    "chars": 6963,
    "preview": "\"\"\"域名、IP、端口、URL 和目标验证工具函数\"\"\"\nimport ipaddress\nimport logging\nfrom urllib.parse import urlparse\n\nimport validators\n\nlogge"
  },
  {
    "path": "backend/apps/common/views/__init__.py",
    "chars": 616,
    "preview": "\"\"\"\n通用模块视图导出\n\n包含:\n- 健康检查视图:Docker 健康检查\n- 认证相关视图:登录、登出、用户信息、修改密码\n- 系统日志视图:实时日志查看\n- 黑名单视图:全局黑名单规则管理\n- 版本视图:系统版本和更新检查\n\"\"\"\n\n"
  },
  {
    "path": "backend/apps/common/views/auth_views.py",
    "chars": 5173,
    "preview": "\"\"\"\n认证相关视图\n使用 Django 内置认证系统,支持 Session 认证\n\"\"\"\nimport logging\nfrom django.contrib.auth import authenticate, login, logout"
  },
  {
    "path": "backend/apps/common/views/blacklist_views.py",
    "chars": 2038,
    "preview": "\"\"\"全局黑名单 API 视图\"\"\"\nimport logging\n\nfrom rest_framework import status\nfrom rest_framework.views import APIView\nfrom rest_"
  },
  {
    "path": "backend/apps/common/views/health_views.py",
    "chars": 417,
    "preview": "\"\"\"\n健康检查视图\n\n提供 Docker 健康检查端点,无需认证。\n\"\"\"\n\nfrom rest_framework.views import APIView\nfrom rest_framework.response import Res"
  },
  {
    "path": "backend/apps/common/views/system_log_views.py",
    "chars": 3363,
    "preview": "\"\"\"\n系统日志视图模块\n\n提供系统日志的 REST API 接口,供前端实时查看系统运行日志。\n\"\"\"\n\nimport logging\n\nfrom django.utils.decorators import method_decorat"
  },
  {
    "path": "backend/apps/common/views/version_views.py",
    "chars": 3773,
    "preview": "\"\"\"\n系统版本相关视图\n\"\"\"\n\nimport logging\nfrom pathlib import Path\n\nimport requests\nfrom rest_framework.request import Request\nfr"
  },
  {
    "path": "backend/apps/common/websocket_auth.py",
    "chars": 916,
    "preview": "\"\"\"\nWebSocket 认证基类\n\n提供需要认证的 WebSocket Consumer 基类\n\"\"\"\n\nimport logging\nfrom channels.generic.websocket import AsyncWebsoc"
  },
  {
    "path": "backend/apps/engine/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "backend/apps/engine/apps.py",
    "chars": 1001,
    "preview": "import os\nfrom django.apps import AppConfig\n\n\nclass EngineConfig(AppConfig):\n    default_auto_field = 'django.db.models."
  },
  {
    "path": "backend/apps/engine/consumers/__init__.py",
    "chars": 128,
    "preview": "\"\"\"\nEngine WebSocket Consumers\n\"\"\"\nfrom .worker_deploy_consumer import WorkerDeployConsumer\n\n__all__ = ['WorkerDeployCon"
  },
  {
    "path": "backend/apps/engine/consumers/worker_deploy_consumer.py",
    "chars": 15756,
    "preview": "\"\"\"\nWebSocket Consumer - Worker 交互式终端 (使用 PTY)\n\"\"\"\n\nimport json\nimport logging\nimport asyncio\nimport os\nfrom asgiref.syn"
  },
  {
    "path": "backend/apps/engine/management/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "backend/apps/engine/management/commands/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "backend/apps/engine/management/commands/init_default_engine.py",
    "chars": 4647,
    "preview": "\"\"\"\n初始化默认扫描引擎\n\n用法:\n  python manage.py init_default_engine              # 只创建不存在的引擎(不覆盖已有)\n  python manage.py init_defaul"
  },
  {
    "path": "backend/apps/engine/management/commands/init_fingerprints.py",
    "chars": 6236,
    "preview": "\"\"\"初始化内置指纹库\n\n- EHole 指纹: ehole.json -> 导入到数据库\n- Goby 指纹: goby.json -> 导入到数据库\n- Wappalyzer 指纹: wappalyzer.json -> 导入到数据库\n"
  },
  {
    "path": "backend/apps/engine/management/commands/init_nuclei_templates.py",
    "chars": 5302,
    "preview": "\"\"\"初始化 Nuclei 模板仓库\n\n项目安装后执行此命令,自动创建官方模板仓库记录。\n\n使用方式:\n    python manage.py init_nuclei_templates           # 只创建记录(检测本地已有仓"
  },
  {
    "path": "backend/apps/engine/management/commands/init_wordlists.py",
    "chars": 4989,
    "preview": "\"\"\"初始化所有内置字典 Wordlist 记录\n\n内置字典从镜像内 /app/backend/wordlist/ 复制到运行时目录 /opt/xingrin/wordlists/:\n- 目录扫描默认字典: dir_default.txt\n"
  },
  {
    "path": "backend/apps/engine/migrations/0001_initial.py",
    "chars": 13770,
    "preview": "# Generated by Django 5.2.7 on 2026-01-06 00:55\n\nfrom django.db import migrations, models\n\n\nclass Migration(migrations.M"
  },
  {
    "path": "backend/apps/engine/migrations/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "backend/apps/engine/models/__init__.py",
    "chars": 576,
    "preview": "\"\"\"Engine Models\n\n导出所有 Engine 模块的 Models\n\"\"\"\n\nfrom .engine import WorkerNode, ScanEngine, Wordlist, NucleiTemplateRepo\nf"
  },
  {
    "path": "backend/apps/engine/models/engine.py",
    "chars": 4717,
    "preview": "\"\"\"Engine 模块核心 Models\n\n包含 WorkerNode, ScanEngine, Wordlist, NucleiTemplateRepo\n\"\"\"\n\nfrom django.db import models\n\n\nclass"
  },
  {
    "path": "backend/apps/engine/models/fingerprints.py",
    "chars": 7176,
    "preview": "\"\"\"指纹相关 Models\n\n包含 EHole、Goby、Wappalyzer 等指纹格式的数据模型\n\"\"\"\n\nfrom django.db import models\n\n\nclass GobyFingerprint(models.Mod"
  },
  {
    "path": "backend/apps/engine/repositories/__init__.py",
    "chars": 527,
    "preview": "\"\"\"Engine Repositories 模块\n\n提供 ScanEngine、WorkerNode、Wordlist、NucleiRepo 等数据访问层实现\n\"\"\"\n\nfrom .django_engine_repository imp"
  },
  {
    "path": "backend/apps/engine/repositories/django_engine_repository.py",
    "chars": 966,
    "preview": "\"\"\"\nScanEngine 数据访问层 Django ORM 实现\n\n基于 Django ORM 的 ScanEngine Repository 实现类\n\"\"\"\n\nimport logging\n\nfrom ..models import "
  },
  {
    "path": "backend/apps/engine/repositories/django_wordlist_repository.py",
    "chars": 1346,
    "preview": "\"\"\"Wordlist 数据访问层 Django ORM 实现\n\n基于 Django ORM 的 Wordlist Repository 实现类\n\"\"\"\n\nimport logging\n\nfrom apps.engine.models im"
  },
  {
    "path": "backend/apps/engine/repositories/django_worker_repository.py",
    "chars": 2548,
    "preview": "\"\"\"\nWorkerNode 数据访问层 Django ORM 实现\n\n基于 Django ORM 的 WorkerNode Repository 实现类\n\"\"\"\n\nimport logging\nfrom typing import Any"
  },
  {
    "path": "backend/apps/engine/repositories/nuclei_repo_repository.py",
    "chars": 5321,
    "preview": "\"\"\"Nuclei 模板仓库 Repository 层\n\n本模块包含两个 Repository 类,负责数据访问:\n\n1. NucleiTemplateRepository\n   - 职责:ORM 操作,按 ID 查询仓库配置\n   - 被"
  },
  {
    "path": "backend/apps/engine/routing.py",
    "chars": 213,
    "preview": "\"\"\"\nWorker WebSocket 路由配置\n\"\"\"\n\nfrom django.urls import path\nfrom .consumers import WorkerDeployConsumer\n\nwebsocket_urlpa"
  },
  {
    "path": "backend/apps/engine/scheduler.py",
    "chars": 3518,
    "preview": "\"\"\"\nAPScheduler 定时任务调度器\n\n替代 Prefect Work Pool,用于触发定时任务。\n实际任务执行通过 task_distributor 分发到各 Worker。\n\"\"\"\nimport logging\nfrom a"
  },
  {
    "path": "backend/apps/engine/serializers/__init__.py",
    "chars": 390,
    "preview": "\"\"\"\nEngine Serializers\n\"\"\"\nfrom .worker_serializer import WorkerNodeSerializer\nfrom .engine_serializer import ScanEngine"
  },
  {
    "path": "backend/apps/engine/serializers/engine_serializer.py",
    "chars": 1219,
    "preview": "\"\"\"\n扫描引擎序列化器\n\"\"\"\nfrom rest_framework import serializers\nfrom apps.engine.models import ScanEngine\n\n\nclass ScanEngineSeri"
  },
  {
    "path": "backend/apps/engine/serializers/fingerprints/__init__.py",
    "chars": 583,
    "preview": "\"\"\"指纹管理 Serializers\n\n导出所有指纹相关的 Serializer 类\n\"\"\"\n\nfrom .ehole import EholeFingerprintSerializer\nfrom .goby import GobyFin"
  },
  {
    "path": "backend/apps/engine/serializers/fingerprints/arl.py",
    "chars": 818,
    "preview": "\"\"\"ARL 指纹 Serializer\"\"\"\n\nfrom rest_framework import serializers\n\nfrom apps.engine.models import ARLFingerprint\n\n\nclass A"
  },
  {
    "path": "backend/apps/engine/serializers/fingerprints/ehole.py",
    "chars": 818,
    "preview": "\"\"\"EHole 指纹 Serializer\"\"\"\n\nfrom rest_framework import serializers\n\nfrom apps.engine.models import EholeFingerprint\n\n\ncla"
  },
  {
    "path": "backend/apps/engine/serializers/fingerprints/fingerprinthub.py",
    "chars": 1514,
    "preview": "\"\"\"FingerPrintHub 指纹 Serializer\"\"\"\n\nfrom rest_framework import serializers\n\nfrom apps.engine.models import FingerPrintHu"
  },
  {
    "path": "backend/apps/engine/serializers/fingerprints/fingers.py",
    "chars": 1389,
    "preview": "\"\"\"Fingers 指纹 Serializer\"\"\"\n\nfrom rest_framework import serializers\n\nfrom apps.engine.models import FingersFingerprint\n\n"
  },
  {
    "path": "backend/apps/engine/serializers/fingerprints/goby.py",
    "chars": 749,
    "preview": "\"\"\"Goby 指纹 Serializer\"\"\"\n\nfrom rest_framework import serializers\n\nfrom apps.engine.models import GobyFingerprint\n\n\nclass"
  },
  {
    "path": "backend/apps/engine/serializers/fingerprints/wappalyzer.py",
    "chars": 731,
    "preview": "\"\"\"Wappalyzer 指纹 Serializer\"\"\"\n\nfrom rest_framework import serializers\n\nfrom apps.engine.models import WappalyzerFingerp"
  },
  {
    "path": "backend/apps/engine/serializers/nuclei_template_repo_serializer.py",
    "chars": 1282,
    "preview": "\"\"\"Nuclei 模板仓库序列化器\n\n用于 DRF ModelViewSet 的 CRUD 操作,将 NucleiTemplateRepo 模型序列化为 JSON。\n\n字段说明:\n- id: 仓库 ID(只读,自动生成)\n- name: "
  },
  {
    "path": "backend/apps/engine/serializers/wordlist_serializer.py",
    "chars": 671,
    "preview": "\"\"\"字典文件序列化器\"\"\"\n\nfrom rest_framework import serializers\n\nfrom apps.engine.models import Wordlist\n\n\nclass WordlistSerializ"
  },
  {
    "path": "backend/apps/engine/serializers/worker_serializer.py",
    "chars": 2883,
    "preview": "\"\"\"\nWorker 节点序列化器\n\"\"\"\nfrom rest_framework import serializers\nfrom apps.engine.models import WorkerNode\n\n\nclass WorkerNod"
  },
  {
    "path": "backend/apps/engine/services/__init__.py",
    "chars": 577,
    "preview": "\"\"\"\nEngine 服务层\n\"\"\"\n\nfrom .engine_service import EngineService\nfrom .worker_service import WorkerService\nfrom .wordlist_s"
  },
  {
    "path": "backend/apps/engine/services/deploy_service.py",
    "chars": 2206,
    "preview": "\"\"\"\n远程节点部署脚本服务\n\n脚本文件位置:backend/scripts/worker-deploy/\n- bootstrap.sh: 环境初始化(安装基础依赖)\n- install.sh: 安装 Docker + 拉取镜像\n- uni"
  },
  {
    "path": "backend/apps/engine/services/engine_service.py",
    "chars": 742,
    "preview": "\"\"\"\nScanEngine 业务逻辑服务层(Service)\n\n负责扫描引擎相关的业务逻辑处理\n\"\"\"\n\nimport logging\n\nfrom ..models import ScanEngine\nfrom ..repositorie"
  },
  {
    "path": "backend/apps/engine/services/fingerprints/__init__.py",
    "chars": 636,
    "preview": "\"\"\"指纹管理 Services\n\n导出所有指纹相关的 Service 类\n\"\"\"\n\nfrom .base import BaseFingerprintService\nfrom .ehole import EholeFingerprintS"
  },
  {
    "path": "backend/apps/engine/services/fingerprints/arl_service.py",
    "chars": 2704,
    "preview": "\"\"\"ARL 指纹管理 Service\n\n实现 ARL 格式指纹的校验、转换和导出逻辑\n支持 YAML 格式的导入导出\n\"\"\"\n\nimport logging\nimport yaml\n\nfrom apps.engine.models imp"
  },
  {
    "path": "backend/apps/engine/services/fingerprints/base.py",
    "chars": 3809,
    "preview": "\"\"\"指纹管理基类 Service\n\n提供通用的批量操作和缓存逻辑,供 EHole/Goby/Wappalyzer 等子类继承\n\"\"\"\n\nimport json\nimport logging\nfrom typing import Any\n\n"
  },
  {
    "path": "backend/apps/engine/services/fingerprints/ehole.py",
    "chars": 2303,
    "preview": "\"\"\"EHole 指纹管理 Service\n\n实现 EHole 格式指纹的校验、转换和导出逻辑\n\"\"\"\n\nfrom apps.engine.models import EholeFingerprint\nfrom .base import B"
  },
  {
    "path": "backend/apps/engine/services/fingerprints/fingerprinthub_service.py",
    "chars": 3271,
    "preview": "\"\"\"FingerPrintHub 指纹管理 Service\n\n实现 FingerPrintHub 格式指纹的校验、转换和导出逻辑\n\"\"\"\n\nfrom apps.engine.models import FingerPrintHubFing"
  },
  {
    "path": "backend/apps/engine/services/fingerprints/fingers_service.py",
    "chars": 2249,
    "preview": "\"\"\"Fingers 指纹管理 Service\n\n实现 Fingers 格式指纹的校验、转换和导出逻辑\n\"\"\"\n\nfrom apps.engine.models import FingersFingerprint\nfrom .base im"
  },
  {
    "path": "backend/apps/engine/services/fingerprints/goby.py",
    "chars": 2452,
    "preview": "\"\"\"Goby 指纹管理 Service\n\n实现 Goby 格式指纹的校验、转换和导出逻辑\n\"\"\"\n\nfrom apps.engine.models import GobyFingerprint\nfrom .base import Base"
  },
  {
    "path": "backend/apps/engine/services/fingerprints/wappalyzer.py",
    "chars": 2983,
    "preview": "\"\"\"Wappalyzer 指纹管理 Service\n\n实现 Wappalyzer 格式指纹的校验、转换和导出逻辑\n\"\"\"\n\nfrom apps.engine.models import WappalyzerFingerprint\nfrom"
  },
  {
    "path": "backend/apps/engine/services/nuclei_template_repo_service.py",
    "chars": 9351,
    "preview": "\"\"\"Nuclei 模板仓库业务 Service 层\n\n本模块封装 Nuclei 多仓库的核心业务逻辑:\n\n1. Git 同步(refresh_repo)\n   - 首次调用:git clone --depth 1\n   - 后续调用:gi"
  },
  {
    "path": "backend/apps/engine/services/task_distributor.py",
    "chars": 22607,
    "preview": "\"\"\"\n负载感知任务分发器\n\n根据 Worker 负载动态分发任务,支持本地和远程 Worker。\n\n核心逻辑:\n1. 查询所有在线 Worker 的负载(从心跳数据)\n2. 选择负载最低的 Worker(可能是本地或远程)\n3. 本地 W"
  },
  {
    "path": "backend/apps/engine/services/wordlist_service.py",
    "chars": 5740,
    "preview": "\"\"\"Wordlist 业务逻辑服务层(Service)\n\n负责字典文件相关的业务逻辑处理\n\"\"\"\n\nimport hashlib\nimport logging\nimport os\nimport time\nfrom typing impor"
  },
  {
    "path": "backend/apps/engine/services/worker_load_service.py",
    "chars": 4022,
    "preview": "\"\"\"\nWorker 负载服务(Redis)\n\n存储结构:\n- worker:load:{worker_id} - Hash: {cpu, mem, updated}\n- TTL: 60 秒(超时自动清理)\n\"\"\"\n\nimport logg"
  },
  {
    "path": "backend/apps/engine/services/worker_service.py",
    "chars": 5210,
    "preview": "\"\"\"\nWorkerNode 业务逻辑服务层(Service)\n\n负责 Worker 节点相关的业务逻辑处理\n\"\"\"\n\nimport logging\nfrom typing import Any\n\nfrom apps.engine.repo"
  },
  {
    "path": "backend/apps/engine/urls.py",
    "chars": 1418,
    "preview": "from django.urls import path, include\nfrom rest_framework.routers import DefaultRouter\n\nfrom .views import (\n    ScanEng"
  },
  {
    "path": "backend/apps/engine/views/__init__.py",
    "chars": 338,
    "preview": "\"\"\"Engine Views\"\"\"\nfrom .worker_views import WorkerNodeViewSet\nfrom .engine_views import ScanEngineViewSet\nfrom .wordlis"
  },
  {
    "path": "backend/apps/engine/views/engine_views.py",
    "chars": 760,
    "preview": "\"\"\"\n扫描引擎 Views\n\"\"\"\nfrom rest_framework import viewsets\n\nfrom apps.engine.serializers import ScanEngineSerializer\nfrom ap"
  },
  {
    "path": "backend/apps/engine/views/fingerprints/__init__.py",
    "chars": 612,
    "preview": "\"\"\"指纹管理 ViewSets\n\n导出所有指纹相关的 ViewSet 类\n\"\"\"\n\nfrom .base import BaseFingerprintViewSet\nfrom .ehole import EholeFingerprintV"
  },
  {
    "path": "backend/apps/engine/views/fingerprints/arl.py",
    "chars": 3753,
    "preview": "\"\"\"ARL 指纹管理 ViewSet\"\"\"\n\nimport yaml\nfrom django.http import HttpResponse\nfrom rest_framework.decorators import action\nfr"
  },
  {
    "path": "backend/apps/engine/views/fingerprints/base.py",
    "chars": 7227,
    "preview": "\"\"\"指纹管理基类 ViewSet\n\n提供通用的 CRUD 和批量操作,供 EHole/Goby/Wappalyzer 等子类继承\n\"\"\"\n\nimport json\nimport logging\n\nfrom django.http impo"
  },
  {
    "path": "backend/apps/engine/views/fingerprints/ehole.py",
    "chars": 1896,
    "preview": "\"\"\"EHole 指纹管理 ViewSet\"\"\"\n\nfrom apps.common.pagination import BasePagination\nfrom apps.engine.models import EholeFingerpr"
  },
  {
    "path": "backend/apps/engine/views/fingerprints/fingerprinthub.py",
    "chars": 2153,
    "preview": "\"\"\"FingerPrintHub 指纹管理 ViewSet\"\"\"\n\nfrom apps.common.pagination import BasePagination\nfrom apps.engine.models import Fing"
  },
  {
    "path": "backend/apps/engine/views/fingerprints/fingers.py",
    "chars": 1948,
    "preview": "\"\"\"Fingers 指纹管理 ViewSet\"\"\"\n\nfrom apps.common.pagination import BasePagination\nfrom apps.engine.models import FingersFing"
  },
  {
    "path": "backend/apps/engine/views/fingerprints/goby.py",
    "chars": 1809,
    "preview": "\"\"\"Goby 指纹管理 ViewSet\"\"\"\n\nfrom apps.common.pagination import BasePagination\nfrom apps.engine.models import GobyFingerprin"
  },
  {
    "path": "backend/apps/engine/views/fingerprints/wappalyzer.py",
    "chars": 2294,
    "preview": "\"\"\"Wappalyzer 指纹管理 ViewSet\"\"\"\n\nfrom apps.common.pagination import BasePagination\nfrom apps.engine.models import Wappalyz"
  },
  {
    "path": "backend/apps/engine/views/nuclei_template_repo_views.py",
    "chars": 8204,
    "preview": "\"\"\"Nuclei 模板仓库 View 层(HTTP 接口)\n\n本模块提供 Nuclei 多仓库管理的 REST API,基于 DRF ModelViewSet。\n\nAPI 列表:\n==========\n\n仓库 CRUD(ModelView"
  },
  {
    "path": "backend/apps/engine/views/wordlist_views.py",
    "chars": 6023,
    "preview": "\"\"\"字典管理 API Views\"\"\"\n\nimport os\n\nfrom django.core.exceptions import ValidationError\nfrom django.http import FileResponse"
  },
  {
    "path": "backend/apps/engine/views/worker_views.py",
    "chars": 14448,
    "preview": "\"\"\"\nWorker 节点 Views\n\"\"\"\nimport os\nimport threading\nimport logging\n\nfrom rest_framework import viewsets, status\nfrom rest"
  },
  {
    "path": "backend/apps/scan/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "backend/apps/scan/apps.py",
    "chars": 310,
    "preview": "from django.apps import AppConfig\n\n\nclass ScanConfig(AppConfig):\n    \"\"\"扫描应用配置类\"\"\"\n    default_auto_field = 'django.db.m"
  },
  {
    "path": "backend/apps/scan/configs/command_templates.py",
    "chars": 9818,
    "preview": "\"\"\"\n扫描工具命令模板(简化版,不使用 Jinja2)\n\n使用 Python 原生字符串格式化,零依赖。\n\"\"\"\n\nfrom django.conf import settings\n\n# ==================== 路径配置"
  },
  {
    "path": "backend/apps/scan/configs/engine_config_example.yaml",
    "chars": 5147,
    "preview": "# 引擎配置\n#\n# 参数命名:统一用中划线(如 rate-limit),系统自动转换为下划线\n# 必需参数:enabled(是否启用)\n# 可选参数:timeout(超时秒数,默认 auto 自动计算)\n\nsubdomain_discov"
  },
  {
    "path": "backend/apps/scan/flows/__init__.py",
    "chars": 344,
    "preview": "\"\"\"Prefect Flows(编排层)\n\n注意:大部分 Flow 已迁移到 scripts/ 目录作为普通脚本执行\n\"\"\"\n\nfrom .initiate_scan_flow import initiate_scan_flow\nfrom"
  },
  {
    "path": "backend/apps/scan/flows/directory_scan_flow.py",
    "chars": 14707,
    "preview": "\"\"\"\n目录扫描 Flow\n\n负责编排目录扫描的完整流程\n\n架构:\n- Flow 负责编排多个原子 Task\n- 支持并发执行扫描工具(使用 ThreadPoolTaskRunner)\n- 每个 Task 可独立重试\n- 配置由 YAML "
  },
  {
    "path": "backend/apps/scan/flows/fingerprint_detect_flow.py",
    "chars": 11034,
    "preview": "\"\"\"\n指纹识别 Flow\n\n负责编排指纹识别的完整流程\n\n架构:\n- Flow 负责编排多个原子 Task\n- 在 site_scan 后串行执行\n- 使用 xingfinger 工具识别技术栈\n- 流式处理输出,批量更新数据库\n\"\"\"\n"
  },
  {
    "path": "backend/apps/scan/flows/initiate_scan_flow.py",
    "chars": 9753,
    "preview": "\"\"\"\n扫描初始化 Flow\n\n负责编排扫描任务的初始化流程\n\n职责:\n- 使用 FlowOrchestrator 解析 YAML 配置\n- 在 Prefect Flow 中执行子 Flow(Subflow)\n- 按照 YAML 顺序编排工"
  },
  {
    "path": "backend/apps/scan/flows/port_scan_flow.py",
    "chars": 12863,
    "preview": "\"\"\"\n端口扫描 Flow\n\n负责编排端口扫描的完整流程\n\n架构:\n- Flow 负责编排多个原子 Task\n- 支持串行执行扫描工具(流式处理)\n- 每个 Task 可独立重试\n- 配置由 YAML 解析\n\"\"\"\n\nimport logg"
  },
  {
    "path": "backend/apps/scan/flows/screenshot_flow.py",
    "chars": 5960,
    "preview": "\"\"\"\n截图 Flow\n\n负责编排截图的完整流程:\n1. 从数据库获取 URL 列表(websites 和/或 endpoints)\n2. 批量截图并保存快照\n3. 同步到资产表\n\n支持两种模式:\n1. 传统模式(向后兼容):使用 targ"
  },
  {
    "path": "backend/apps/scan/flows/site_scan_flow.py",
    "chars": 12140,
    "preview": "\"\"\"\n站点扫描 Flow\n\n负责编排站点扫描的完整流程\n\n架构:\n- Flow 负责编排多个原子 Task\n- 支持串行执行扫描工具(流式处理)\n- 每个 Task 可独立重试\n- 配置由 YAML 解析\n\"\"\"\n\nimport logg"
  },
  {
    "path": "backend/apps/scan/flows/subdomain_discovery_flow.py",
    "chars": 22954,
    "preview": "\"\"\"\n子域名发现扫描 Flow\n\n负责编排子域名发现扫描的完整流程\n\n架构:\n- Flow 负责编排多个原子 Task\n- 支持并行执行扫描工具\n- 每个 Task 可独立重试\n- 配置由 YAML 解析\n\n增强流程(4 阶段):\n   "
  },
  {
    "path": "backend/apps/scan/flows/url_fetch/__init__.py",
    "chars": 505,
    "preview": "\"\"\"\nURL Fetch Flow 模块\n\n提供 URL 获取相关的 Flow:\n- url_fetch_flow: 主 Flow(按输入类型编排 + 统一后处理)\n- domain_name_url_fetch_flow: 基于 dom"
  },
  {
    "path": "backend/apps/scan/flows/url_fetch/domain_name_url_fetch_flow.py",
    "chars": 6967,
    "preview": "\"\"\"\n基于 Target 根域名的 URL 被动收集 Flow\n\n用于 waymore 等被动收集工具:\n- 输入:Target 的根域名(target_name,如 example.com)\n- 工具会自动从第三方源(Wayback M"
  },
  {
    "path": "backend/apps/scan/flows/url_fetch/main_flow.py",
    "chars": 13153,
    "preview": "\"\"\"\nURL Fetch 主 Flow\n\n负责编排不同输入类型的 URL 获取子 Flow(domain_name / sites_file),以及统一的后处理(uro 去重、httpx 验证)\n\n架构:\n- 调用 domain_name"
  }
]

// ... and 628 more files (download for full content)

About this extraction

This page contains the full source code of the yyhuni/xingrin GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 828 files (11.1 MB), approximately 3.0M tokens, and a symbol index with 3062 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!