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.
Files changed (255) hide show
  1. {voidaccess-1.9.4/voidaccess.egg-info → voidaccess-2.0.0}/PKG-INFO +4 -1
  2. voidaccess-2.0.0/alembic.ini +58 -0
  3. {voidaccess-1.9.4 → voidaccess-2.0.0}/api/main.py +15 -0
  4. {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/investigations.py +158 -15
  5. {voidaccess-1.9.4 → voidaccess-2.0.0}/config.py +22 -0
  6. {voidaccess-1.9.4 → voidaccess-2.0.0}/crawler/spider.py +123 -68
  7. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/queries.py +5 -2
  8. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/search_engine_stats.py +34 -4
  9. voidaccess-2.0.0/extractor/dependency_relationship.py +314 -0
  10. {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/entity_shape.py +31 -3
  11. {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/gazetteer.py +76 -9
  12. {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/ner.py +18 -1
  13. {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/normalizer.py +47 -2
  14. {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/pipeline.py +239 -0
  15. voidaccess-2.0.0/extractor/software_suppression.py +293 -0
  16. voidaccess-2.0.0/pacing/__init__.py +579 -0
  17. {voidaccess-1.9.4 → voidaccess-2.0.0}/pyproject.toml +12 -2
  18. {voidaccess-1.9.4 → voidaccess-2.0.0}/scraper/scrape.py +278 -70
  19. voidaccess-2.0.0/scraper/scrape_js.py +457 -0
  20. voidaccess-2.0.0/scraper/tor_pool.py +368 -0
  21. {voidaccess-1.9.4 → voidaccess-2.0.0}/search/__init__.py +40 -41
  22. voidaccess-2.0.0/search/config.py +45 -0
  23. {voidaccess-1.9.4 → voidaccess-2.0.0}/search/search.py +63 -70
  24. voidaccess-2.0.0/search/tor_isolation.py +94 -0
  25. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/blockchain.py +17 -1
  26. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/breach_lookup.py +93 -41
  27. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/data/onion_seeds.json +522 -522
  28. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/dns_enrichment.py +16 -2
  29. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/domain_reputation.py +93 -6
  30. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/engines.py +63 -44
  31. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/enrichment.py +1732 -1620
  32. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/github_scraper.py +46 -15
  33. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/gitlab_scraper.py +36 -16
  34. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/historical_intel.py +13 -1
  35. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/infostealer.py +41 -14
  36. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/ip_reputation.py +97 -4
  37. voidaccess-2.0.0/sources/nvd.py +444 -0
  38. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/paste_scraper.py +19 -3
  39. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/pastes.py +30 -20
  40. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/rss_scraper.py +33 -3
  41. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/seed_manager.py +25 -13
  42. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/shodan.py +2 -1
  43. voidaccess-2.0.0/sources/virustotal.py +168 -0
  44. voidaccess-2.0.0/tests/test_actor_extraction_gap.py +30 -0
  45. voidaccess-2.0.0/tests/test_cli_manifest.py +17 -0
  46. voidaccess-2.0.0/tests/test_community_detection_parity.py +92 -0
  47. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_crawler.py +12 -8
  48. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_db.py +58 -0
  49. voidaccess-2.0.0/tests/test_dependency_relationship.py +110 -0
  50. voidaccess-2.0.0/tests/test_entity_priority.py +83 -0
  51. voidaccess-2.0.0/tests/test_gazetteer.py +55 -0
  52. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_github_scraper.py +30 -9
  53. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_gitlab_scraper.py +22 -16
  54. voidaccess-2.0.0/tests/test_heuristic_expansion.py +125 -0
  55. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_monitor_integration.py +6 -9
  56. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_nvd.py +30 -0
  57. voidaccess-2.0.0/tests/test_pacing.py +432 -0
  58. voidaccess-2.0.0/tests/test_pacing_enrichment.py +517 -0
  59. voidaccess-2.0.0/tests/test_pacing_quotas.py +790 -0
  60. voidaccess-2.0.0/tests/test_requirement_bugs.py +76 -0
  61. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_rss_scraper.py +51 -6
  62. voidaccess-2.0.0/tests/test_scrape_isolation.py +239 -0
  63. voidaccess-2.0.0/tests/test_software_suppression.py +84 -0
  64. voidaccess-2.0.0/utils/entity_priority.py +189 -0
  65. {voidaccess-1.9.4 → voidaccess-2.0.0}/vector/store.py +0 -3
  66. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess/llm.py +189 -17
  67. {voidaccess-1.9.4 → voidaccess-2.0.0/voidaccess.egg-info}/PKG-INFO +4 -1
  68. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess.egg-info/SOURCES.txt +20 -0
  69. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess.egg-info/requires.txt +3 -0
  70. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess.egg-info/top_level.txt +1 -0
  71. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/__init__.py +1 -1
  72. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/adapters/sqlite.py +54 -1
  73. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/browser.py +15 -5
  74. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/commands/configure.py +59 -0
  75. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/commands/export.py +13 -3
  76. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/commands/investigate.py +277 -14
  77. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/config.py +30 -0
  78. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/display.py +7 -3
  79. voidaccess-1.9.4/scraper/scrape_js.py +0 -270
  80. voidaccess-1.9.4/search/config.py +0 -13
  81. voidaccess-1.9.4/sources/nvd.py +0 -245
  82. voidaccess-1.9.4/sources/virustotal.py +0 -113
  83. {voidaccess-1.9.4 → voidaccess-2.0.0}/LICENSE +0 -0
  84. {voidaccess-1.9.4 → voidaccess-2.0.0}/README.md +0 -0
  85. {voidaccess-1.9.4 → voidaccess-2.0.0}/analysis/__init__.py +0 -0
  86. {voidaccess-1.9.4 → voidaccess-2.0.0}/analysis/opsec.py +0 -0
  87. {voidaccess-1.9.4 → voidaccess-2.0.0}/analysis/patterns.py +0 -0
  88. {voidaccess-1.9.4 → voidaccess-2.0.0}/analysis/temporal.py +0 -0
  89. {voidaccess-1.9.4 → voidaccess-2.0.0}/api/__init__.py +0 -0
  90. {voidaccess-1.9.4 → voidaccess-2.0.0}/api/auth.py +0 -0
  91. {voidaccess-1.9.4 → voidaccess-2.0.0}/api/errors.py +0 -0
  92. {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/__init__.py +0 -0
  93. {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/actors.py +0 -0
  94. {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/admin.py +0 -0
  95. {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/auth.py +0 -0
  96. {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/entities.py +0 -0
  97. {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/export.py +0 -0
  98. {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/monitors.py +0 -0
  99. {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/search.py +0 -0
  100. {voidaccess-1.9.4 → voidaccess-2.0.0}/api/routes/settings.py +0 -0
  101. {voidaccess-1.9.4 → voidaccess-2.0.0}/auth/__init__.py +0 -0
  102. {voidaccess-1.9.4 → voidaccess-2.0.0}/auth/token_blacklist.py +0 -0
  103. {voidaccess-1.9.4 → voidaccess-2.0.0}/crawler/__init__.py +0 -0
  104. {voidaccess-1.9.4 → voidaccess-2.0.0}/crawler/dedup.py +0 -0
  105. {voidaccess-1.9.4 → voidaccess-2.0.0}/crawler/frontier.py +0 -0
  106. {voidaccess-1.9.4 → voidaccess-2.0.0}/crawler/utils.py +0 -0
  107. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/__init__.py +0 -0
  108. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/__init__.py +0 -0
  109. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/env.py +0 -0
  110. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0001_initial_schema.py +0 -0
  111. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0002_add_investigation_status_column.py +0 -0
  112. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0002_add_missing_tables.py +0 -0
  113. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0003_add_canonical_value_and_entity_links.py +0 -0
  114. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0004_add_page_posted_at.py +0 -0
  115. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0005_add_extraction_method.py +0 -0
  116. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0006_add_monitor_alerts.py +0 -0
  117. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0007_add_actor_style_profiles.py +0 -0
  118. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0008_add_users_table.py +0 -0
  119. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0009_add_investigation_id_to_relationships.py +0 -0
  120. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0010_add_composite_index_entity_relationships.py +0 -0
  121. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0011_add_page_extraction_cache.py +0 -0
  122. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0013_add_graph_status.py +0 -0
  123. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0015_add_progress_fields.py +0 -0
  124. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0016_backfill_graph_status.py +0 -0
  125. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0017_add_user_api_keys.py +0 -0
  126. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0018_add_user_id_to_investigations.py +0 -0
  127. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0019_add_content_safety_log.py +0 -0
  128. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0020_add_entity_source_tracking.py +0 -0
  129. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0021_add_search_engine_stats.py +0 -0
  130. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0022_add_actor_profiles.py +0 -0
  131. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0023_add_investigation_metadata.py +0 -0
  132. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0024_add_investigation_step_metrics.py +0 -0
  133. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0025_add_cancellation_requested.py +0 -0
  134. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/migrations/versions/0026_add_entity_investigation_count.py +0 -0
  135. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/models.py +0 -0
  136. {voidaccess-1.9.4 → voidaccess-2.0.0}/db/session.py +0 -0
  137. {voidaccess-1.9.4 → voidaccess-2.0.0}/export/__init__.py +0 -0
  138. {voidaccess-1.9.4 → voidaccess-2.0.0}/export/_entity_loading.py +0 -0
  139. {voidaccess-1.9.4 → voidaccess-2.0.0}/export/ioc_package.py +0 -0
  140. {voidaccess-1.9.4 → voidaccess-2.0.0}/export/misp.py +0 -0
  141. {voidaccess-1.9.4 → voidaccess-2.0.0}/export/relationships.py +0 -0
  142. {voidaccess-1.9.4 → voidaccess-2.0.0}/export/sigma.py +0 -0
  143. {voidaccess-1.9.4 → voidaccess-2.0.0}/export/snort_export.py +0 -0
  144. {voidaccess-1.9.4 → voidaccess-2.0.0}/export/stix.py +0 -0
  145. {voidaccess-1.9.4 → voidaccess-2.0.0}/export/yara_export.py +0 -0
  146. {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/__init__.py +0 -0
  147. {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/confidence.py +0 -0
  148. {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/identity.py +0 -0
  149. {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/llm_extract.py +0 -0
  150. {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/regex_patterns.py +0 -0
  151. {voidaccess-1.9.4 → voidaccess-2.0.0}/extractor/relationship_extract.py +0 -0
  152. {voidaccess-1.9.4 → voidaccess-2.0.0}/fingerprint/__init__.py +0 -0
  153. {voidaccess-1.9.4 → voidaccess-2.0.0}/fingerprint/calibration.py +0 -0
  154. {voidaccess-1.9.4 → voidaccess-2.0.0}/fingerprint/profiler.py +0 -0
  155. {voidaccess-1.9.4 → voidaccess-2.0.0}/fingerprint/stylometry.py +0 -0
  156. {voidaccess-1.9.4 → voidaccess-2.0.0}/graph/__init__.py +0 -0
  157. {voidaccess-1.9.4 → voidaccess-2.0.0}/graph/builder.py +0 -0
  158. {voidaccess-1.9.4 → voidaccess-2.0.0}/graph/export.py +0 -0
  159. {voidaccess-1.9.4 → voidaccess-2.0.0}/graph/model.py +0 -0
  160. {voidaccess-1.9.4 → voidaccess-2.0.0}/graph/queries.py +0 -0
  161. {voidaccess-1.9.4 → voidaccess-2.0.0}/graph/visualize.py +0 -0
  162. {voidaccess-1.9.4 → voidaccess-2.0.0}/i18n/__init__.py +0 -0
  163. {voidaccess-1.9.4 → voidaccess-2.0.0}/i18n/detect.py +0 -0
  164. {voidaccess-1.9.4 → voidaccess-2.0.0}/i18n/query_expand.py +0 -0
  165. {voidaccess-1.9.4 → voidaccess-2.0.0}/i18n/translate.py +0 -0
  166. {voidaccess-1.9.4 → voidaccess-2.0.0}/monitor/__init__.py +0 -0
  167. {voidaccess-1.9.4 → voidaccess-2.0.0}/monitor/_db.py +0 -0
  168. {voidaccess-1.9.4 → voidaccess-2.0.0}/monitor/alerts.py +0 -0
  169. {voidaccess-1.9.4 → voidaccess-2.0.0}/monitor/config.py +0 -0
  170. {voidaccess-1.9.4 → voidaccess-2.0.0}/monitor/diff.py +0 -0
  171. {voidaccess-1.9.4 → voidaccess-2.0.0}/monitor/jobs.py +0 -0
  172. {voidaccess-1.9.4 → voidaccess-2.0.0}/monitor/scheduler.py +0 -0
  173. {voidaccess-1.9.4 → voidaccess-2.0.0}/scraper/__init__.py +0 -0
  174. {voidaccess-1.9.4 → voidaccess-2.0.0}/search/circuit_breaker.py +0 -0
  175. {voidaccess-1.9.4 → voidaccess-2.0.0}/search/engine_catalog.py +0 -0
  176. {voidaccess-1.9.4 → voidaccess-2.0.0}/search/query_builder.py +0 -0
  177. {voidaccess-1.9.4 → voidaccess-2.0.0}/setup.cfg +0 -0
  178. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/__init__.py +0 -0
  179. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/actor_profiles.py +0 -0
  180. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/cache.py +0 -0
  181. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/cisa.py +0 -0
  182. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/data/__init__.py +0 -0
  183. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/email_reputation.py +0 -0
  184. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/hash_reputation.py +0 -0
  185. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/proxy_client.py +0 -0
  186. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/seeds.py +0 -0
  187. {voidaccess-1.9.4 → voidaccess-2.0.0}/sources/telegram.py +0 -0
  188. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_analysis_opsec.py +0 -0
  189. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_analysis_stylometry.py +0 -0
  190. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_analysis_temporal.py +0 -0
  191. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_api.py +0 -0
  192. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_api_monitors.py +0 -0
  193. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_blockchain.py +0 -0
  194. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_breach_lookup.py +0 -0
  195. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_cli_proxy_config.py +0 -0
  196. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_confidence_calibration.py +0 -0
  197. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_config.py +0 -0
  198. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_consolidated_contracts.py +0 -0
  199. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_dns_enrichment.py +0 -0
  200. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_domain_reputation.py +0 -0
  201. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_email_reputation.py +0 -0
  202. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_extractor_llm_pipeline.py +0 -0
  203. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_filter_e2e_manual.py +0 -0
  204. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_filter_parser_manual.py +0 -0
  205. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_fingerprint.py +0 -0
  206. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_graph.py +0 -0
  207. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_graph_integration.py +0 -0
  208. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_hash_reputation.py +0 -0
  209. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_i18n.py +0 -0
  210. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_infostealer.py +0 -0
  211. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_ip_reputation.py +0 -0
  212. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_llm.py +0 -0
  213. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_llm_utils.py +0 -0
  214. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_model_singleton.py +0 -0
  215. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_monitor.py +0 -0
  216. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_pagination.py +0 -0
  217. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_paste_scraper.py +0 -0
  218. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_proxy_client.py +0 -0
  219. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_ransomlook.py +0 -0
  220. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_regex_patterns.py +0 -0
  221. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_relationship_extract.py +0 -0
  222. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_scrape_js.py +0 -0
  223. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_settings.py +0 -0
  224. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_snort_export.py +0 -0
  225. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_sources.py +0 -0
  226. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_sources_enrichment_new.py +0 -0
  227. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_stix_export.py +0 -0
  228. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_vector.py +0 -0
  229. {voidaccess-1.9.4 → voidaccess-2.0.0}/tests/test_yara_export.py +0 -0
  230. {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/__init__.py +0 -0
  231. {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/async_utils.py +0 -0
  232. {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/content_dedup.py +0 -0
  233. {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/content_safety.py +0 -0
  234. {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/defang.py +0 -0
  235. {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/encryption.py +0 -0
  236. {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/enrichment_cache.py +0 -0
  237. {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/investigation_metrics.py +0 -0
  238. {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/ioc_freshness.py +0 -0
  239. {voidaccess-1.9.4 → voidaccess-2.0.0}/utils/user_keys.py +0 -0
  240. {voidaccess-1.9.4 → voidaccess-2.0.0}/vector/__init__.py +0 -0
  241. {voidaccess-1.9.4 → voidaccess-2.0.0}/vector/embedder.py +0 -0
  242. {voidaccess-1.9.4 → voidaccess-2.0.0}/vector/model_singleton.py +0 -0
  243. {voidaccess-1.9.4 → voidaccess-2.0.0}/vector/search.py +0 -0
  244. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess/__init__.py +0 -0
  245. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess/config.py +0 -0
  246. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess/llm_utils.py +0 -0
  247. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess.egg-info/dependency_links.txt +0 -0
  248. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess.egg-info/entry_points.txt +0 -0
  249. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/adapters/__init__.py +0 -0
  250. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/commands/__init__.py +0 -0
  251. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/commands/actors.py +0 -0
  252. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/commands/enrich.py +0 -0
  253. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/commands/show.py +0 -0
  254. {voidaccess-1.9.4 → voidaccess-2.0.0}/voidaccess_cli/main.py +0 -0
  255. {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: 1.9.4
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
- """Extract reset timestamp from a 429 error and return seconds to wait."""
917
- import time
918
- import re
919
- exc_str = str(exc)
920
- # OpenRouter returns X-RateLimit-Reset as epoch milliseconds in metadata
921
- match = re.search(r"'X-RateLimit-Reset':\s*'?(\d{13})'?", exc_str)
922
- if match:
923
- reset_ms = int(match.group(1))
924
- wait = (reset_ms / 1000.0) - time.time() + 1.0 # +1s buffer
925
- return max(wait, 5.0)
926
- # Fallback: 65s to outlast a 60s/1-min rate-limit window
927
- return 65.0
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
- query = session.query(Entity).filter(
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