voidaccess 1.9.4__tar.gz → 2.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {voidaccess-1.9.4/voidaccess.egg-info → voidaccess-2.0.0}/PKG-INFO +4 -1
- voidaccess-2.0.0/alembic.ini +58 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/api/main.py +15 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/investigations.py +158 -15
- {voidaccess-1.9.4 → voidaccess-2.0.0}/config.py +22 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/crawler/spider.py +123 -68
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/queries.py +5 -2
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/search_engine_stats.py +34 -4
- voidaccess-2.0.0/extractor/dependency_relationship.py +314 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/entity_shape.py +31 -3
- {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/gazetteer.py +76 -9
- {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/ner.py +18 -1
- {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/normalizer.py +47 -2
- {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/pipeline.py +239 -0
- voidaccess-2.0.0/extractor/software_suppression.py +293 -0
- voidaccess-2.0.0/pacing/__init__.py +579 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/pyproject.toml +12 -2
- {voidaccess-1.9.4 → voidaccess-2.0.0}/scraper/scrape.py +278 -70
- voidaccess-2.0.0/scraper/scrape_js.py +457 -0
- voidaccess-2.0.0/scraper/tor_pool.py +368 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/search/__init__.py +40 -41
- voidaccess-2.0.0/search/config.py +45 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/search/search.py +63 -70
- voidaccess-2.0.0/search/tor_isolation.py +94 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/blockchain.py +17 -1
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/breach_lookup.py +93 -41
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/data/onion_seeds.json +522 -522
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/dns_enrichment.py +16 -2
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/domain_reputation.py +93 -6
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/engines.py +63 -44
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/enrichment.py +1732 -1620
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/github_scraper.py +46 -15
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/gitlab_scraper.py +36 -16
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/historical_intel.py +13 -1
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/infostealer.py +41 -14
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/ip_reputation.py +97 -4
- voidaccess-2.0.0/sources/nvd.py +444 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/paste_scraper.py +19 -3
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/pastes.py +30 -20
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/rss_scraper.py +33 -3
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/seed_manager.py +25 -13
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/shodan.py +2 -1
- voidaccess-2.0.0/sources/virustotal.py +168 -0
- voidaccess-2.0.0/tests/test_actor_extraction_gap.py +30 -0
- voidaccess-2.0.0/tests/test_cli_manifest.py +17 -0
- voidaccess-2.0.0/tests/test_community_detection_parity.py +92 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_crawler.py +12 -8
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_db.py +58 -0
- voidaccess-2.0.0/tests/test_dependency_relationship.py +110 -0
- voidaccess-2.0.0/tests/test_entity_priority.py +83 -0
- voidaccess-2.0.0/tests/test_gazetteer.py +55 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_github_scraper.py +30 -9
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_gitlab_scraper.py +22 -16
- voidaccess-2.0.0/tests/test_heuristic_expansion.py +125 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_monitor_integration.py +6 -9
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_nvd.py +30 -0
- voidaccess-2.0.0/tests/test_pacing.py +432 -0
- voidaccess-2.0.0/tests/test_pacing_enrichment.py +517 -0
- voidaccess-2.0.0/tests/test_pacing_quotas.py +790 -0
- voidaccess-2.0.0/tests/test_requirement_bugs.py +76 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_rss_scraper.py +51 -6
- voidaccess-2.0.0/tests/test_scrape_isolation.py +239 -0
- voidaccess-2.0.0/tests/test_software_suppression.py +84 -0
- voidaccess-2.0.0/utils/entity_priority.py +189 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/vector/store.py +0 -3
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess/llm.py +189 -17
- {voidaccess-1.9.4 → voidaccess-2.0.0/voidaccess.egg-info}/PKG-INFO +4 -1
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess.egg-info/SOURCES.txt +20 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess.egg-info/requires.txt +3 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess.egg-info/top_level.txt +1 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/__init__.py +1 -1
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/adapters/sqlite.py +54 -1
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/browser.py +15 -5
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/commands/configure.py +59 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/commands/export.py +13 -3
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/commands/investigate.py +277 -14
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/config.py +30 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/display.py +7 -3
- voidaccess-1.9.4/scraper/scrape_js.py +0 -270
- voidaccess-1.9.4/search/config.py +0 -13
- voidaccess-1.9.4/sources/nvd.py +0 -245
- voidaccess-1.9.4/sources/virustotal.py +0 -113
- {voidaccess-1.9.4 → voidaccess-2.0.0}/LICENSE +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/README.md +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/analysis/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/analysis/opsec.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/analysis/patterns.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/analysis/temporal.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/api/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/api/auth.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/api/errors.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/actors.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/admin.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/auth.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/entities.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/export.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/monitors.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/search.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/settings.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/auth/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/auth/token_blacklist.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/crawler/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/crawler/dedup.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/crawler/frontier.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/crawler/utils.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/env.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0001_initial_schema.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0002_add_investigation_status_column.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0002_add_missing_tables.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0003_add_canonical_value_and_entity_links.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0004_add_page_posted_at.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0005_add_extraction_method.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0006_add_monitor_alerts.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0007_add_actor_style_profiles.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0008_add_users_table.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0009_add_investigation_id_to_relationships.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0010_add_composite_index_entity_relationships.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0011_add_page_extraction_cache.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0013_add_graph_status.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0015_add_progress_fields.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0016_backfill_graph_status.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0017_add_user_api_keys.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0018_add_user_id_to_investigations.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0019_add_content_safety_log.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0020_add_entity_source_tracking.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0021_add_search_engine_stats.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0022_add_actor_profiles.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0023_add_investigation_metadata.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0024_add_investigation_step_metrics.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0025_add_cancellation_requested.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0026_add_entity_investigation_count.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/models.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/db/session.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/export/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/export/_entity_loading.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/export/ioc_package.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/export/misp.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/export/relationships.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/export/sigma.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/export/snort_export.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/export/stix.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/export/yara_export.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/confidence.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/identity.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/llm_extract.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/regex_patterns.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/relationship_extract.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/fingerprint/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/fingerprint/calibration.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/fingerprint/profiler.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/fingerprint/stylometry.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/graph/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/graph/builder.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/graph/export.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/graph/model.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/graph/queries.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/graph/visualize.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/i18n/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/i18n/detect.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/i18n/query_expand.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/i18n/translate.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/monitor/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/monitor/_db.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/monitor/alerts.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/monitor/config.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/monitor/diff.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/monitor/jobs.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/monitor/scheduler.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/scraper/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/search/circuit_breaker.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/search/engine_catalog.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/search/query_builder.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/setup.cfg +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/actor_profiles.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/cache.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/cisa.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/data/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/email_reputation.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/hash_reputation.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/proxy_client.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/seeds.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/telegram.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_analysis_opsec.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_analysis_stylometry.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_analysis_temporal.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_api.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_api_monitors.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_blockchain.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_breach_lookup.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_cli_proxy_config.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_confidence_calibration.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_config.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_consolidated_contracts.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_dns_enrichment.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_domain_reputation.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_email_reputation.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_extractor_llm_pipeline.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_filter_e2e_manual.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_filter_parser_manual.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_fingerprint.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_graph.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_graph_integration.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_hash_reputation.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_i18n.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_infostealer.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_ip_reputation.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_llm.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_llm_utils.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_model_singleton.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_monitor.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_pagination.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_paste_scraper.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_proxy_client.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_ransomlook.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_regex_patterns.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_relationship_extract.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_scrape_js.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_settings.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_snort_export.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_sources.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_sources_enrichment_new.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_stix_export.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_vector.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_yara_export.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/async_utils.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/content_dedup.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/content_safety.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/defang.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/encryption.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/enrichment_cache.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/investigation_metrics.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/ioc_freshness.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/user_keys.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/vector/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/vector/embedder.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/vector/model_singleton.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/vector/search.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess/config.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess/llm_utils.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess.egg-info/dependency_links.txt +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess.egg-info/entry_points.txt +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/adapters/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/commands/__init__.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/commands/actors.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/commands/enrich.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/commands/show.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/main.py +0 -0
- {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/tor_detect.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voidaccess
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Dark web OSINT CLI — automated threat intelligence from query to report
|
|
5
5
|
Author: VoidAccess
|
|
6
6
|
License-Expression: MIT
|
|
@@ -45,6 +45,7 @@ Requires-Dist: networkx==3.4.2
|
|
|
45
45
|
Requires-Dist: pydantic==2.13.3
|
|
46
46
|
Requires-Dist: python-jose[cryptography]==3.5.0
|
|
47
47
|
Requires-Dist: passlib[bcrypt]==1.7.4
|
|
48
|
+
Requires-Dist: bcrypt<4.1,>=3.1.0
|
|
48
49
|
Requires-Dist: slowapi==0.1.9
|
|
49
50
|
Requires-Dist: redis==7.4.0
|
|
50
51
|
Requires-Dist: pyyaml==6.0.3
|
|
@@ -55,6 +56,7 @@ Requires-Dist: langdetect==1.0.9
|
|
|
55
56
|
Requires-Dist: pyvis==0.3.2
|
|
56
57
|
Requires-Dist: urllib3==2.6.3
|
|
57
58
|
Requires-Dist: trafilatura==2.0.0
|
|
59
|
+
Requires-Dist: lxml_html_clean>=0.4.2
|
|
58
60
|
Requires-Dist: requests==2.33.1
|
|
59
61
|
Requires-Dist: python-socks[asyncio]==2.8.1
|
|
60
62
|
Requires-Dist: tldextract==5.3.1
|
|
@@ -62,6 +64,7 @@ Requires-Dist: stix2==3.0.2
|
|
|
62
64
|
Provides-Extra: dev
|
|
63
65
|
Requires-Dist: pytest; extra == "dev"
|
|
64
66
|
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
67
|
+
Requires-Dist: aioresponses>=0.7.6; extra == "dev"
|
|
65
68
|
Provides-Extra: nlp
|
|
66
69
|
Requires-Dist: sentence-transformers>=3.0; extra == "nlp"
|
|
67
70
|
Requires-Dist: torch>=2.3; extra == "nlp"
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Alembic configuration for VoidAccess.
|
|
2
|
+
# Run all alembic commands from the voidaccess/ directory.
|
|
3
|
+
#
|
|
4
|
+
# Common commands:
|
|
5
|
+
# alembic upgrade head — apply all pending migrations
|
|
6
|
+
# alembic downgrade -1 — roll back one migration
|
|
7
|
+
# alembic revision --autogenerate -m "describe change" — generate a new migration
|
|
8
|
+
# alembic history — list applied migrations
|
|
9
|
+
|
|
10
|
+
[alembic]
|
|
11
|
+
script_location = db:migrations
|
|
12
|
+
prepend_sys_path = .
|
|
13
|
+
file_template = %%(rev)s_%%(slug)s
|
|
14
|
+
timezone = UTC
|
|
15
|
+
|
|
16
|
+
# sqlalchemy.url is intentionally NOT set here.
|
|
17
|
+
# It is loaded from the DATABASE_URL environment variable inside db/migrations/env.py.
|
|
18
|
+
# This prevents credentials from ever being committed to version control.
|
|
19
|
+
|
|
20
|
+
[post_write_hooks]
|
|
21
|
+
# hooks = black
|
|
22
|
+
# black.type = console_scripts
|
|
23
|
+
# black.entrypoint = black
|
|
24
|
+
# black.options = -l 88
|
|
25
|
+
|
|
26
|
+
[loggers]
|
|
27
|
+
keys = root,sqlalchemy,alembic
|
|
28
|
+
|
|
29
|
+
[handlers]
|
|
30
|
+
keys = console
|
|
31
|
+
|
|
32
|
+
[formatters]
|
|
33
|
+
keys = generic
|
|
34
|
+
|
|
35
|
+
[logger_root]
|
|
36
|
+
level = WARN
|
|
37
|
+
handlers = console
|
|
38
|
+
qualname =
|
|
39
|
+
|
|
40
|
+
[logger_sqlalchemy]
|
|
41
|
+
level = WARN
|
|
42
|
+
handlers =
|
|
43
|
+
qualname = sqlalchemy.engine
|
|
44
|
+
|
|
45
|
+
[logger_alembic]
|
|
46
|
+
level = INFO
|
|
47
|
+
handlers =
|
|
48
|
+
qualname = alembic
|
|
49
|
+
|
|
50
|
+
[handler_console]
|
|
51
|
+
class = StreamHandler
|
|
52
|
+
args = (sys.stderr,)
|
|
53
|
+
level = NOTSET
|
|
54
|
+
formatter = generic
|
|
55
|
+
|
|
56
|
+
[formatter_generic]
|
|
57
|
+
format = %(levelname)-5.5s [%(name)s] %(message)s
|
|
58
|
+
datefmt = %H:%M:%S
|
|
@@ -13,6 +13,7 @@ from __future__ import annotations
|
|
|
13
13
|
import asyncio
|
|
14
14
|
import logging
|
|
15
15
|
import os
|
|
16
|
+
import sys
|
|
16
17
|
from contextlib import asynccontextmanager
|
|
17
18
|
from datetime import datetime, timedelta, timezone
|
|
18
19
|
from typing import Callable, Optional
|
|
@@ -491,6 +492,13 @@ def _run_migrations() -> None:
|
|
|
491
492
|
|
|
492
493
|
project_root = pathlib.Path(__file__).resolve().parents[1]
|
|
493
494
|
ini_path = project_root / "alembic.ini"
|
|
495
|
+
if not ini_path.exists():
|
|
496
|
+
# Wheel data-files are installed at the virtualenv prefix, while
|
|
497
|
+
# api/ and db/ live under site-packages. Locate the shipped
|
|
498
|
+
# config there for non-editable/API deployments.
|
|
499
|
+
ini_path = pathlib.Path(sys.prefix) / "alembic.ini"
|
|
500
|
+
if not ini_path.exists():
|
|
501
|
+
raise FileNotFoundError(f"Alembic config not found: {ini_path}")
|
|
494
502
|
alembic_cfg = Config(str(ini_path))
|
|
495
503
|
alembic_cfg.set_main_option("sqlalchemy.url", os.environ["DATABASE_URL"])
|
|
496
504
|
alembic_cfg.set_main_option("script_location", str(project_root / "db" / "migrations"))
|
|
@@ -620,6 +628,13 @@ async def tor_test(_=Depends(get_current_user)) -> dict:
|
|
|
620
628
|
import aiohttp # noqa: PLC0415
|
|
621
629
|
from aiohttp_socks import ProxyConnector # noqa: PLC0415
|
|
622
630
|
|
|
631
|
+
# Deliberately NOT the shared pool in scraper/tor_pool.py, and the one
|
|
632
|
+
# justified exception to the "no private Tor connector" rule: this
|
|
633
|
+
# endpoint exists to test whether the Tor transport itself works, so it
|
|
634
|
+
# must not report health through the very session cache whose health is
|
|
635
|
+
# in question — a pooled session would make a stale or broken cached
|
|
636
|
+
# connector look like a working Tor. It also fetches one fixed clearnet
|
|
637
|
+
# URL, so no .onion stream isolation is at stake here.
|
|
623
638
|
connector = ProxyConnector.from_url(f"socks5://{TOR_PROXY_HOST}:{TOR_PROXY_PORT}")
|
|
624
639
|
timeout = aiohttp.ClientTimeout(total=10)
|
|
625
640
|
async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
|
|
@@ -95,6 +95,10 @@ router = APIRouter()
|
|
|
95
95
|
# Populated during the pipeline run; read by the GET detail endpoint.
|
|
96
96
|
_infra_cluster_cache: dict[str, list] = {}
|
|
97
97
|
|
|
98
|
+
# In-process cache: investigation_id (str) → node-id/community-id map.
|
|
99
|
+
# The DB metadata copy is the durable source of truth.
|
|
100
|
+
_communities_cache: dict[str, dict[str, int]] = {}
|
|
101
|
+
|
|
98
102
|
# In-process cache: investigation_id (str) → sources_used status dict.
|
|
99
103
|
# Populated during the pipeline run; read by the GET detail endpoint.
|
|
100
104
|
# Phase 6.1: this is now a *fast-path* cache — the DB metadata column is
|
|
@@ -124,6 +128,22 @@ def _set_infra_clusters(investigation_id: str, clusters: list) -> None:
|
|
|
124
128
|
{"infrastructure_clusters": clusters},
|
|
125
129
|
)
|
|
126
130
|
|
|
131
|
+
|
|
132
|
+
def _set_communities(investigation_id: str, communities: dict[str, int]) -> None:
|
|
133
|
+
"""Cache and persist the server-computed community partition."""
|
|
134
|
+
normalized = {
|
|
135
|
+
str(node_id): int(community_id)
|
|
136
|
+
for node_id, community_id in communities.items()
|
|
137
|
+
}
|
|
138
|
+
_communities_cache[investigation_id] = normalized
|
|
139
|
+
_update_investigation_metadata(
|
|
140
|
+
investigation_id,
|
|
141
|
+
{
|
|
142
|
+
"communities": normalized,
|
|
143
|
+
"community_count": len(set(normalized.values())) if normalized else 0,
|
|
144
|
+
},
|
|
145
|
+
)
|
|
146
|
+
|
|
127
147
|
# ---------------------------------------------------------------------------
|
|
128
148
|
# Phase 6.2 — per-phase timeouts (Phase 6.1: caches also persisted to DB)
|
|
129
149
|
# ---------------------------------------------------------------------------
|
|
@@ -411,6 +431,26 @@ async def _build_graph_phase(
|
|
|
411
431
|
session.commit()
|
|
412
432
|
except Exception as e:
|
|
413
433
|
logger.info("[%s] Edge persistence failed (non-fatal): %s", inv_uuid, e)
|
|
434
|
+
|
|
435
|
+
# Match the CLI's pre-finalization community step. Feed the helper
|
|
436
|
+
# the complete graph, before the graph endpoint's display truncation
|
|
437
|
+
# to 20 nodes/50 edges; using that response slice would create an
|
|
438
|
+
# incomplete partition and break CLI/API parity.
|
|
439
|
+
try:
|
|
440
|
+
communities = await asyncio.to_thread(
|
|
441
|
+
_detect_communities_for_graph, graph_obj
|
|
442
|
+
)
|
|
443
|
+
_set_communities(str(inv_uuid), communities)
|
|
444
|
+
logger.info(
|
|
445
|
+
"[%s] Community detection: %s communities (%s nodes)",
|
|
446
|
+
inv_uuid,
|
|
447
|
+
len(set(communities.values())) if communities else 0,
|
|
448
|
+
len(communities),
|
|
449
|
+
)
|
|
450
|
+
except Exception as exc:
|
|
451
|
+
logger.warning(
|
|
452
|
+
"[%s] Community detection failed (non-fatal): %s", inv_uuid, exc
|
|
453
|
+
)
|
|
414
454
|
except Exception as exc:
|
|
415
455
|
logger.exception("[%s] Graph building failed: %s", inv_uuid, str(exc))
|
|
416
456
|
return None
|
|
@@ -725,6 +765,7 @@ def _get_db_investigation(investigation_id: str) -> Any:
|
|
|
725
765
|
|
|
726
766
|
db_sources_used = db_metadata.get("sources_used") or {}
|
|
727
767
|
db_infra_clusters = db_metadata.get("infrastructure_clusters") or []
|
|
768
|
+
db_communities = db_metadata.get("communities") or {}
|
|
728
769
|
|
|
729
770
|
sources_used = (
|
|
730
771
|
_sources_used_cache.get(str(inv.id))
|
|
@@ -736,6 +777,15 @@ def _get_db_investigation(investigation_id: str) -> Any:
|
|
|
736
777
|
or _infra_cluster_cache.get(investigation_id)
|
|
737
778
|
or db_infra_clusters
|
|
738
779
|
)
|
|
780
|
+
communities = (
|
|
781
|
+
_communities_cache.get(str(inv.id))
|
|
782
|
+
or _communities_cache.get(investigation_id)
|
|
783
|
+
or db_communities
|
|
784
|
+
)
|
|
785
|
+
communities = {
|
|
786
|
+
str(node_id): int(community_id)
|
|
787
|
+
for node_id, community_id in communities.items()
|
|
788
|
+
}
|
|
739
789
|
|
|
740
790
|
return {
|
|
741
791
|
"id": str(inv.id),
|
|
@@ -759,6 +809,8 @@ def _get_db_investigation(investigation_id: str) -> Any:
|
|
|
759
809
|
"paste_sources_used": paste_sources_used,
|
|
760
810
|
"infrastructure_clusters": infrastructure_clusters,
|
|
761
811
|
"sources_used": sources_used,
|
|
812
|
+
"communities": communities,
|
|
813
|
+
"community_count": len(set(communities.values())) if communities else 0,
|
|
762
814
|
"seeds_discovered": _count_discovered_seeds_for_investigation(str(inv.id)),
|
|
763
815
|
}
|
|
764
816
|
except HTTPException:
|
|
@@ -912,19 +964,34 @@ async def _get_investigation_model_choice(model: Optional[str]) -> tuple[str, An
|
|
|
912
964
|
# ---------------------------------------------------------------------------
|
|
913
965
|
|
|
914
966
|
|
|
967
|
+
# LLM retry bounds. Unlike an enrichment client's documented interval, 65 s here
|
|
968
|
+
# is a guess ("outlast a 1-minute window"), so it is a fallback rather than a
|
|
969
|
+
# floor — a provider naming a shorter wait is honoured down to the minimum.
|
|
970
|
+
_LLM_RATE_LIMIT_FALLBACK = 65.0
|
|
971
|
+
_LLM_RATE_LIMIT_MIN_WAIT = 5.0
|
|
972
|
+
_LLM_RATE_LIMIT_MAX_WAIT = 300.0
|
|
973
|
+
|
|
974
|
+
|
|
915
975
|
def _parse_rate_limit_reset(exc: Exception) -> float:
|
|
916
|
-
"""
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
976
|
+
"""
|
|
977
|
+
Seconds to wait after an LLM 429, from the provider's own headers.
|
|
978
|
+
|
|
979
|
+
Delegates to ``pacing.retry_after_seconds`` so there is one implementation
|
|
980
|
+
of "the server told us how long to wait" shared with the enrichment API
|
|
981
|
+
clients, rather than two that can drift. The exception object is passed
|
|
982
|
+
through directly: LangChain buries the 429 headers in the exception message,
|
|
983
|
+
which that helper handles via its string branch.
|
|
984
|
+
|
|
985
|
+
The 65 s fallback outlasts a 60 s/1-min window when no header is present.
|
|
986
|
+
"""
|
|
987
|
+
import pacing
|
|
988
|
+
|
|
989
|
+
return pacing.retry_after_seconds(
|
|
990
|
+
exc,
|
|
991
|
+
fallback=_LLM_RATE_LIMIT_FALLBACK,
|
|
992
|
+
floor=_LLM_RATE_LIMIT_MIN_WAIT,
|
|
993
|
+
max_wait=_LLM_RATE_LIMIT_MAX_WAIT,
|
|
994
|
+
)
|
|
928
995
|
|
|
929
996
|
|
|
930
997
|
async def _llm_with_backoff(fn, *args, max_retries: int = 4, investigation_id: "uuid.UUID | None" = None, **kwargs):
|
|
@@ -2835,7 +2902,7 @@ async def get_investigation_entities(
|
|
|
2835
2902
|
raise HTTPException(status_code=503, detail="Database not configured")
|
|
2836
2903
|
try:
|
|
2837
2904
|
from db.session import get_session
|
|
2838
|
-
from db.models import Entity, InvestigationEntityLink
|
|
2905
|
+
from db.models import Entity, EntityRelationship, InvestigationEntityLink
|
|
2839
2906
|
from db.queries import get_investigation_by_id_or_run
|
|
2840
2907
|
from graph.builder import _make_node_id
|
|
2841
2908
|
from utils.ioc_freshness import get_freshness_tag, get_freshness_display
|
|
@@ -2853,10 +2920,24 @@ async def get_investigation_entities(
|
|
|
2853
2920
|
sa_select(InvestigationEntityLink.entity_id)
|
|
2854
2921
|
.where(InvestigationEntityLink.investigation_id == inv.id)
|
|
2855
2922
|
)
|
|
2856
|
-
|
|
2923
|
+
scoped_query = session.query(Entity).filter(
|
|
2857
2924
|
(Entity.investigation_id == inv.id)
|
|
2858
2925
|
| Entity.id.in_(linked_ids_select)
|
|
2859
2926
|
)
|
|
2927
|
+
# Scores are normalized against the complete investigation, not
|
|
2928
|
+
# the current page or confidence/type filter. This keeps ranking
|
|
2929
|
+
# stable across pagination and makes it comparable to the CLI
|
|
2930
|
+
# export for the same investigation.
|
|
2931
|
+
score_entities = scoped_query.all()
|
|
2932
|
+
relationships = (
|
|
2933
|
+
session.query(EntityRelationship)
|
|
2934
|
+
.filter(EntityRelationship.investigation_id == inv.id)
|
|
2935
|
+
.all()
|
|
2936
|
+
)
|
|
2937
|
+
from utils.entity_priority import score_map
|
|
2938
|
+
priority_by_id = score_map(score_entities, relationships)
|
|
2939
|
+
|
|
2940
|
+
query = scoped_query
|
|
2860
2941
|
if entity_type:
|
|
2861
2942
|
query = query.filter(Entity.entity_type == entity_type)
|
|
2862
2943
|
if min_confidence > 0.0:
|
|
@@ -2929,6 +3010,18 @@ async def get_investigation_entities(
|
|
|
2929
3010
|
"corroborating_sources": json.loads(e.corroborating_sources or '["dark_web_scrape"]'),
|
|
2930
3011
|
"cross_referenced": (getattr(e, "investigation_count", 1) or 1) > 1,
|
|
2931
3012
|
"graph_node_id": graph_node_id,
|
|
3013
|
+
"priority_score": priority_by_id.get(str(e.id), {}).get(
|
|
3014
|
+
"priority_score", 0.0
|
|
3015
|
+
),
|
|
3016
|
+
"priority_score_components": priority_by_id.get(
|
|
3017
|
+
str(e.id), {}
|
|
3018
|
+
).get("priority_score_components", {}),
|
|
3019
|
+
"priority_score_centrality_contribution": priority_by_id.get(
|
|
3020
|
+
str(e.id), {}
|
|
3021
|
+
).get("priority_score_centrality_contribution", 0.0),
|
|
3022
|
+
"typed_relationship_degree": priority_by_id.get(
|
|
3023
|
+
str(e.id), {}
|
|
3024
|
+
).get("typed_relationship_degree", 0),
|
|
2932
3025
|
"defanged": defang,
|
|
2933
3026
|
}
|
|
2934
3027
|
)
|
|
@@ -2961,8 +3054,9 @@ async def export_investigation_entities_csv(
|
|
|
2961
3054
|
|
|
2962
3055
|
try:
|
|
2963
3056
|
from db.session import get_session
|
|
2964
|
-
from db.models import Entity, InvestigationEntityLink
|
|
3057
|
+
from db.models import Entity, EntityRelationship, InvestigationEntityLink
|
|
2965
3058
|
from db.queries import get_investigation_by_id_or_run
|
|
3059
|
+
from utils.entity_priority import score_map
|
|
2966
3060
|
|
|
2967
3061
|
with get_session() as session:
|
|
2968
3062
|
inv = get_investigation_by_id_or_run(session, inv_uuid)
|
|
@@ -2983,6 +3077,12 @@ async def export_investigation_entities_csv(
|
|
|
2983
3077
|
)
|
|
2984
3078
|
.all()
|
|
2985
3079
|
)
|
|
3080
|
+
relationships = (
|
|
3081
|
+
session.query(EntityRelationship)
|
|
3082
|
+
.filter(EntityRelationship.investigation_id == inv.id)
|
|
3083
|
+
.all()
|
|
3084
|
+
)
|
|
3085
|
+
priority_by_id = score_map(entities, relationships)
|
|
2986
3086
|
|
|
2987
3087
|
output = io.StringIO()
|
|
2988
3088
|
writer = csv.writer(output)
|
|
@@ -2990,6 +3090,7 @@ async def export_investigation_entities_csv(
|
|
|
2990
3090
|
"entity_type",
|
|
2991
3091
|
"canonical_value",
|
|
2992
3092
|
"confidence",
|
|
3093
|
+
"priority_score",
|
|
2993
3094
|
"occurrence_count",
|
|
2994
3095
|
"first_seen_page",
|
|
2995
3096
|
"context_snippet",
|
|
@@ -3011,6 +3112,7 @@ async def export_investigation_entities_csv(
|
|
|
3011
3112
|
e.entity_type,
|
|
3012
3113
|
e.canonical_value or e.value,
|
|
3013
3114
|
e.confidence,
|
|
3115
|
+
priority_by_id.get(str(e.id), {}).get("priority_score", 0.0),
|
|
3014
3116
|
1,
|
|
3015
3117
|
source_url,
|
|
3016
3118
|
context[:500],
|
|
@@ -3080,6 +3182,27 @@ def _rebuild_networkx_graph(nodes: list, edges: list) -> "nx.DiGraph":
|
|
|
3080
3182
|
return G
|
|
3081
3183
|
|
|
3082
3184
|
|
|
3185
|
+
def _detect_communities_for_graph(graph_obj) -> dict[str, int]:
|
|
3186
|
+
"""Run the shared detector over the complete API graph.
|
|
3187
|
+
|
|
3188
|
+
``_rebuild_networkx_graph`` is the API representation adapter. It is
|
|
3189
|
+
correct here because it receives the full ``to_json`` graph, not the
|
|
3190
|
+
endpoint's presentation slice. Its directed graph is equivalent to the
|
|
3191
|
+
CLI helper's simple undirected graph for detection: ``detect_communities``
|
|
3192
|
+
projects either input to an undirected graph before running the algorithm.
|
|
3193
|
+
"""
|
|
3194
|
+
from graph.builder import detect_communities
|
|
3195
|
+
from graph.export import to_json
|
|
3196
|
+
|
|
3197
|
+
graph_data = to_json(graph_obj)
|
|
3198
|
+
rebuilt = _rebuild_networkx_graph(graph_data["nodes"], graph_data["edges"])
|
|
3199
|
+
partition = detect_communities(rebuilt)
|
|
3200
|
+
return {
|
|
3201
|
+
str(node_id): int(community_id)
|
|
3202
|
+
for node_id, community_id in partition.items()
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
|
|
3083
3206
|
@router.get("/{investigation_id}/graph")
|
|
3084
3207
|
async def get_investigation_graph(
|
|
3085
3208
|
investigation_id: str,
|
|
@@ -3106,6 +3229,20 @@ async def get_investigation_graph(
|
|
|
3106
3229
|
if graph_status not in ("built", "complete", "skipped_overflow", "no_data"):
|
|
3107
3230
|
return {"status": "pending"}
|
|
3108
3231
|
internal_id = inv.id
|
|
3232
|
+
graph_communities = _communities_cache.get(str(internal_id), {})
|
|
3233
|
+
if not graph_communities:
|
|
3234
|
+
graph_metadata = getattr(inv, "metadata_json", None)
|
|
3235
|
+
if isinstance(graph_metadata, str):
|
|
3236
|
+
try:
|
|
3237
|
+
graph_metadata = json.loads(graph_metadata)
|
|
3238
|
+
except (ValueError, TypeError):
|
|
3239
|
+
graph_metadata = {}
|
|
3240
|
+
if isinstance(graph_metadata, dict):
|
|
3241
|
+
graph_communities = graph_metadata.get("communities") or {}
|
|
3242
|
+
graph_communities = {
|
|
3243
|
+
str(node_id): int(community_id)
|
|
3244
|
+
for node_id, community_id in graph_communities.items()
|
|
3245
|
+
}
|
|
3109
3246
|
|
|
3110
3247
|
graph = build_graph_from_db_cached(investigation_id=internal_id) if not force_rebuild else build_graph_from_db(investigation_id=internal_id)
|
|
3111
3248
|
graph_data = to_json(graph)
|
|
@@ -3119,6 +3256,12 @@ async def get_investigation_graph(
|
|
|
3119
3256
|
"summary_stats": summary_stats(graph),
|
|
3120
3257
|
"nodes": nodes,
|
|
3121
3258
|
"edges": edges,
|
|
3259
|
+
"communities": graph_communities,
|
|
3260
|
+
"community_count": (
|
|
3261
|
+
len(set(graph_communities.values()))
|
|
3262
|
+
if graph_communities
|
|
3263
|
+
else 0
|
|
3264
|
+
),
|
|
3122
3265
|
"visualization_html": viz_html,
|
|
3123
3266
|
}
|
|
3124
3267
|
except HTTPException:
|
|
@@ -37,6 +37,16 @@ DATABASE_URL = _clean_env("DATABASE_URL")
|
|
|
37
37
|
TOR_PROXY_HOST = _clean_env("TOR_PROXY_HOST", "127.0.0.1")
|
|
38
38
|
TOR_PROXY_PORT = _clean_env("TOR_PROXY_PORT", "9050")
|
|
39
39
|
|
|
40
|
+
# Optional second SocksPort configured with `IsolateDestAddr`, used only by the
|
|
41
|
+
# Playwright renderer. Every aiohttp caller isolates by handing Tor a distinct
|
|
42
|
+
# SOCKS credential per target, but Playwright's driver refuses SOCKS5
|
|
43
|
+
# credentials outright, so that path needs Tor to do the keying itself.
|
|
44
|
+
# docker/Dockerfile.tor opens 9250 for exactly this. If the port is absent —
|
|
45
|
+
# system tor, Tor Browser, any deployment not using our container — the JS
|
|
46
|
+
# render path runs un-isolated and says so once; it is never silently assumed to
|
|
47
|
+
# be isolated. Set to an empty value to disable the probe entirely.
|
|
48
|
+
TOR_ISOLATION_SOCKS_PORT = _clean_env("TOR_ISOLATION_SOCKS_PORT", "9250")
|
|
49
|
+
|
|
40
50
|
# Phase 1D — expanded sources (all optional; missing vars disable that source)
|
|
41
51
|
DARKSEARCH_API_KEY = _clean_env("DARKSEARCH_API_KEY") # optional free-tier key
|
|
42
52
|
TELEGRAM_API_ID = _clean_env("TELEGRAM_API_ID") # from my.telegram.org
|
|
@@ -80,6 +90,14 @@ else:
|
|
|
80
90
|
OTX_API_KEY = _clean_env("OTX_API_KEY", "") # AlienVault OTX — free at otx.alienvault.com
|
|
81
91
|
VT_API_KEY = _clean_env("VT_API_KEY", "") # VirusTotal — free tier at virustotal.com
|
|
82
92
|
|
|
93
|
+
# VirusTotal pacing tier. The public API allows 4 requests/minute (a 15 s gap);
|
|
94
|
+
# premium keys have no per-minute limit at all, and a key string is
|
|
95
|
+
# indistinguishable between the two, so a paying subscriber was being paced 25x
|
|
96
|
+
# slower than their entitlement. Set VT_API_TIER=premium to opt out of
|
|
97
|
+
# free-tier pacing. Unset or unrecognised means "public" — the safe default,
|
|
98
|
+
# since guessing premium wrongly earns a 429.
|
|
99
|
+
VT_API_TIER = (_clean_env("VT_API_TIER", "public") or "public").strip().lower()
|
|
100
|
+
|
|
83
101
|
# IP Reputation Enrichment (all optional — features degrade gracefully without keys)
|
|
84
102
|
ABUSEIPDB_API_KEY = _clean_env("ABUSEIPDB_API_KEY", "") # Community IP abuse reports
|
|
85
103
|
GREYNOISE_API_KEY = _clean_env("GREYNOISE_API_KEY", "") # Suppresses benign scanner IPs
|
|
@@ -127,6 +145,10 @@ SCRAPINGANT_API_KEY = _clean_env("SCRAPINGANT_API_KEY", "")
|
|
|
127
145
|
# UserApiKey registration on the API surface).
|
|
128
146
|
SCRAPINGANT_PROXY_TYPE = _clean_env("SCRAPINGANT_PROXY_TYPE", "residential")
|
|
129
147
|
|
|
148
|
+
# Shodan InternetDB publishes NO rate limit and needs no API key (verified
|
|
149
|
+
# 2026-07-29) — this is VoidAccess's own courtesy pacing, not a quota. It is
|
|
150
|
+
# still routed through pacing.scale_delay_floor() at the call site in
|
|
151
|
+
# sources/shodan.py so the mechanism matches every other enrichment client.
|
|
130
152
|
SHODAN_RATE_LIMIT_DELAY = 1.0 # seconds between Shodan requests (InternetDB)
|
|
131
153
|
MAX_IPS_PER_INVESTIGATION = 50 # max IPs to query Shodan per investigation
|
|
132
154
|
MAX_HASHES_PER_INVESTIGATION = 20 # max file hashes to query VirusTotal per investigation
|