voidaccess 1.9.1__tar.gz → 1.9.2__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 (229) hide show
  1. {voidaccess-1.9.1 → voidaccess-1.9.2}/PKG-INFO +12 -1
  2. {voidaccess-1.9.1 → voidaccess-1.9.2}/README.md +11 -0
  3. {voidaccess-1.9.1 → voidaccess-1.9.2}/export/ioc_package.py +1 -1
  4. {voidaccess-1.9.1 → voidaccess-1.9.2}/export/misp.py +72 -19
  5. voidaccess-1.9.2/export/relationships.py +81 -0
  6. {voidaccess-1.9.1 → voidaccess-1.9.2}/export/stix.py +45 -22
  7. {voidaccess-1.9.1 → voidaccess-1.9.2}/graph/builder.py +10 -2
  8. {voidaccess-1.9.1 → voidaccess-1.9.2}/pyproject.toml +1 -1
  9. {voidaccess-1.9.1 → voidaccess-1.9.2}/vector/model_singleton.py +5 -0
  10. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess.egg-info/PKG-INFO +12 -1
  11. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess.egg-info/SOURCES.txt +1 -0
  12. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess_cli/__init__.py +1 -1
  13. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess_cli/commands/configure.py +2 -3
  14. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess_cli/commands/export.py +17 -0
  15. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess_cli/config.py +67 -20
  16. {voidaccess-1.9.1 → voidaccess-1.9.2}/LICENSE +0 -0
  17. {voidaccess-1.9.1 → voidaccess-1.9.2}/analysis/__init__.py +0 -0
  18. {voidaccess-1.9.1 → voidaccess-1.9.2}/analysis/opsec.py +0 -0
  19. {voidaccess-1.9.1 → voidaccess-1.9.2}/analysis/patterns.py +0 -0
  20. {voidaccess-1.9.1 → voidaccess-1.9.2}/analysis/temporal.py +0 -0
  21. {voidaccess-1.9.1 → voidaccess-1.9.2}/api/__init__.py +0 -0
  22. {voidaccess-1.9.1 → voidaccess-1.9.2}/api/auth.py +0 -0
  23. {voidaccess-1.9.1 → voidaccess-1.9.2}/api/errors.py +0 -0
  24. {voidaccess-1.9.1 → voidaccess-1.9.2}/api/main.py +0 -0
  25. {voidaccess-1.9.1 → voidaccess-1.9.2}/api/routes/__init__.py +0 -0
  26. {voidaccess-1.9.1 → voidaccess-1.9.2}/api/routes/actors.py +0 -0
  27. {voidaccess-1.9.1 → voidaccess-1.9.2}/api/routes/admin.py +0 -0
  28. {voidaccess-1.9.1 → voidaccess-1.9.2}/api/routes/auth.py +0 -0
  29. {voidaccess-1.9.1 → voidaccess-1.9.2}/api/routes/entities.py +0 -0
  30. {voidaccess-1.9.1 → voidaccess-1.9.2}/api/routes/export.py +0 -0
  31. {voidaccess-1.9.1 → voidaccess-1.9.2}/api/routes/investigations.py +0 -0
  32. {voidaccess-1.9.1 → voidaccess-1.9.2}/api/routes/monitors.py +0 -0
  33. {voidaccess-1.9.1 → voidaccess-1.9.2}/api/routes/search.py +0 -0
  34. {voidaccess-1.9.1 → voidaccess-1.9.2}/api/routes/settings.py +0 -0
  35. {voidaccess-1.9.1 → voidaccess-1.9.2}/auth/__init__.py +0 -0
  36. {voidaccess-1.9.1 → voidaccess-1.9.2}/auth/token_blacklist.py +0 -0
  37. {voidaccess-1.9.1 → voidaccess-1.9.2}/config.py +0 -0
  38. {voidaccess-1.9.1 → voidaccess-1.9.2}/crawler/__init__.py +0 -0
  39. {voidaccess-1.9.1 → voidaccess-1.9.2}/crawler/dedup.py +0 -0
  40. {voidaccess-1.9.1 → voidaccess-1.9.2}/crawler/frontier.py +0 -0
  41. {voidaccess-1.9.1 → voidaccess-1.9.2}/crawler/spider.py +0 -0
  42. {voidaccess-1.9.1 → voidaccess-1.9.2}/crawler/utils.py +0 -0
  43. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/__init__.py +0 -0
  44. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/__init__.py +0 -0
  45. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/env.py +0 -0
  46. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0001_initial_schema.py +0 -0
  47. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0002_add_investigation_status_column.py +0 -0
  48. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0002_add_missing_tables.py +0 -0
  49. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0003_add_canonical_value_and_entity_links.py +0 -0
  50. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0004_add_page_posted_at.py +0 -0
  51. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0005_add_extraction_method.py +0 -0
  52. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0006_add_monitor_alerts.py +0 -0
  53. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0007_add_actor_style_profiles.py +0 -0
  54. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0008_add_users_table.py +0 -0
  55. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0009_add_investigation_id_to_relationships.py +0 -0
  56. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0010_add_composite_index_entity_relationships.py +0 -0
  57. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0011_add_page_extraction_cache.py +0 -0
  58. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0013_add_graph_status.py +0 -0
  59. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0015_add_progress_fields.py +0 -0
  60. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0016_backfill_graph_status.py +0 -0
  61. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0017_add_user_api_keys.py +0 -0
  62. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0018_add_user_id_to_investigations.py +0 -0
  63. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0019_add_content_safety_log.py +0 -0
  64. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0020_add_entity_source_tracking.py +0 -0
  65. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0021_add_search_engine_stats.py +0 -0
  66. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0022_add_actor_profiles.py +0 -0
  67. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0023_add_investigation_metadata.py +0 -0
  68. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0024_add_investigation_step_metrics.py +0 -0
  69. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/migrations/versions/0025_add_cancellation_requested.py +0 -0
  70. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/models.py +0 -0
  71. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/queries.py +0 -0
  72. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/search_engine_stats.py +0 -0
  73. {voidaccess-1.9.1 → voidaccess-1.9.2}/db/session.py +0 -0
  74. {voidaccess-1.9.1 → voidaccess-1.9.2}/export/__init__.py +0 -0
  75. {voidaccess-1.9.1 → voidaccess-1.9.2}/export/_entity_loading.py +0 -0
  76. {voidaccess-1.9.1 → voidaccess-1.9.2}/export/sigma.py +0 -0
  77. {voidaccess-1.9.1 → voidaccess-1.9.2}/export/snort_export.py +0 -0
  78. {voidaccess-1.9.1 → voidaccess-1.9.2}/export/yara_export.py +0 -0
  79. {voidaccess-1.9.1 → voidaccess-1.9.2}/extractor/__init__.py +0 -0
  80. {voidaccess-1.9.1 → voidaccess-1.9.2}/extractor/confidence.py +0 -0
  81. {voidaccess-1.9.1 → voidaccess-1.9.2}/extractor/entity_shape.py +0 -0
  82. {voidaccess-1.9.1 → voidaccess-1.9.2}/extractor/gazetteer.py +0 -0
  83. {voidaccess-1.9.1 → voidaccess-1.9.2}/extractor/identity.py +0 -0
  84. {voidaccess-1.9.1 → voidaccess-1.9.2}/extractor/llm_extract.py +0 -0
  85. {voidaccess-1.9.1 → voidaccess-1.9.2}/extractor/ner.py +0 -0
  86. {voidaccess-1.9.1 → voidaccess-1.9.2}/extractor/normalizer.py +0 -0
  87. {voidaccess-1.9.1 → voidaccess-1.9.2}/extractor/pipeline.py +0 -0
  88. {voidaccess-1.9.1 → voidaccess-1.9.2}/extractor/regex_patterns.py +0 -0
  89. {voidaccess-1.9.1 → voidaccess-1.9.2}/extractor/relationship_extract.py +0 -0
  90. {voidaccess-1.9.1 → voidaccess-1.9.2}/fingerprint/__init__.py +0 -0
  91. {voidaccess-1.9.1 → voidaccess-1.9.2}/fingerprint/calibration.py +0 -0
  92. {voidaccess-1.9.1 → voidaccess-1.9.2}/fingerprint/profiler.py +0 -0
  93. {voidaccess-1.9.1 → voidaccess-1.9.2}/fingerprint/stylometry.py +0 -0
  94. {voidaccess-1.9.1 → voidaccess-1.9.2}/graph/__init__.py +0 -0
  95. {voidaccess-1.9.1 → voidaccess-1.9.2}/graph/export.py +0 -0
  96. {voidaccess-1.9.1 → voidaccess-1.9.2}/graph/model.py +0 -0
  97. {voidaccess-1.9.1 → voidaccess-1.9.2}/graph/queries.py +0 -0
  98. {voidaccess-1.9.1 → voidaccess-1.9.2}/graph/visualize.py +0 -0
  99. {voidaccess-1.9.1 → voidaccess-1.9.2}/i18n/__init__.py +0 -0
  100. {voidaccess-1.9.1 → voidaccess-1.9.2}/i18n/detect.py +0 -0
  101. {voidaccess-1.9.1 → voidaccess-1.9.2}/i18n/query_expand.py +0 -0
  102. {voidaccess-1.9.1 → voidaccess-1.9.2}/i18n/translate.py +0 -0
  103. {voidaccess-1.9.1 → voidaccess-1.9.2}/monitor/__init__.py +0 -0
  104. {voidaccess-1.9.1 → voidaccess-1.9.2}/monitor/_db.py +0 -0
  105. {voidaccess-1.9.1 → voidaccess-1.9.2}/monitor/alerts.py +0 -0
  106. {voidaccess-1.9.1 → voidaccess-1.9.2}/monitor/config.py +0 -0
  107. {voidaccess-1.9.1 → voidaccess-1.9.2}/monitor/diff.py +0 -0
  108. {voidaccess-1.9.1 → voidaccess-1.9.2}/monitor/jobs.py +0 -0
  109. {voidaccess-1.9.1 → voidaccess-1.9.2}/monitor/scheduler.py +0 -0
  110. {voidaccess-1.9.1 → voidaccess-1.9.2}/scraper/__init__.py +0 -0
  111. {voidaccess-1.9.1 → voidaccess-1.9.2}/scraper/scrape.py +0 -0
  112. {voidaccess-1.9.1 → voidaccess-1.9.2}/scraper/scrape_js.py +0 -0
  113. {voidaccess-1.9.1 → voidaccess-1.9.2}/search/__init__.py +0 -0
  114. {voidaccess-1.9.1 → voidaccess-1.9.2}/search/circuit_breaker.py +0 -0
  115. {voidaccess-1.9.1 → voidaccess-1.9.2}/search/engine_catalog.py +0 -0
  116. {voidaccess-1.9.1 → voidaccess-1.9.2}/search/query_builder.py +0 -0
  117. {voidaccess-1.9.1 → voidaccess-1.9.2}/search/search.py +0 -0
  118. {voidaccess-1.9.1 → voidaccess-1.9.2}/setup.cfg +0 -0
  119. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/__init__.py +0 -0
  120. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/actor_profiles.py +0 -0
  121. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/blockchain.py +0 -0
  122. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/breach_lookup.py +0 -0
  123. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/cache.py +0 -0
  124. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/cisa.py +0 -0
  125. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/data/__init__.py +0 -0
  126. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/data/onion_seeds.json +0 -0
  127. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/dns_enrichment.py +0 -0
  128. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/domain_reputation.py +0 -0
  129. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/email_reputation.py +0 -0
  130. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/engines.py +0 -0
  131. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/enrichment.py +0 -0
  132. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/github_scraper.py +0 -0
  133. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/gitlab_scraper.py +0 -0
  134. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/hash_reputation.py +0 -0
  135. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/historical_intel.py +0 -0
  136. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/infostealer.py +0 -0
  137. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/ip_reputation.py +0 -0
  138. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/nvd.py +0 -0
  139. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/paste_scraper.py +0 -0
  140. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/pastes.py +0 -0
  141. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/proxy_client.py +0 -0
  142. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/rss_scraper.py +0 -0
  143. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/seed_manager.py +0 -0
  144. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/seeds.py +0 -0
  145. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/shodan.py +0 -0
  146. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/telegram.py +0 -0
  147. {voidaccess-1.9.1 → voidaccess-1.9.2}/sources/virustotal.py +0 -0
  148. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_analysis_opsec.py +0 -0
  149. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_analysis_stylometry.py +0 -0
  150. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_analysis_temporal.py +0 -0
  151. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_api.py +0 -0
  152. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_api_monitors.py +0 -0
  153. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_blockchain.py +0 -0
  154. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_breach_lookup.py +0 -0
  155. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_cli_proxy_config.py +0 -0
  156. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_confidence_calibration.py +0 -0
  157. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_config.py +0 -0
  158. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_consolidated_contracts.py +0 -0
  159. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_crawler.py +0 -0
  160. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_db.py +0 -0
  161. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_dns_enrichment.py +0 -0
  162. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_domain_reputation.py +0 -0
  163. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_email_reputation.py +0 -0
  164. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_extractor_llm_pipeline.py +0 -0
  165. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_filter_e2e_manual.py +0 -0
  166. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_filter_parser_manual.py +0 -0
  167. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_fingerprint.py +0 -0
  168. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_github_scraper.py +0 -0
  169. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_gitlab_scraper.py +0 -0
  170. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_graph.py +0 -0
  171. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_graph_integration.py +0 -0
  172. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_hash_reputation.py +0 -0
  173. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_i18n.py +0 -0
  174. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_infostealer.py +0 -0
  175. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_ip_reputation.py +0 -0
  176. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_llm.py +0 -0
  177. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_llm_utils.py +0 -0
  178. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_model_singleton.py +0 -0
  179. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_monitor.py +0 -0
  180. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_monitor_integration.py +0 -0
  181. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_nvd.py +0 -0
  182. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_pagination.py +0 -0
  183. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_paste_scraper.py +0 -0
  184. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_proxy_client.py +0 -0
  185. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_ransomlook.py +0 -0
  186. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_regex_patterns.py +0 -0
  187. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_relationship_extract.py +0 -0
  188. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_rss_scraper.py +0 -0
  189. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_scrape_js.py +0 -0
  190. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_settings.py +0 -0
  191. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_snort_export.py +0 -0
  192. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_sources.py +0 -0
  193. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_sources_enrichment_new.py +0 -0
  194. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_stix_export.py +0 -0
  195. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_vector.py +0 -0
  196. {voidaccess-1.9.1 → voidaccess-1.9.2}/tests/test_yara_export.py +0 -0
  197. {voidaccess-1.9.1 → voidaccess-1.9.2}/utils/__init__.py +0 -0
  198. {voidaccess-1.9.1 → voidaccess-1.9.2}/utils/async_utils.py +0 -0
  199. {voidaccess-1.9.1 → voidaccess-1.9.2}/utils/content_dedup.py +0 -0
  200. {voidaccess-1.9.1 → voidaccess-1.9.2}/utils/content_safety.py +0 -0
  201. {voidaccess-1.9.1 → voidaccess-1.9.2}/utils/defang.py +0 -0
  202. {voidaccess-1.9.1 → voidaccess-1.9.2}/utils/encryption.py +0 -0
  203. {voidaccess-1.9.1 → voidaccess-1.9.2}/utils/enrichment_cache.py +0 -0
  204. {voidaccess-1.9.1 → voidaccess-1.9.2}/utils/investigation_metrics.py +0 -0
  205. {voidaccess-1.9.1 → voidaccess-1.9.2}/utils/ioc_freshness.py +0 -0
  206. {voidaccess-1.9.1 → voidaccess-1.9.2}/utils/user_keys.py +0 -0
  207. {voidaccess-1.9.1 → voidaccess-1.9.2}/vector/__init__.py +0 -0
  208. {voidaccess-1.9.1 → voidaccess-1.9.2}/vector/embedder.py +0 -0
  209. {voidaccess-1.9.1 → voidaccess-1.9.2}/vector/search.py +0 -0
  210. {voidaccess-1.9.1 → voidaccess-1.9.2}/vector/store.py +0 -0
  211. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess/__init__.py +0 -0
  212. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess/config.py +0 -0
  213. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess/llm.py +0 -0
  214. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess/llm_utils.py +0 -0
  215. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess.egg-info/dependency_links.txt +0 -0
  216. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess.egg-info/entry_points.txt +0 -0
  217. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess.egg-info/requires.txt +0 -0
  218. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess.egg-info/top_level.txt +0 -0
  219. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess_cli/adapters/__init__.py +0 -0
  220. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess_cli/adapters/sqlite.py +0 -0
  221. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess_cli/browser.py +0 -0
  222. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess_cli/commands/__init__.py +0 -0
  223. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess_cli/commands/actors.py +0 -0
  224. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess_cli/commands/enrich.py +0 -0
  225. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess_cli/commands/investigate.py +0 -0
  226. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess_cli/commands/show.py +0 -0
  227. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess_cli/display.py +0 -0
  228. {voidaccess-1.9.1 → voidaccess-1.9.2}/voidaccess_cli/main.py +0 -0
  229. {voidaccess-1.9.1 → voidaccess-1.9.2}/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.1
3
+ Version: 1.9.2
4
4
  Summary: Dark web OSINT CLI — automated threat intelligence from query to report
5
5
  Author: VoidAccess
6
6
  License-Expression: MIT
@@ -172,6 +172,17 @@ Full pipeline behavior, timeouts, recovery, and data flow are documented in [Arc
172
172
 
173
173
  Sources that need API keys skip cleanly when their keys are absent. The complete key and configuration reference is in [Architecture](docs/architecture.md#13-configuration-reference).
174
174
 
175
+ ### Optional vector embeddings
176
+
177
+ The default installation can run without PyTorch. When the embedding stack is
178
+ unavailable, VoidAccess logs that it is using a deterministic SHA-256 fallback
179
+ encoder. Install the optional NLP dependencies to enable full
180
+ sentence-transformer vector embeddings:
181
+
182
+ ```bash
183
+ pip install "voidaccess[nlp]"
184
+ ```
185
+
175
186
  ## LLM Providers
176
187
 
177
188
  | Provider | Typical models | Notes |
@@ -87,6 +87,17 @@ Full pipeline behavior, timeouts, recovery, and data flow are documented in [Arc
87
87
 
88
88
  Sources that need API keys skip cleanly when their keys are absent. The complete key and configuration reference is in [Architecture](docs/architecture.md#13-configuration-reference).
89
89
 
90
+ ### Optional vector embeddings
91
+
92
+ The default installation can run without PyTorch. When the embedding stack is
93
+ unavailable, VoidAccess logs that it is using a deterministic SHA-256 fallback
94
+ encoder. Install the optional NLP dependencies to enable full
95
+ sentence-transformer vector embeddings:
96
+
97
+ ```bash
98
+ pip install "voidaccess[nlp]"
99
+ ```
100
+
90
101
  ## LLM Providers
91
102
 
92
103
  | Provider | Typical models | Notes |
@@ -57,7 +57,7 @@ logger = logging.getLogger(__name__)
57
57
  # ---------------------------------------------------------------------------
58
58
 
59
59
  PACKAGE_FORMAT = "voidaccess-ioc-v1"
60
- PACKAGE_VERSION = "1.9.1"
60
+ PACKAGE_VERSION = "1.9.2"
61
61
  SNORT_SID_BASE = 9000001 # 9xxxxxx reserved for VoidAccess-generated rules
62
62
 
63
63
  # Map extractor upper-case entity types to the per-file bucket they belong to.
@@ -16,6 +16,7 @@ from __future__ import annotations
16
16
  import json
17
17
  import logging
18
18
  import os
19
+ import uuid
19
20
  from datetime import datetime, timezone
20
21
  from typing import Any, Optional
21
22
 
@@ -76,6 +77,11 @@ _MISP_ATTR_MAP: dict[str, dict] = {
76
77
  "category": "Attribution",
77
78
  "to_ids": False,
78
79
  },
80
+ "ORGANIZATION_NAME": {
81
+ "type": "target-org",
82
+ "category": "Targeting data",
83
+ "to_ids": False,
84
+ },
79
85
  }
80
86
 
81
87
 
@@ -94,7 +100,7 @@ def investigation_to_misp_event(
94
100
  Returns a valid (but empty-attribute) event if the investigation is not found.
95
101
  Never raises.
96
102
  """
97
- investigation, entities = _load_investigation_and_entities(
103
+ investigation, entity_rows = _load_investigation_and_entity_rows(
98
104
  investigation_id, entity_ids=entity_ids
99
105
  )
100
106
 
@@ -110,7 +116,14 @@ def investigation_to_misp_event(
110
116
  query = getattr(investigation, "query", "") or ""
111
117
 
112
118
  attributes: list[dict] = []
113
- for entity in entities:
119
+ graph_endpoint_map: dict[str, str] = {}
120
+ objects: list[dict] = []
121
+
122
+ from extractor.identity import entity_graph_id # noqa: PLC0415
123
+ from export._entity_loading import normalized_entity_from_db_row # noqa: PLC0415
124
+
125
+ for row in entity_rows:
126
+ entity = normalized_entity_from_db_row(row)
114
127
  mapping = _MISP_ATTR_MAP.get(entity.entity_type)
115
128
  if mapping is None:
116
129
  continue
@@ -123,6 +136,40 @@ def investigation_to_misp_event(
123
136
  }
124
137
  attributes.append(attr)
125
138
 
139
+ object_uuid = str(row.id)
140
+ graph_endpoint_map[entity_graph_id(entity)] = object_uuid
141
+ objects.append(
142
+ {
143
+ "name": "voidaccess-entity",
144
+ "meta-category": "threat-intelligence",
145
+ "uuid": object_uuid,
146
+ "Attribute": [dict(attr, uuid=str(uuid.uuid4()))],
147
+ "ObjectReference": [],
148
+ }
149
+ )
150
+
151
+ # Use the same graph relationship enumeration as STIX. This includes
152
+ # typed persisted edges and applies the same endpoint/deduplication rules.
153
+ from export.relationships import load_export_relationships # noqa: PLC0415
154
+ graph_relationships, relationship_warning = load_export_relationships(
155
+ investigation_id, graph_endpoint_map
156
+ )
157
+ if relationship_warning:
158
+ logger.warning("MISP relationships were not included: %s", relationship_warning)
159
+
160
+ object_by_uuid = {obj["uuid"]: obj for obj in objects}
161
+ for relationship in graph_relationships:
162
+ source_object = object_by_uuid.get(relationship["source_ref"])
163
+ if source_object is None or relationship["target_ref"] not in object_by_uuid:
164
+ continue
165
+ source_object["ObjectReference"].append(
166
+ {
167
+ "object_uuid": relationship["source_ref"],
168
+ "referenced_uuid": relationship["target_ref"],
169
+ "relationship_type": relationship["edge_type"],
170
+ }
171
+ )
172
+
126
173
  return {
127
174
  "Event": {
128
175
  "info": f"VoidAccess Investigation: {query}",
@@ -131,6 +178,7 @@ def investigation_to_misp_event(
131
178
  "analysis": "2", # Completed
132
179
  "distribution": "0", # Your organisation only
133
180
  "Attribute": attributes,
181
+ "Object": objects,
134
182
  }
135
183
  }
136
184
 
@@ -163,8 +211,21 @@ def _load_investigation_and_entities(
163
211
 
164
212
  Returns (investigation, entities) or (None, []) on error / not found.
165
213
  """
166
- import uuid as _uuid
214
+ investigation, rows = _load_investigation_and_entity_rows(
215
+ investigation_id, entity_ids=entity_ids
216
+ )
217
+ if investigation is None:
218
+ return None, []
167
219
 
220
+ from export._entity_loading import normalized_entity_from_db_row # noqa: PLC0415
221
+ return investigation, [normalized_entity_from_db_row(row) for row in rows]
222
+
223
+
224
+ def _load_investigation_and_entity_rows(
225
+ investigation_id: Any,
226
+ entity_ids: Optional[list[str]] = None,
227
+ ):
228
+ """Load an investigation and its ORM entity rows for export rendering."""
168
229
  if not os.getenv("DATABASE_URL"):
169
230
  return None, []
170
231
 
@@ -172,7 +233,7 @@ def _load_investigation_and_entities(
172
233
  from db.session import get_session # noqa: PLC0415
173
234
  from db.queries import get_investigation_by_id_or_run # noqa: PLC0415
174
235
  from db.models import Entity, InvestigationEntityLink # noqa: PLC0415
175
- from extractor.normalizer import NormalizedEntity # noqa: PLC0415
236
+ from sqlalchemy.orm import joinedload # noqa: PLC0415
176
237
 
177
238
  inv_uuid = _coerce_uuid(investigation_id)
178
239
  if inv_uuid is None:
@@ -192,15 +253,16 @@ def _load_investigation_and_entities(
192
253
  if investigation is None:
193
254
  return None, []
194
255
 
195
- linked_ids_select = (
196
- session.query(InvestigationEntityLink.entity_id)
197
- .filter(InvestigationEntityLink.investigation_id == investigation.id)
198
- )
199
256
  db_entities = (
200
257
  session.query(Entity)
258
+ .outerjoin(
259
+ InvestigationEntityLink,
260
+ InvestigationEntityLink.entity_id == Entity.id,
261
+ )
262
+ .options(joinedload(Entity.page))
201
263
  .filter(
202
264
  (Entity.investigation_id == investigation.id)
203
- | Entity.id.in_(linked_ids_select)
265
+ | (InvestigationEntityLink.investigation_id == investigation.id)
204
266
  )
205
267
  .all()
206
268
  )
@@ -209,17 +271,8 @@ def _load_investigation_and_entities(
209
271
  want = frozenset(filter_uuids)
210
272
  db_entities = [e for e in db_entities if e.id in want]
211
273
 
212
- from export._entity_loading import normalized_entity_from_db_row # noqa: PLC0415
213
-
214
- # Shared row→NormalizedEntity mapping (same one STIX/Sigma use), so
215
- # auxiliary fields (source_quality, extraction_method) no longer
216
- # differ between export paths.
217
- normalized: list[NormalizedEntity] = [
218
- normalized_entity_from_db_row(e) for e in db_entities
219
- ]
220
-
221
274
  session.expunge_all()
222
- return investigation, normalized
275
+ return investigation, db_entities
223
276
 
224
277
  except Exception as exc:
225
278
  logger.warning("_load_investigation_and_entities failed: %s", exc)
@@ -0,0 +1,81 @@
1
+ """Shared relationship loading for structured exporters.
2
+
3
+ The graph builder is the canonical source for both persisted typed edges and
4
+ the bounded co-occurrence edges used by exports. Keeping the enumeration in
5
+ one place prevents one format from silently losing relationships that another
6
+ format preserves.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from typing import Any, Optional
13
+ import uuid
14
+
15
+ from graph.model import RELATIONSHIP_TYPE_STIX
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ def load_export_relationships(
21
+ investigation_id: Any,
22
+ endpoint_map: dict[str, str],
23
+ ) -> tuple[list[dict[str, Any]], Optional[str]]:
24
+ """Return graph relationships whose endpoints exist in ``endpoint_map``.
25
+
26
+ Each result contains the original VoidAccess ``edge_type`` plus its STIX
27
+ spelling. Consumers can therefore share the exact same deduplication and
28
+ endpoint selection while rendering format-specific relationship records.
29
+ """
30
+ try:
31
+ from graph.builder import build_graph_from_db # noqa: PLC0415
32
+
33
+ inv_uuid = _coerce_uuid(investigation_id)
34
+ graph = build_graph_from_db(investigation_id=inv_uuid)
35
+ relationships: list[dict[str, Any]] = []
36
+ seen: set[tuple[str, str, str]] = set()
37
+
38
+ for source_node, target_node, data in graph.edges(data=True):
39
+ source_ref = endpoint_map.get(source_node)
40
+ target_ref = endpoint_map.get(target_node)
41
+ if not source_ref or not target_ref:
42
+ continue
43
+
44
+ edge_type = _normalise_edge_type(data.get("edge_type", "related-to"))
45
+ stix_type = RELATIONSHIP_TYPE_STIX.get(edge_type, "related-to")
46
+ key = (stix_type, source_ref, target_ref)
47
+ if key in seen:
48
+ continue
49
+ seen.add(key)
50
+ relationships.append(
51
+ {
52
+ "source_ref": source_ref,
53
+ "target_ref": target_ref,
54
+ "edge_type": edge_type,
55
+ "stix_relationship_type": stix_type,
56
+ "confidence": data.get("confidence"),
57
+ }
58
+ )
59
+ return relationships, None
60
+ except Exception as exc:
61
+ logger.warning("load_export_relationships failed: %s", exc)
62
+ return [], f"load_export_relationships failed: {exc}"
63
+
64
+
65
+ def _normalise_edge_type(edge_type: Any) -> str:
66
+ """Normalise enum/string edge types without changing their meaning."""
67
+ value = getattr(edge_type, "value", edge_type)
68
+ text = str(value or "related-to").strip()
69
+ upper = text.upper()
70
+ if upper in RELATIONSHIP_TYPE_STIX:
71
+ return upper
72
+ return text
73
+
74
+
75
+ def _coerce_uuid(value: Any) -> Optional[uuid.UUID]:
76
+ if isinstance(value, uuid.UUID):
77
+ return value
78
+ try:
79
+ return uuid.UUID(str(value))
80
+ except (ValueError, AttributeError):
81
+ return None
@@ -22,7 +22,6 @@ import warnings
22
22
  from typing import Any, Optional
23
23
  import uuid
24
24
 
25
- from graph.model import RELATIONSHIP_TYPE_STIX
26
25
  from extractor.confidence import get_entity_confidence
27
26
 
28
27
  logger = logging.getLogger(__name__)
@@ -161,6 +160,11 @@ def entity_to_stix_malware(entity: Any) -> Optional[Any]:
161
160
  malware = stix2.Malware(
162
161
  name=entity.value,
163
162
  is_family=True,
163
+ malware_types=(
164
+ ["ransomware"]
165
+ if entity.entity_type == "RANSOMWARE_GROUP"
166
+ else ["malware"]
167
+ ),
164
168
  confidence=_to_stix_confidence(get_entity_confidence(entity)),
165
169
  allow_custom=True,
166
170
  x_voidaccess_source_quality=getattr(entity, "source_quality", 1.0),
@@ -176,6 +180,28 @@ def entity_to_stix_malware(entity: Any) -> Optional[Any]:
176
180
  return None
177
181
 
178
182
 
183
+ def entity_to_stix_identity(entity: Any) -> Optional[Any]:
184
+ """Convert an organization entity to a STIX Identity object."""
185
+ if not _STIX2_AVAILABLE or entity.entity_type != "ORGANIZATION_NAME":
186
+ return None
187
+ try:
188
+ return stix2.Identity(
189
+ name=entity.value,
190
+ identity_class="organization",
191
+ confidence=_to_stix_confidence(get_entity_confidence(entity)),
192
+ allow_custom=True,
193
+ x_voidaccess_source_quality=getattr(entity, "source_quality", 1.0),
194
+ external_references=(
195
+ [{"source_name": "voidaccess", "url": entity.source_url}]
196
+ if entity.source_url
197
+ else []
198
+ ),
199
+ )
200
+ except Exception as exc:
201
+ logger.warning("entity_to_stix_identity failed for %r: %s", entity.value, exc)
202
+ return None
203
+
204
+
179
205
  def entity_to_stix_threat_actor(entity: Any) -> Optional[Any]:
180
206
  """
181
207
  Convert a THREAT_ACTOR_HANDLE entity to a STIX 2.1 ThreatActor object.
@@ -287,6 +313,11 @@ def investigation_to_stix_bundle(
287
313
  stix_objects.append(actor)
288
314
  _register(entity, actor.id)
289
315
 
316
+ identity = entity_to_stix_identity(entity)
317
+ if identity:
318
+ stix_objects.append(identity)
319
+ _register(entity, identity.id)
320
+
290
321
  if include_relationships and stix_objects:
291
322
  relationships, relationship_warning = _build_stix_relationships(
292
323
  investigation_id, stix_id_map
@@ -436,32 +467,22 @@ def _build_stix_relationships(
436
467
  if not _STIX2_AVAILABLE:
437
468
  return [], "stix2 is not installed"
438
469
  try:
439
- from graph.builder import build_graph_from_db # noqa: PLC0415
440
-
441
- inv_uuid = _coerce_uuid(investigation_id)
442
- graph = build_graph_from_db(investigation_id=inv_uuid)
470
+ from export.relationships import load_export_relationships # noqa: PLC0415
443
471
 
472
+ graph_relationships, warning = load_export_relationships(
473
+ investigation_id, stix_id_map
474
+ )
475
+ if warning:
476
+ return [], warning
444
477
  relationships: list[Any] = []
445
- seen_relationships: set[tuple[str, str, str]] = set()
446
- for source_node, target_node, data in graph.edges(data=True):
447
- src_stix_id = stix_id_map.get(source_node)
448
- tgt_stix_id = stix_id_map.get(target_node)
449
- if not src_stix_id or not tgt_stix_id:
450
- continue
451
- edge_type = data.get("edge_type", "related-to")
452
- # Map VoidAccess edge types to STIX relationship types
453
- rel_type = _edge_type_to_stix(edge_type)
454
- relationship_key = (rel_type, src_stix_id, tgt_stix_id)
455
- if relationship_key in seen_relationships:
456
- continue
457
- seen_relationships.add(relationship_key)
478
+ for data in graph_relationships:
458
479
  # Carry the edge's own confidence onto the SRO. Typed relationships
459
480
  # have a claim-specific confidence distinct from co-occurrence's
460
481
  # flat value; STIX 2.1 confidence is an int 0-100.
461
482
  rel_kwargs: dict[str, Any] = {
462
- "relationship_type": rel_type,
463
- "source_ref": src_stix_id,
464
- "target_ref": tgt_stix_id,
483
+ "relationship_type": data["stix_relationship_type"],
484
+ "source_ref": data["source_ref"],
485
+ "target_ref": data["target_ref"],
465
486
  "allow_custom": True,
466
487
  }
467
488
  try:
@@ -488,7 +509,9 @@ def _build_stix_relationships(
488
509
 
489
510
  def _edge_type_to_stix(edge_type: str) -> str:
490
511
  """Map VoidAccess graph edge types to STIX relationship type strings."""
491
- return RELATIONSHIP_TYPE_STIX.get(edge_type, "related-to")
512
+ from export.relationships import _normalise_edge_type # noqa: PLC0415
513
+ from graph.model import RELATIONSHIP_TYPE_STIX # noqa: PLC0415
514
+ return RELATIONSHIP_TYPE_STIX.get(_normalise_edge_type(edge_type), "related-to")
492
515
 
493
516
 
494
517
  def _coerce_uuid(value: Any) -> Optional[uuid.UUID]:
@@ -789,8 +789,12 @@ def build_graph_from_db(
789
789
  .join(
790
790
  InvestigationEntityLink,
791
791
  InvestigationEntityLink.entity_id == Entity.id,
792
+ isouter=True,
793
+ )
794
+ .filter(
795
+ (Entity.investigation_id == investigation_id)
796
+ | (InvestigationEntityLink.investigation_id == investigation_id)
792
797
  )
793
- .filter(InvestigationEntityLink.investigation_id == investigation_id)
794
798
  .options(joinedload(Entity.page))
795
799
  )
796
800
  else:
@@ -1230,8 +1234,12 @@ def build_graph_from_db_cached(investigation_id: uuid.UUID) -> nx.MultiDiGraph:
1230
1234
  .join(
1231
1235
  InvestigationEntityLink,
1232
1236
  InvestigationEntityLink.entity_id == Entity.id,
1237
+ isouter=True,
1238
+ )
1239
+ .filter(
1240
+ (Entity.investigation_id == investigation_id)
1241
+ | (InvestigationEntityLink.investigation_id == investigation_id)
1233
1242
  )
1234
- .filter(InvestigationEntityLink.investigation_id == investigation_id)
1235
1243
  .options(joinedload(Entity.page))
1236
1244
  .yield_per(500)
1237
1245
  )
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "voidaccess"
7
- version = "1.9.1"
7
+ version = "1.9.2"
8
8
  description = "Dark web OSINT CLI — automated threat intelligence from query to report"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -82,6 +82,11 @@ def get_embedding_model() -> "SentenceTransformer | None":
82
82
  logger.info("Loaded embedding model all-MiniLM-L6-v2 (singleton)")
83
83
  except Exception as exc:
84
84
  logger.warning("Failed to load embedding model: %s", exc)
85
+ logger.warning(
86
+ "Deterministic embedding fallback is active; install "
87
+ "voidaccess[nlp] (torch + sentence-transformers) for full "
88
+ "vector embeddings."
89
+ )
85
90
  _model = _FallbackEmbeddingModel()
86
91
 
87
92
  return _model
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voidaccess
3
- Version: 1.9.1
3
+ Version: 1.9.2
4
4
  Summary: Dark web OSINT CLI — automated threat intelligence from query to report
5
5
  Author: VoidAccess
6
6
  License-Expression: MIT
@@ -172,6 +172,17 @@ Full pipeline behavior, timeouts, recovery, and data flow are documented in [Arc
172
172
 
173
173
  Sources that need API keys skip cleanly when their keys are absent. The complete key and configuration reference is in [Architecture](docs/architecture.md#13-configuration-reference).
174
174
 
175
+ ### Optional vector embeddings
176
+
177
+ The default installation can run without PyTorch. When the embedding stack is
178
+ unavailable, VoidAccess logs that it is using a deterministic SHA-256 fallback
179
+ encoder. Install the optional NLP dependencies to enable full
180
+ sentence-transformer vector embeddings:
181
+
182
+ ```bash
183
+ pip install "voidaccess[nlp]"
184
+ ```
185
+
175
186
  ## LLM Providers
176
187
 
177
188
  | Provider | Typical models | Notes |
@@ -62,6 +62,7 @@ export/__init__.py
62
62
  export/_entity_loading.py
63
63
  export/ioc_package.py
64
64
  export/misp.py
65
+ export/relationships.py
65
66
  export/sigma.py
66
67
  export/snort_export.py
67
68
  export/stix.py
@@ -1,3 +1,3 @@
1
1
  """voidaccess CLI — dark-web OSINT command-line interface."""
2
2
 
3
- __version__ = "1.9.1"
3
+ __version__ = "1.9.2"
@@ -418,7 +418,7 @@ def configure_proxy(
418
418
  api_label = "[green]enabled[/green]" if api_transport else "[red]disabled[/red]"
419
419
  proxy_label = "[green]enabled[/green]" if proxy_transport else "[red]disabled[/red]"
420
420
  console.print(f" REST API transport : {api_label}")
421
- console.print(f" Residential proxy : {proxy_label}")
421
+ console.print(f" Proxy transport : {proxy_label}")
422
422
  console.print(" Scope : paste + RSS only, never Tor")
423
423
  if proxy_transport and (not proxy_username or not proxy_password):
424
424
  console.print(
@@ -457,7 +457,7 @@ def configure_proxy(
457
457
  console.print(f"ScrapingAnt proxy transport [green]{state}[/green]")
458
458
  if enable_proxy and not cfg.get("enrichment_keys", {}).get("SCRAPINGANT_API_KEY"):
459
459
  console.print(
460
- "[yellow]Note:[/yellow] verify that the separate proxy username/password pair is configured — "
460
+ "[yellow]Note:[/yellow] no SCRAPINGANT_API_KEY configured yet — "
461
461
  "set one with `voidaccess configure proxy` (interactive) or "
462
462
  "`voidaccess configure keys`."
463
463
  )
@@ -467,4 +467,3 @@ def configure_proxy(
467
467
  _prompt_scrapingant(cfg)
468
468
  cli_config.save_config(cfg)
469
469
  console.print(f"[green]Saved to[/green] {cli_config.CONFIG_PATH}")
470
-
@@ -85,6 +85,13 @@ def run(
85
85
  target, suffix, fmt=fmt, query=(data.get("investigation") or {}).get("query")
86
86
  )
87
87
  out_path = Path(out_path).expanduser()
88
+ if out_path.exists():
89
+ safe_path = _collision_safe_output_path(out_path, fmt)
90
+ console.print(
91
+ f"[yellow]Warning:[/yellow] {out_path} already exists; "
92
+ f"writing {safe_path} to avoid overwriting it."
93
+ )
94
+ out_path = safe_path
88
95
  out_path.parent.mkdir(parents=True, exist_ok=True)
89
96
  if isinstance(payload, bytes):
90
97
  out_path.write_bytes(payload)
@@ -93,6 +100,16 @@ def run(
93
100
  console.print(f"[green]Wrote[/green] {out_path}")
94
101
 
95
102
 
103
+ def _collision_safe_output_path(path: Path, fmt: str) -> Path:
104
+ """Return a format-aware unused path without overwriting an export."""
105
+ candidate = path.with_name(f"{path.stem}-{fmt}{path.suffix}")
106
+ counter = 2
107
+ while candidate.exists():
108
+ candidate = path.with_name(f"{path.stem}-{fmt}-{counter}{path.suffix}")
109
+ counter += 1
110
+ return candidate
111
+
112
+
96
113
  def _load_target(target: str) -> tuple[Optional[str], Optional[dict]]:
97
114
  p = Path(target).expanduser()
98
115
  if p.exists() and p.suffix == ".json":