voidaccess 1.9.1__tar.gz → 1.9.3__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.3}/PKG-INFO +12 -1
  2. {voidaccess-1.9.1 → voidaccess-1.9.3}/README.md +11 -0
  3. {voidaccess-1.9.1 → voidaccess-1.9.3}/api/main.py +1 -1
  4. {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/investigations.py +1 -1
  5. {voidaccess-1.9.1 → voidaccess-1.9.3}/export/ioc_package.py +1 -1
  6. {voidaccess-1.9.1 → voidaccess-1.9.3}/export/misp.py +72 -19
  7. voidaccess-1.9.3/export/relationships.py +81 -0
  8. {voidaccess-1.9.1 → voidaccess-1.9.3}/export/stix.py +150 -22
  9. {voidaccess-1.9.1 → voidaccess-1.9.3}/graph/builder.py +10 -2
  10. {voidaccess-1.9.1 → voidaccess-1.9.3}/pyproject.toml +1 -1
  11. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/email_reputation.py +4 -2
  12. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_api.py +62 -48
  13. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_confidence_calibration.py +10 -8
  14. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_db.py +1 -0
  15. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_llm.py +10 -0
  16. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_llm_utils.py +1 -1
  17. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_regex_patterns.py +21 -21
  18. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_snort_export.py +28 -28
  19. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_stix_export.py +20 -0
  20. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_yara_export.py +17 -17
  21. {voidaccess-1.9.1 → voidaccess-1.9.3}/vector/model_singleton.py +5 -0
  22. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess/llm.py +15 -10
  23. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess.egg-info/PKG-INFO +12 -1
  24. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess.egg-info/SOURCES.txt +1 -0
  25. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/__init__.py +1 -1
  26. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/commands/configure.py +2 -3
  27. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/commands/export.py +17 -0
  28. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/config.py +67 -20
  29. {voidaccess-1.9.1 → voidaccess-1.9.3}/LICENSE +0 -0
  30. {voidaccess-1.9.1 → voidaccess-1.9.3}/analysis/__init__.py +0 -0
  31. {voidaccess-1.9.1 → voidaccess-1.9.3}/analysis/opsec.py +0 -0
  32. {voidaccess-1.9.1 → voidaccess-1.9.3}/analysis/patterns.py +0 -0
  33. {voidaccess-1.9.1 → voidaccess-1.9.3}/analysis/temporal.py +0 -0
  34. {voidaccess-1.9.1 → voidaccess-1.9.3}/api/__init__.py +0 -0
  35. {voidaccess-1.9.1 → voidaccess-1.9.3}/api/auth.py +0 -0
  36. {voidaccess-1.9.1 → voidaccess-1.9.3}/api/errors.py +0 -0
  37. {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/__init__.py +0 -0
  38. {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/actors.py +0 -0
  39. {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/admin.py +0 -0
  40. {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/auth.py +0 -0
  41. {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/entities.py +0 -0
  42. {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/export.py +2 -2
  43. {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/monitors.py +0 -0
  44. {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/search.py +0 -0
  45. {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/settings.py +0 -0
  46. {voidaccess-1.9.1 → voidaccess-1.9.3}/auth/__init__.py +0 -0
  47. {voidaccess-1.9.1 → voidaccess-1.9.3}/auth/token_blacklist.py +0 -0
  48. {voidaccess-1.9.1 → voidaccess-1.9.3}/config.py +0 -0
  49. {voidaccess-1.9.1 → voidaccess-1.9.3}/crawler/__init__.py +0 -0
  50. {voidaccess-1.9.1 → voidaccess-1.9.3}/crawler/dedup.py +0 -0
  51. {voidaccess-1.9.1 → voidaccess-1.9.3}/crawler/frontier.py +0 -0
  52. {voidaccess-1.9.1 → voidaccess-1.9.3}/crawler/spider.py +0 -0
  53. {voidaccess-1.9.1 → voidaccess-1.9.3}/crawler/utils.py +0 -0
  54. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/__init__.py +0 -0
  55. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/__init__.py +0 -0
  56. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/env.py +0 -0
  57. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0001_initial_schema.py +0 -0
  58. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0002_add_investigation_status_column.py +0 -0
  59. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0002_add_missing_tables.py +0 -0
  60. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0003_add_canonical_value_and_entity_links.py +0 -0
  61. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0004_add_page_posted_at.py +0 -0
  62. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0005_add_extraction_method.py +0 -0
  63. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0006_add_monitor_alerts.py +0 -0
  64. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0007_add_actor_style_profiles.py +0 -0
  65. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0008_add_users_table.py +0 -0
  66. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0009_add_investigation_id_to_relationships.py +0 -0
  67. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0010_add_composite_index_entity_relationships.py +0 -0
  68. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0011_add_page_extraction_cache.py +0 -0
  69. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0013_add_graph_status.py +0 -0
  70. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0015_add_progress_fields.py +0 -0
  71. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0016_backfill_graph_status.py +0 -0
  72. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0017_add_user_api_keys.py +0 -0
  73. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0018_add_user_id_to_investigations.py +0 -0
  74. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0019_add_content_safety_log.py +0 -0
  75. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0020_add_entity_source_tracking.py +0 -0
  76. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0021_add_search_engine_stats.py +0 -0
  77. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0022_add_actor_profiles.py +0 -0
  78. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0023_add_investigation_metadata.py +0 -0
  79. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0024_add_investigation_step_metrics.py +0 -0
  80. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0025_add_cancellation_requested.py +0 -0
  81. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/models.py +0 -0
  82. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/queries.py +0 -0
  83. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/search_engine_stats.py +0 -0
  84. {voidaccess-1.9.1 → voidaccess-1.9.3}/db/session.py +0 -0
  85. {voidaccess-1.9.1 → voidaccess-1.9.3}/export/__init__.py +0 -0
  86. {voidaccess-1.9.1 → voidaccess-1.9.3}/export/_entity_loading.py +0 -0
  87. {voidaccess-1.9.1 → voidaccess-1.9.3}/export/sigma.py +0 -0
  88. {voidaccess-1.9.1 → voidaccess-1.9.3}/export/snort_export.py +0 -0
  89. {voidaccess-1.9.1 → voidaccess-1.9.3}/export/yara_export.py +0 -0
  90. {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/__init__.py +0 -0
  91. {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/confidence.py +0 -0
  92. {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/entity_shape.py +0 -0
  93. {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/gazetteer.py +0 -0
  94. {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/identity.py +0 -0
  95. {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/llm_extract.py +0 -0
  96. {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/ner.py +0 -0
  97. {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/normalizer.py +0 -0
  98. {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/pipeline.py +0 -0
  99. {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/regex_patterns.py +0 -0
  100. {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/relationship_extract.py +0 -0
  101. {voidaccess-1.9.1 → voidaccess-1.9.3}/fingerprint/__init__.py +0 -0
  102. {voidaccess-1.9.1 → voidaccess-1.9.3}/fingerprint/calibration.py +0 -0
  103. {voidaccess-1.9.1 → voidaccess-1.9.3}/fingerprint/profiler.py +0 -0
  104. {voidaccess-1.9.1 → voidaccess-1.9.3}/fingerprint/stylometry.py +0 -0
  105. {voidaccess-1.9.1 → voidaccess-1.9.3}/graph/__init__.py +0 -0
  106. {voidaccess-1.9.1 → voidaccess-1.9.3}/graph/export.py +0 -0
  107. {voidaccess-1.9.1 → voidaccess-1.9.3}/graph/model.py +0 -0
  108. {voidaccess-1.9.1 → voidaccess-1.9.3}/graph/queries.py +0 -0
  109. {voidaccess-1.9.1 → voidaccess-1.9.3}/graph/visualize.py +0 -0
  110. {voidaccess-1.9.1 → voidaccess-1.9.3}/i18n/__init__.py +0 -0
  111. {voidaccess-1.9.1 → voidaccess-1.9.3}/i18n/detect.py +0 -0
  112. {voidaccess-1.9.1 → voidaccess-1.9.3}/i18n/query_expand.py +0 -0
  113. {voidaccess-1.9.1 → voidaccess-1.9.3}/i18n/translate.py +0 -0
  114. {voidaccess-1.9.1 → voidaccess-1.9.3}/monitor/__init__.py +0 -0
  115. {voidaccess-1.9.1 → voidaccess-1.9.3}/monitor/_db.py +0 -0
  116. {voidaccess-1.9.1 → voidaccess-1.9.3}/monitor/alerts.py +0 -0
  117. {voidaccess-1.9.1 → voidaccess-1.9.3}/monitor/config.py +0 -0
  118. {voidaccess-1.9.1 → voidaccess-1.9.3}/monitor/diff.py +0 -0
  119. {voidaccess-1.9.1 → voidaccess-1.9.3}/monitor/jobs.py +0 -0
  120. {voidaccess-1.9.1 → voidaccess-1.9.3}/monitor/scheduler.py +0 -0
  121. {voidaccess-1.9.1 → voidaccess-1.9.3}/scraper/__init__.py +0 -0
  122. {voidaccess-1.9.1 → voidaccess-1.9.3}/scraper/scrape.py +0 -0
  123. {voidaccess-1.9.1 → voidaccess-1.9.3}/scraper/scrape_js.py +0 -0
  124. {voidaccess-1.9.1 → voidaccess-1.9.3}/search/__init__.py +0 -0
  125. {voidaccess-1.9.1 → voidaccess-1.9.3}/search/circuit_breaker.py +0 -0
  126. {voidaccess-1.9.1 → voidaccess-1.9.3}/search/engine_catalog.py +0 -0
  127. {voidaccess-1.9.1 → voidaccess-1.9.3}/search/query_builder.py +0 -0
  128. {voidaccess-1.9.1 → voidaccess-1.9.3}/search/search.py +0 -0
  129. {voidaccess-1.9.1 → voidaccess-1.9.3}/setup.cfg +0 -0
  130. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/__init__.py +0 -0
  131. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/actor_profiles.py +0 -0
  132. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/blockchain.py +0 -0
  133. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/breach_lookup.py +0 -0
  134. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/cache.py +0 -0
  135. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/cisa.py +0 -0
  136. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/data/__init__.py +0 -0
  137. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/data/onion_seeds.json +0 -0
  138. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/dns_enrichment.py +0 -0
  139. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/domain_reputation.py +0 -0
  140. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/engines.py +0 -0
  141. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/enrichment.py +0 -0
  142. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/github_scraper.py +0 -0
  143. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/gitlab_scraper.py +0 -0
  144. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/hash_reputation.py +0 -0
  145. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/historical_intel.py +0 -0
  146. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/infostealer.py +0 -0
  147. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/ip_reputation.py +0 -0
  148. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/nvd.py +0 -0
  149. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/paste_scraper.py +0 -0
  150. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/pastes.py +0 -0
  151. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/proxy_client.py +0 -0
  152. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/rss_scraper.py +0 -0
  153. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/seed_manager.py +0 -0
  154. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/seeds.py +0 -0
  155. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/shodan.py +0 -0
  156. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/telegram.py +0 -0
  157. {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/virustotal.py +0 -0
  158. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_analysis_opsec.py +0 -0
  159. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_analysis_stylometry.py +0 -0
  160. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_analysis_temporal.py +0 -0
  161. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_api_monitors.py +0 -0
  162. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_blockchain.py +0 -0
  163. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_breach_lookup.py +0 -0
  164. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_cli_proxy_config.py +0 -0
  165. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_config.py +0 -0
  166. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_consolidated_contracts.py +0 -0
  167. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_crawler.py +0 -0
  168. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_dns_enrichment.py +0 -0
  169. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_domain_reputation.py +0 -0
  170. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_email_reputation.py +0 -0
  171. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_extractor_llm_pipeline.py +0 -0
  172. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_filter_e2e_manual.py +0 -0
  173. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_filter_parser_manual.py +0 -0
  174. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_fingerprint.py +0 -0
  175. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_github_scraper.py +0 -0
  176. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_gitlab_scraper.py +0 -0
  177. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_graph.py +0 -0
  178. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_graph_integration.py +0 -0
  179. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_hash_reputation.py +0 -0
  180. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_i18n.py +0 -0
  181. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_infostealer.py +0 -0
  182. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_ip_reputation.py +0 -0
  183. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_model_singleton.py +0 -0
  184. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_monitor.py +0 -0
  185. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_monitor_integration.py +0 -0
  186. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_nvd.py +0 -0
  187. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_pagination.py +0 -0
  188. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_paste_scraper.py +0 -0
  189. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_proxy_client.py +0 -0
  190. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_ransomlook.py +0 -0
  191. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_relationship_extract.py +0 -0
  192. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_rss_scraper.py +0 -0
  193. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_scrape_js.py +0 -0
  194. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_settings.py +0 -0
  195. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_sources.py +0 -0
  196. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_sources_enrichment_new.py +0 -0
  197. {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_vector.py +0 -0
  198. {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/__init__.py +0 -0
  199. {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/async_utils.py +0 -0
  200. {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/content_dedup.py +0 -0
  201. {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/content_safety.py +0 -0
  202. {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/defang.py +0 -0
  203. {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/encryption.py +0 -0
  204. {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/enrichment_cache.py +0 -0
  205. {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/investigation_metrics.py +0 -0
  206. {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/ioc_freshness.py +0 -0
  207. {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/user_keys.py +0 -0
  208. {voidaccess-1.9.1 → voidaccess-1.9.3}/vector/__init__.py +0 -0
  209. {voidaccess-1.9.1 → voidaccess-1.9.3}/vector/embedder.py +0 -0
  210. {voidaccess-1.9.1 → voidaccess-1.9.3}/vector/search.py +0 -0
  211. {voidaccess-1.9.1 → voidaccess-1.9.3}/vector/store.py +0 -0
  212. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess/__init__.py +0 -0
  213. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess/config.py +0 -0
  214. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess/llm_utils.py +0 -0
  215. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess.egg-info/dependency_links.txt +0 -0
  216. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess.egg-info/entry_points.txt +0 -0
  217. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess.egg-info/requires.txt +0 -0
  218. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess.egg-info/top_level.txt +0 -0
  219. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/adapters/__init__.py +0 -0
  220. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/adapters/sqlite.py +0 -0
  221. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/browser.py +0 -0
  222. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/commands/__init__.py +0 -0
  223. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/commands/actors.py +0 -0
  224. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/commands/enrich.py +0 -0
  225. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/commands/investigate.py +0 -0
  226. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/commands/show.py +0 -0
  227. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/display.py +0 -0
  228. {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/main.py +0 -0
  229. {voidaccess-1.9.1 → voidaccess-1.9.3}/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.3
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 |
@@ -40,7 +40,7 @@ logging.basicConfig(level=logging.DEBUG)
40
40
  # Rate limiter setup
41
41
  # ---------------------------------------------------------------------------
42
42
 
43
- DISABLE_RATE_LIMIT = os.getenv("DISABLE_RATE_LIMIT", "false").lower() == "true"
43
+ DISABLE_RATE_LIMIT = (os.getenv("DISABLE_RATE_LIMIT", "false") or "false").lower() == "true"
44
44
 
45
45
  if DISABLE_RATE_LIMIT:
46
46
  limiter = None
@@ -537,7 +537,7 @@ async def _check_cancelled(inv_uuid: uuid.UUID, investigation_id: str) -> bool:
537
537
  # Rate limiting (shared key_func with api/main.py; enforcement via app.state.limiter)
538
538
  # ---------------------------------------------------------------------------
539
539
 
540
- _DISABLE_RATE_LIMIT = os.getenv("DISABLE_RATE_LIMIT", "false").lower() == "true"
540
+ _DISABLE_RATE_LIMIT = (os.getenv("DISABLE_RATE_LIMIT", "false") or "false").lower() == "true"
541
541
 
542
542
  if not _DISABLE_RATE_LIMIT:
543
543
  try:
@@ -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.3"
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
@@ -18,11 +18,11 @@ from __future__ import annotations
18
18
  import json
19
19
  import logging
20
20
  import os
21
+ import re
21
22
  import warnings
22
23
  from typing import Any, Optional
23
24
  import uuid
24
25
 
25
- from graph.model import RELATIONSHIP_TYPE_STIX
26
26
  from extractor.confidence import get_entity_confidence
27
27
 
28
28
  logger = logging.getLogger(__name__)
@@ -79,6 +79,114 @@ _STIX_PATTERNS: dict[str, str] = {
79
79
  # Entity types that map to STIX Malware objects
80
80
  _MALWARE_TYPES = frozenset({"MALWARE_FAMILY", "RANSOMWARE_GROUP"})
81
81
 
82
+ # STIX 2.1's malware-type vocabulary is intentionally small. Keep the
83
+ # mapping here instead of treating every MALWARE_FAMILY as the generic
84
+ # ``malware`` type: family names and enrichment metadata often carry enough
85
+ # information for SIEM consumers to filter useful categories.
86
+ _STIX_MALWARE_TYPE_ALIASES = {
87
+ "infostealer": "spyware",
88
+ "stealer": "spyware",
89
+ "remote_access_trojan": "remote-access-trojan",
90
+ "remote access trojan": "remote-access-trojan",
91
+ "rat": "remote-access-trojan",
92
+ "coinminer": "resource-exploitation",
93
+ "cryptominer": "resource-exploitation",
94
+ "crypto-miner": "resource-exploitation",
95
+ "miner": "resource-exploitation",
96
+ }
97
+ _STIX_MALWARE_TYPES = frozenset({
98
+ "adware",
99
+ "backdoor",
100
+ "bot",
101
+ "ddos",
102
+ "dropper",
103
+ "exploit-kit",
104
+ "keylogger",
105
+ "ransomware",
106
+ "remote-access-trojan",
107
+ "resource-exploitation",
108
+ "rootkit",
109
+ "screen-capture",
110
+ "spyware",
111
+ "wiper",
112
+ "malware",
113
+ })
114
+
115
+ # These are conservative, high-signal family/name hints. Unknown families
116
+ # retain the valid generic STIX type rather than being assigned a speculative
117
+ # category.
118
+ _MALWARE_NAME_PATTERNS = (
119
+ ("ransomware", re.compile(
120
+ r"\b(?:ransomware|lockbit|blackcat|alphv|clop|conti|ryuk|hive|revil|darkside|wannacry)\b",
121
+ re.IGNORECASE,
122
+ )),
123
+ ("remote-access-trojan", re.compile(
124
+ r"\b(?:remote\s+access\s+trojan|remcos|njrat|nanocore|quasar|darkcomet|xrat)\b",
125
+ re.IGNORECASE,
126
+ )),
127
+ ("spyware", re.compile(
128
+ r"\b(?:spyware|infostealer|stealer|redline|vidar|lumma|raccoon|agent\s+tesla|formbook)\b",
129
+ re.IGNORECASE,
130
+ )),
131
+ ("bot", re.compile(
132
+ r"\b(?:botnet|emotet|qakbot|qbot|trickbot|mirai|zeus|mozi)\b",
133
+ re.IGNORECASE,
134
+ )),
135
+ ("keylogger", re.compile(r"\bkeylogger\b", re.IGNORECASE)),
136
+ ("exploit-kit", re.compile(r"\bexploit[\s-]+kit\b", re.IGNORECASE)),
137
+ ("rootkit", re.compile(r"\brootkit\b", re.IGNORECASE)),
138
+ ("screen-capture", re.compile(r"\bscreen[\s-]+capture\b", re.IGNORECASE)),
139
+ ("dropper", re.compile(r"\bdropper\b", re.IGNORECASE)),
140
+ ("wiper", re.compile(r"\bwiper\b", re.IGNORECASE)),
141
+ ("adware", re.compile(r"\badware\b", re.IGNORECASE)),
142
+ ("resource-exploitation", re.compile(
143
+ r"\b(?:cryptominer|crypto[\s-]+miner|coinminer|cryptojacking)\b",
144
+ re.IGNORECASE,
145
+ )),
146
+ ("backdoor", re.compile(r"\bbackdoor\b", re.IGNORECASE)),
147
+ )
148
+
149
+
150
+ def _entity_attribute(entity: Any, name: str) -> Any:
151
+ """Read optional classification metadata from dicts and ORM-like rows."""
152
+ if isinstance(entity, dict):
153
+ return entity.get(name)
154
+ return getattr(entity, name, None)
155
+
156
+
157
+ def _normalise_malware_type(value: Any) -> Optional[str]:
158
+ if not isinstance(value, str):
159
+ return None
160
+ key = value.strip().lower().replace("_", " ")
161
+ key = _STIX_MALWARE_TYPE_ALIASES.get(key, key)
162
+ key = key.replace(" ", "-")
163
+ return key if key in _STIX_MALWARE_TYPES else None
164
+
165
+
166
+ def _classify_malware(entity: Any) -> list[str]:
167
+ """Return evidence-backed STIX malware types for an entity."""
168
+ entity_type = str(getattr(entity, "entity_type", "") or "").upper()
169
+ if isinstance(entity, dict):
170
+ entity_type = str(entity.get("entity_type") or "").upper()
171
+ if entity_type == "RANSOMWARE_GROUP":
172
+ return ["ransomware"]
173
+
174
+ for field_name in ("malware_types", "malware_type", "malware_category", "category", "family_type"):
175
+ raw = _entity_attribute(entity, field_name)
176
+ values = raw if isinstance(raw, (list, tuple, set)) else [raw]
177
+ classified = [item for item in (_normalise_malware_type(v) for v in values) if item]
178
+ if classified:
179
+ return list(dict.fromkeys(classified))
180
+
181
+ evidence = " ".join(
182
+ str(_entity_attribute(entity, field) or "")
183
+ for field in ("value", "context_snippet")
184
+ )
185
+ for malware_type, pattern in _MALWARE_NAME_PATTERNS:
186
+ if pattern.search(evidence):
187
+ return [malware_type]
188
+ return ["malware"]
189
+
82
190
  # ---------------------------------------------------------------------------
83
191
  # Confidence mapping: VoidAccess float → STIX integer (0-100)
84
192
  # ---------------------------------------------------------------------------
@@ -161,6 +269,7 @@ def entity_to_stix_malware(entity: Any) -> Optional[Any]:
161
269
  malware = stix2.Malware(
162
270
  name=entity.value,
163
271
  is_family=True,
272
+ malware_types=_classify_malware(entity),
164
273
  confidence=_to_stix_confidence(get_entity_confidence(entity)),
165
274
  allow_custom=True,
166
275
  x_voidaccess_source_quality=getattr(entity, "source_quality", 1.0),
@@ -176,6 +285,28 @@ def entity_to_stix_malware(entity: Any) -> Optional[Any]:
176
285
  return None
177
286
 
178
287
 
288
+ def entity_to_stix_identity(entity: Any) -> Optional[Any]:
289
+ """Convert an organization entity to a STIX Identity object."""
290
+ if not _STIX2_AVAILABLE or entity.entity_type != "ORGANIZATION_NAME":
291
+ return None
292
+ try:
293
+ return stix2.Identity(
294
+ name=entity.value,
295
+ identity_class="organization",
296
+ confidence=_to_stix_confidence(get_entity_confidence(entity)),
297
+ allow_custom=True,
298
+ x_voidaccess_source_quality=getattr(entity, "source_quality", 1.0),
299
+ external_references=(
300
+ [{"source_name": "voidaccess", "url": entity.source_url}]
301
+ if entity.source_url
302
+ else []
303
+ ),
304
+ )
305
+ except Exception as exc:
306
+ logger.warning("entity_to_stix_identity failed for %r: %s", entity.value, exc)
307
+ return None
308
+
309
+
179
310
  def entity_to_stix_threat_actor(entity: Any) -> Optional[Any]:
180
311
  """
181
312
  Convert a THREAT_ACTOR_HANDLE entity to a STIX 2.1 ThreatActor object.
@@ -287,6 +418,11 @@ def investigation_to_stix_bundle(
287
418
  stix_objects.append(actor)
288
419
  _register(entity, actor.id)
289
420
 
421
+ identity = entity_to_stix_identity(entity)
422
+ if identity:
423
+ stix_objects.append(identity)
424
+ _register(entity, identity.id)
425
+
290
426
  if include_relationships and stix_objects:
291
427
  relationships, relationship_warning = _build_stix_relationships(
292
428
  investigation_id, stix_id_map
@@ -436,32 +572,22 @@ def _build_stix_relationships(
436
572
  if not _STIX2_AVAILABLE:
437
573
  return [], "stix2 is not installed"
438
574
  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)
575
+ from export.relationships import load_export_relationships # noqa: PLC0415
443
576
 
577
+ graph_relationships, warning = load_export_relationships(
578
+ investigation_id, stix_id_map
579
+ )
580
+ if warning:
581
+ return [], warning
444
582
  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)
583
+ for data in graph_relationships:
458
584
  # Carry the edge's own confidence onto the SRO. Typed relationships
459
585
  # have a claim-specific confidence distinct from co-occurrence's
460
586
  # flat value; STIX 2.1 confidence is an int 0-100.
461
587
  rel_kwargs: dict[str, Any] = {
462
- "relationship_type": rel_type,
463
- "source_ref": src_stix_id,
464
- "target_ref": tgt_stix_id,
588
+ "relationship_type": data["stix_relationship_type"],
589
+ "source_ref": data["source_ref"],
590
+ "target_ref": data["target_ref"],
465
591
  "allow_custom": True,
466
592
  }
467
593
  try:
@@ -488,7 +614,9 @@ def _build_stix_relationships(
488
614
 
489
615
  def _edge_type_to_stix(edge_type: str) -> str:
490
616
  """Map VoidAccess graph edge types to STIX relationship type strings."""
491
- return RELATIONSHIP_TYPE_STIX.get(edge_type, "related-to")
617
+ from export.relationships import _normalise_edge_type # noqa: PLC0415
618
+ from graph.model import RELATIONSHIP_TYPE_STIX # noqa: PLC0415
619
+ return RELATIONSHIP_TYPE_STIX.get(_normalise_edge_type(edge_type), "related-to")
492
620
 
493
621
 
494
622
  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.3"
8
8
  description = "Dark web OSINT CLI — automated threat intelligence from query to report"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -555,8 +555,10 @@ def _update_email_entities_in_db(
555
555
  existing.append(tag)
556
556
  db_entity.corroborating_sources = json.dumps(existing)
557
557
  session.commit()
558
- except Exception as exc:
559
- logger.warning("email_reputation: DB update failed: %s", exc)
558
+ except Exception:
559
+ # Do not log the raw SQLAlchemy exception: its parameter dump can
560
+ # contain the full email address being enriched.
561
+ logger.warning("email_reputation: DB update failed")
560
562
 
561
563
 
562
564
  # ---------------------------------------------------------------------------