voidaccess 2.0.1__tar.gz → 2.0.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 (262) hide show
  1. {voidaccess-2.0.1 → voidaccess-2.0.3}/PKG-INFO +1 -1
  2. {voidaccess-2.0.1 → voidaccess-2.0.3}/api/main.py +1 -1
  3. {voidaccess-2.0.1 → voidaccess-2.0.3}/export/__init__.py +2 -0
  4. {voidaccess-2.0.1 → voidaccess-2.0.3}/export/ioc_package.py +3 -47
  5. {voidaccess-2.0.1 → voidaccess-2.0.3}/export/sigma.py +58 -1
  6. {voidaccess-2.0.1 → voidaccess-2.0.3}/pyproject.toml +2 -2
  7. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_cli_proxy_config.py +33 -0
  8. voidaccess-2.0.3/tests/test_legacy_backfill.py +124 -0
  9. voidaccess-2.0.3/voidaccess/backfill.py +251 -0
  10. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess.egg-info/PKG-INFO +1 -1
  11. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess.egg-info/SOURCES.txt +2 -0
  12. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/__init__.py +1 -1
  13. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/commands/export.py +53 -17
  14. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/config.py +15 -0
  15. {voidaccess-2.0.1 → voidaccess-2.0.3}/LICENSE +0 -0
  16. {voidaccess-2.0.1 → voidaccess-2.0.3}/README.md +0 -0
  17. {voidaccess-2.0.1 → voidaccess-2.0.3}/alembic.ini +0 -0
  18. {voidaccess-2.0.1 → voidaccess-2.0.3}/analysis/__init__.py +0 -0
  19. {voidaccess-2.0.1 → voidaccess-2.0.3}/analysis/opsec.py +0 -0
  20. {voidaccess-2.0.1 → voidaccess-2.0.3}/analysis/patterns.py +0 -0
  21. {voidaccess-2.0.1 → voidaccess-2.0.3}/analysis/temporal.py +0 -0
  22. {voidaccess-2.0.1 → voidaccess-2.0.3}/api/__init__.py +0 -0
  23. {voidaccess-2.0.1 → voidaccess-2.0.3}/api/auth.py +0 -0
  24. {voidaccess-2.0.1 → voidaccess-2.0.3}/api/errors.py +0 -0
  25. {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/__init__.py +0 -0
  26. {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/actors.py +0 -0
  27. {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/admin.py +0 -0
  28. {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/auth.py +0 -0
  29. {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/entities.py +0 -0
  30. {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/export.py +0 -0
  31. {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/investigations.py +0 -0
  32. {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/monitors.py +0 -0
  33. {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/search.py +0 -0
  34. {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/settings.py +0 -0
  35. {voidaccess-2.0.1 → voidaccess-2.0.3}/auth/__init__.py +0 -0
  36. {voidaccess-2.0.1 → voidaccess-2.0.3}/auth/token_blacklist.py +0 -0
  37. {voidaccess-2.0.1 → voidaccess-2.0.3}/config.py +0 -0
  38. {voidaccess-2.0.1 → voidaccess-2.0.3}/crawler/__init__.py +0 -0
  39. {voidaccess-2.0.1 → voidaccess-2.0.3}/crawler/dedup.py +0 -0
  40. {voidaccess-2.0.1 → voidaccess-2.0.3}/crawler/frontier.py +0 -0
  41. {voidaccess-2.0.1 → voidaccess-2.0.3}/crawler/spider.py +0 -0
  42. {voidaccess-2.0.1 → voidaccess-2.0.3}/crawler/utils.py +0 -0
  43. {voidaccess-2.0.1 → voidaccess-2.0.3}/data/__init__.py +0 -0
  44. {voidaccess-2.0.1 → voidaccess-2.0.3}/data/common_words_en.txt +0 -0
  45. {voidaccess-2.0.1 → voidaccess-2.0.3}/data/domain_stopwords_en.txt +0 -0
  46. {voidaccess-2.0.1 → voidaccess-2.0.3}/data/monitors.yaml +0 -0
  47. {voidaccess-2.0.1 → voidaccess-2.0.3}/data/seed_data.json.gz +0 -0
  48. {voidaccess-2.0.1 → voidaccess-2.0.3}/data/threat_gazetteer.json +0 -0
  49. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/__init__.py +0 -0
  50. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/__init__.py +0 -0
  51. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/env.py +0 -0
  52. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0001_initial_schema.py +0 -0
  53. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0002_add_investigation_status_column.py +0 -0
  54. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0002_add_missing_tables.py +0 -0
  55. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0003_add_canonical_value_and_entity_links.py +0 -0
  56. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0004_add_page_posted_at.py +0 -0
  57. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0005_add_extraction_method.py +0 -0
  58. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0006_add_monitor_alerts.py +0 -0
  59. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0007_add_actor_style_profiles.py +0 -0
  60. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0008_add_users_table.py +0 -0
  61. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0009_add_investigation_id_to_relationships.py +0 -0
  62. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0010_add_composite_index_entity_relationships.py +0 -0
  63. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0011_add_page_extraction_cache.py +0 -0
  64. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0013_add_graph_status.py +0 -0
  65. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0015_add_progress_fields.py +0 -0
  66. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0016_backfill_graph_status.py +0 -0
  67. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0017_add_user_api_keys.py +0 -0
  68. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0018_add_user_id_to_investigations.py +0 -0
  69. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0019_add_content_safety_log.py +0 -0
  70. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0020_add_entity_source_tracking.py +0 -0
  71. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0021_add_search_engine_stats.py +0 -0
  72. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0022_add_actor_profiles.py +0 -0
  73. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0023_add_investigation_metadata.py +0 -0
  74. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0024_add_investigation_step_metrics.py +0 -0
  75. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0025_add_cancellation_requested.py +0 -0
  76. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0026_add_entity_investigation_count.py +0 -0
  77. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/models.py +0 -0
  78. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/queries.py +0 -0
  79. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/search_engine_stats.py +0 -0
  80. {voidaccess-2.0.1 → voidaccess-2.0.3}/db/session.py +0 -0
  81. {voidaccess-2.0.1 → voidaccess-2.0.3}/export/_entity_loading.py +0 -0
  82. {voidaccess-2.0.1 → voidaccess-2.0.3}/export/misp.py +0 -0
  83. {voidaccess-2.0.1 → voidaccess-2.0.3}/export/relationships.py +0 -0
  84. {voidaccess-2.0.1 → voidaccess-2.0.3}/export/snort_export.py +0 -0
  85. {voidaccess-2.0.1 → voidaccess-2.0.3}/export/stix.py +0 -0
  86. {voidaccess-2.0.1 → voidaccess-2.0.3}/export/yara_export.py +0 -0
  87. {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/__init__.py +0 -0
  88. {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/confidence.py +0 -0
  89. {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/dependency_relationship.py +0 -0
  90. {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/entity_shape.py +0 -0
  91. {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/gazetteer.py +0 -0
  92. {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/identity.py +0 -0
  93. {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/llm_extract.py +0 -0
  94. {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/ner.py +0 -0
  95. {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/normalizer.py +0 -0
  96. {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/pipeline.py +0 -0
  97. {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/regex_patterns.py +0 -0
  98. {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/relationship_extract.py +0 -0
  99. {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/software_suppression.py +0 -0
  100. {voidaccess-2.0.1 → voidaccess-2.0.3}/fingerprint/__init__.py +0 -0
  101. {voidaccess-2.0.1 → voidaccess-2.0.3}/fingerprint/calibration.py +0 -0
  102. {voidaccess-2.0.1 → voidaccess-2.0.3}/fingerprint/profiler.py +0 -0
  103. {voidaccess-2.0.1 → voidaccess-2.0.3}/fingerprint/stylometry.py +0 -0
  104. {voidaccess-2.0.1 → voidaccess-2.0.3}/graph/__init__.py +0 -0
  105. {voidaccess-2.0.1 → voidaccess-2.0.3}/graph/builder.py +0 -0
  106. {voidaccess-2.0.1 → voidaccess-2.0.3}/graph/export.py +0 -0
  107. {voidaccess-2.0.1 → voidaccess-2.0.3}/graph/model.py +0 -0
  108. {voidaccess-2.0.1 → voidaccess-2.0.3}/graph/queries.py +0 -0
  109. {voidaccess-2.0.1 → voidaccess-2.0.3}/graph/visualize.py +0 -0
  110. {voidaccess-2.0.1 → voidaccess-2.0.3}/i18n/__init__.py +0 -0
  111. {voidaccess-2.0.1 → voidaccess-2.0.3}/i18n/detect.py +0 -0
  112. {voidaccess-2.0.1 → voidaccess-2.0.3}/i18n/query_expand.py +0 -0
  113. {voidaccess-2.0.1 → voidaccess-2.0.3}/i18n/translate.py +0 -0
  114. {voidaccess-2.0.1 → voidaccess-2.0.3}/monitor/__init__.py +0 -0
  115. {voidaccess-2.0.1 → voidaccess-2.0.3}/monitor/_db.py +0 -0
  116. {voidaccess-2.0.1 → voidaccess-2.0.3}/monitor/alerts.py +0 -0
  117. {voidaccess-2.0.1 → voidaccess-2.0.3}/monitor/config.py +0 -0
  118. {voidaccess-2.0.1 → voidaccess-2.0.3}/monitor/diff.py +0 -0
  119. {voidaccess-2.0.1 → voidaccess-2.0.3}/monitor/jobs.py +0 -0
  120. {voidaccess-2.0.1 → voidaccess-2.0.3}/monitor/scheduler.py +0 -0
  121. {voidaccess-2.0.1 → voidaccess-2.0.3}/pacing/__init__.py +0 -0
  122. {voidaccess-2.0.1 → voidaccess-2.0.3}/scraper/__init__.py +0 -0
  123. {voidaccess-2.0.1 → voidaccess-2.0.3}/scraper/scrape.py +0 -0
  124. {voidaccess-2.0.1 → voidaccess-2.0.3}/scraper/scrape_js.py +0 -0
  125. {voidaccess-2.0.1 → voidaccess-2.0.3}/scraper/tor_pool.py +0 -0
  126. {voidaccess-2.0.1 → voidaccess-2.0.3}/search/__init__.py +0 -0
  127. {voidaccess-2.0.1 → voidaccess-2.0.3}/search/circuit_breaker.py +0 -0
  128. {voidaccess-2.0.1 → voidaccess-2.0.3}/search/config.py +0 -0
  129. {voidaccess-2.0.1 → voidaccess-2.0.3}/search/engine_catalog.py +0 -0
  130. {voidaccess-2.0.1 → voidaccess-2.0.3}/search/query_builder.py +0 -0
  131. {voidaccess-2.0.1 → voidaccess-2.0.3}/search/search.py +0 -0
  132. {voidaccess-2.0.1 → voidaccess-2.0.3}/search/tor_isolation.py +0 -0
  133. {voidaccess-2.0.1 → voidaccess-2.0.3}/setup.cfg +0 -0
  134. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/__init__.py +0 -0
  135. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/actor_profiles.py +0 -0
  136. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/blockchain.py +0 -0
  137. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/breach_lookup.py +0 -0
  138. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/cache.py +0 -0
  139. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/cisa.py +0 -0
  140. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/data/__init__.py +0 -0
  141. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/data/onion_seeds.json +0 -0
  142. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/dns_enrichment.py +0 -0
  143. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/domain_reputation.py +0 -0
  144. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/email_reputation.py +0 -0
  145. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/engines.py +0 -0
  146. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/enrichment.py +0 -0
  147. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/github_scraper.py +0 -0
  148. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/gitlab_scraper.py +0 -0
  149. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/hash_reputation.py +0 -0
  150. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/historical_intel.py +0 -0
  151. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/infostealer.py +0 -0
  152. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/ip_reputation.py +0 -0
  153. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/nvd.py +0 -0
  154. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/paste_scraper.py +0 -0
  155. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/pastes.py +0 -0
  156. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/proxy_client.py +0 -0
  157. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/rss_scraper.py +0 -0
  158. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/seed_manager.py +0 -0
  159. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/seeds.py +0 -0
  160. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/shodan.py +0 -0
  161. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/telegram.py +0 -0
  162. {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/virustotal.py +0 -0
  163. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_actor_extraction_gap.py +0 -0
  164. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_analysis_opsec.py +0 -0
  165. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_analysis_stylometry.py +0 -0
  166. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_analysis_temporal.py +0 -0
  167. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_api.py +0 -0
  168. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_api_monitors.py +0 -0
  169. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_blockchain.py +0 -0
  170. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_breach_lookup.py +0 -0
  171. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_cli_manifest.py +0 -0
  172. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_community_detection_parity.py +0 -0
  173. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_confidence_calibration.py +0 -0
  174. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_config.py +0 -0
  175. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_consolidated_contracts.py +0 -0
  176. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_crawler.py +0 -0
  177. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_db.py +0 -0
  178. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_dependency_relationship.py +0 -0
  179. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_dns_enrichment.py +0 -0
  180. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_domain_reputation.py +0 -0
  181. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_email_reputation.py +0 -0
  182. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_entity_priority.py +0 -0
  183. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_extractor_llm_pipeline.py +0 -0
  184. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_filter_e2e_manual.py +0 -0
  185. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_filter_parser_manual.py +0 -0
  186. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_fingerprint.py +0 -0
  187. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_gazetteer.py +0 -0
  188. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_github_scraper.py +0 -0
  189. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_gitlab_scraper.py +0 -0
  190. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_graph.py +0 -0
  191. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_graph_integration.py +0 -0
  192. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_hash_reputation.py +0 -0
  193. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_heuristic_expansion.py +0 -0
  194. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_i18n.py +0 -0
  195. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_infostealer.py +0 -0
  196. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_ioc_package_export.py +0 -0
  197. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_ip_reputation.py +0 -0
  198. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_llm.py +0 -0
  199. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_llm_utils.py +0 -0
  200. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_model_singleton.py +0 -0
  201. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_monitor.py +0 -0
  202. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_monitor_integration.py +0 -0
  203. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_nvd.py +0 -0
  204. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_pacing.py +0 -0
  205. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_pacing_enrichment.py +0 -0
  206. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_pacing_quotas.py +0 -0
  207. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_pagination.py +0 -0
  208. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_paste_scraper.py +0 -0
  209. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_priority_score_cli_api_parity.py +0 -0
  210. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_proxy_client.py +0 -0
  211. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_ransomlook.py +0 -0
  212. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_regex_patterns.py +0 -0
  213. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_relationship_extract.py +0 -0
  214. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_requirement_bugs.py +0 -0
  215. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_rss_scraper.py +0 -0
  216. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_scrape_isolation.py +0 -0
  217. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_scrape_js.py +0 -0
  218. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_settings.py +0 -0
  219. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_sigma_export.py +0 -0
  220. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_snort_export.py +0 -0
  221. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_software_suppression.py +0 -0
  222. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_sources.py +0 -0
  223. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_sources_enrichment_new.py +0 -0
  224. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_stix_export.py +0 -0
  225. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_vector.py +0 -0
  226. {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_yara_export.py +0 -0
  227. {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/__init__.py +0 -0
  228. {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/async_utils.py +0 -0
  229. {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/content_dedup.py +0 -0
  230. {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/content_safety.py +0 -0
  231. {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/defang.py +0 -0
  232. {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/encryption.py +0 -0
  233. {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/enrichment_cache.py +0 -0
  234. {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/entity_priority.py +0 -0
  235. {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/investigation_metrics.py +0 -0
  236. {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/ioc_freshness.py +0 -0
  237. {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/user_keys.py +0 -0
  238. {voidaccess-2.0.1 → voidaccess-2.0.3}/vector/__init__.py +0 -0
  239. {voidaccess-2.0.1 → voidaccess-2.0.3}/vector/embedder.py +0 -0
  240. {voidaccess-2.0.1 → voidaccess-2.0.3}/vector/model_singleton.py +0 -0
  241. {voidaccess-2.0.1 → voidaccess-2.0.3}/vector/search.py +0 -0
  242. {voidaccess-2.0.1 → voidaccess-2.0.3}/vector/store.py +0 -0
  243. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess/__init__.py +0 -0
  244. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess/config.py +0 -0
  245. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess/llm.py +0 -0
  246. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess/llm_utils.py +0 -0
  247. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess.egg-info/dependency_links.txt +0 -0
  248. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess.egg-info/entry_points.txt +0 -0
  249. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess.egg-info/requires.txt +0 -0
  250. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess.egg-info/top_level.txt +0 -0
  251. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/adapters/__init__.py +0 -0
  252. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/adapters/sqlite.py +0 -0
  253. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/browser.py +0 -0
  254. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/commands/__init__.py +0 -0
  255. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/commands/actors.py +0 -0
  256. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/commands/configure.py +0 -0
  257. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/commands/enrich.py +0 -0
  258. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/commands/investigate.py +0 -0
  259. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/commands/show.py +0 -0
  260. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/display.py +0 -0
  261. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/main.py +0 -0
  262. {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/tor_detect.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voidaccess
3
- Version: 2.0.1
3
+ Version: 2.0.3
4
4
  Summary: Dark web OSINT CLI — automated threat intelligence from query to report
5
5
  Author: VoidAccess
6
6
  License-Expression: MIT
@@ -332,7 +332,7 @@ async def _periodic_stuck_sweep() -> None:
332
332
  app = FastAPI(
333
333
  title="VoidAccess Intelligence API",
334
334
  description="VoidAccess: Dark Web Intelligence Platform",
335
- version="2.0.1",
335
+ version="2.0.3",
336
336
  docs_url="/docs",
337
337
  redoc_url="/redoc",
338
338
  lifespan=lifespan,
@@ -17,6 +17,7 @@ from export.misp import (
17
17
  from export.sigma import (
18
18
  entities_to_sigma_rules,
19
19
  export_sigma_rules,
20
+ render_sigma_rules,
20
21
  sigma_rule_to_yaml,
21
22
  )
22
23
  from export.yara_export import (
@@ -44,6 +45,7 @@ __all__ = [
44
45
  # sigma
45
46
  "entities_to_sigma_rules",
46
47
  "sigma_rule_to_yaml",
48
+ "render_sigma_rules",
47
49
  "export_sigma_rules",
48
50
  # yara
49
51
  "generate_yara_rules",
@@ -57,7 +57,7 @@ logger = logging.getLogger(__name__)
57
57
  # ---------------------------------------------------------------------------
58
58
 
59
59
  PACKAGE_FORMAT = "voidaccess-ioc-v1"
60
- PACKAGE_VERSION = "2.0.1"
60
+ PACKAGE_VERSION = "2.0.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.
@@ -702,57 +702,13 @@ def _safe_misp_json(entities: list[Any], investigation_id: Any) -> str:
702
702
 
703
703
  def _safe_sigma_yml(entities: list[Any]) -> str:
704
704
  try:
705
- from export.sigma import entities_to_sigma_rules, sigma_rule_to_yaml
706
- # Sigma rules expect object-style entities (NormalizedEntity). When the
707
- # caller passes plain dicts, wrap them in a tiny adapter so Sigma can
708
- # read .entity_type, .value, .confidence, and .source_url.
709
- adapted: list[Any] = []
710
- for e in entities or []:
711
- if isinstance(e, dict):
712
- adapted.append(_DictEntityAdapter(e))
713
- else:
714
- adapted.append(e)
715
- rules = entities_to_sigma_rules(adapted)
716
- return "\n---\n".join(sigma_rule_to_yaml(r) for r in rules if r)
705
+ from export.sigma import render_sigma_rules
706
+ return render_sigma_rules(entities or [])
717
707
  except Exception as exc:
718
708
  logger.warning("Sigma generation for IOC package failed: %s", exc)
719
709
  return ""
720
710
 
721
711
 
722
- class _DictEntityAdapter:
723
- """Adapt a plain dict into an object with .entity_type / .value / etc.
724
-
725
- Used to feed dict-shaped entities into Sigma / STIX / MISP generators
726
- that expect object access. Keeps the IOC package module standalone
727
- without changing the existing Sigma / STIX / MISP module signatures.
728
- """
729
-
730
- __slots__ = ("_d",)
731
-
732
- def __init__(self, d: dict[str, Any]) -> None:
733
- self._d = d
734
-
735
- @property
736
- def entity_type(self) -> str:
737
- return _entity_type(self._d)
738
-
739
- @property
740
- def value(self) -> str:
741
- return _entity_value(self._d)
742
-
743
- @property
744
- def canonical_value(self) -> str:
745
- return entity_canonical_id(self._d)
746
-
747
- @property
748
- def confidence(self) -> float:
749
- return _entity_confidence(self._d)
750
-
751
- @property
752
- def source_url(self) -> str:
753
- return _entity_source_url(self._d)
754
-
755
-
756
712
  # ---------------------------------------------------------------------------
757
713
  # README + metadata builders
758
714
  # ---------------------------------------------------------------------------
@@ -207,7 +207,8 @@ def entities_to_sigma_rules(
207
207
  Falls back to base rule if LLM fails.
208
208
  """
209
209
  rules: list[dict] = []
210
- for entity in entities:
210
+ for raw_entity in entities:
211
+ entity = _adapt_entity(raw_entity)
211
212
  if entity.entity_type not in _SIGMA_ENTITY_TYPES:
212
213
  continue
213
214
  base = _build_base_rule(entity)
@@ -228,6 +229,23 @@ def sigma_rule_to_yaml(rule: dict) -> str:
228
229
  return ""
229
230
 
230
231
 
232
+ def render_sigma_rules(
233
+ entities: list[Any],
234
+ llm: Optional[Any] = None,
235
+ ) -> str:
236
+ """Render the shared Sigma artifact used by every export surface.
237
+
238
+ The CLI export and the IOC package both receive the same export payload,
239
+ whose entities are normally dictionaries. Keeping adaptation, rule
240
+ generation, and YAML assembly here prevents one surface from growing a
241
+ second DB-loading implementation with different failure semantics.
242
+ """
243
+ rules = entities_to_sigma_rules(list(entities or []), llm=llm)
244
+ return "\n---\n".join(
245
+ sigma_rule_to_yaml(rule) for rule in rules if rule
246
+ )
247
+
248
+
231
249
  def export_sigma_rules(
232
250
  investigation_id: Any,
233
251
  output_dir: str,
@@ -320,6 +338,45 @@ def _load_entities_for_investigation(investigation_id: Any) -> list[Any]:
320
338
  return []
321
339
 
322
340
 
341
+ class _DictEntityAdapter:
342
+ """Expose dictionary-shaped export entities through Sigma's object API."""
343
+
344
+ __slots__ = ("_data",)
345
+
346
+ def __init__(self, data: dict[str, Any]) -> None:
347
+ self._data = data
348
+
349
+ @property
350
+ def entity_type(self) -> str:
351
+ return str(self._data.get("entity_type") or "")
352
+
353
+ @property
354
+ def value(self) -> str:
355
+ return str(
356
+ self._data.get("canonical_value")
357
+ or self._data.get("value")
358
+ or ""
359
+ )
360
+
361
+ @property
362
+ def confidence(self) -> float:
363
+ try:
364
+ return float(self._data.get("confidence", 0.0) or 0.0)
365
+ except (TypeError, ValueError):
366
+ return 0.0
367
+
368
+ @property
369
+ def source_url(self) -> str:
370
+ return str(self._data.get("source_url") or "")
371
+
372
+
373
+ def _adapt_entity(entity: Any) -> Any:
374
+ """Accept both ORM/normalized entities and export dictionaries."""
375
+ if isinstance(entity, dict):
376
+ return _DictEntityAdapter(entity)
377
+ return entity
378
+
379
+
323
380
  def _coerce_uuid(value: Any):
324
381
  """Coerce value to uuid.UUID. Returns None on failure."""
325
382
  import uuid as _uuid
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "voidaccess"
7
- version = "2.0.1"
7
+ version = "2.0.3"
8
8
  description = "Dark web OSINT CLI — automated threat intelligence from query to report"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -154,7 +154,7 @@ exclude = [
154
154
  "sources" = ["data/onion_seeds.json"]
155
155
  "sources.data" = ["onion_seeds.json"]
156
156
 
157
- # v2.0.1 — ship the top-level reference data bundle inside the installed
157
+ # v2.0.2 — ship the top-level reference data bundle inside the installed
158
158
  # package. The runtime resolves each of these via
159
159
  # ``os.path.dirname(os.path.dirname(__file__))/data`` (from extractor/ and
160
160
  # monitor/), i.e. ``site-packages/data/<file>`` after a wheel install. The
@@ -46,6 +46,8 @@ No real network. All file I/O uses tmp_path.
46
46
  from __future__ import annotations
47
47
 
48
48
  import os
49
+ import stat
50
+ import subprocess
49
51
  import sys
50
52
  import types
51
53
 
@@ -64,11 +66,42 @@ def isolated_config_home(tmp_path, monkeypatch):
64
66
 
65
67
  monkeypatch.setattr(cli_config, "CLI_HOME", tmp_path)
66
68
  monkeypatch.setattr(cli_config, "CONFIG_PATH", tmp_path / "config.json")
69
+ monkeypatch.setattr(cli_config, "ENV_PATH", tmp_path / ".env")
67
70
  monkeypatch.setattr(cli_config, "DB_PATH", tmp_path / "investigations.db")
68
71
  monkeypatch.setattr(cli_config, "DEFAULT_OUTPUT_DIR", tmp_path / "results")
69
72
  return cli_config
70
73
 
71
74
 
75
+ def test_fresh_configure_run_locks_env_file(isolated_config_home):
76
+ """A configure write applies the same owner-only protection to .env."""
77
+ isolated_config_home.ENV_PATH.write_text(
78
+ "JWT_SECRET=test-only\n",
79
+ encoding="utf-8",
80
+ )
81
+
82
+ from typer.testing import CliRunner
83
+ from voidaccess_cli.commands.configure import app as configure_app
84
+
85
+ result = CliRunner().invoke(
86
+ configure_app,
87
+ ["tor", "--host", "127.0.0.1", "--port", "9050"],
88
+ )
89
+ assert result.exit_code == 0, result.output
90
+
91
+ if os.name != "nt":
92
+ assert stat.S_IMODE(isolated_config_home.ENV_PATH.stat().st_mode) == 0o600
93
+ else:
94
+ acl = subprocess.run(
95
+ ["icacls", str(isolated_config_home.ENV_PATH)],
96
+ check=True,
97
+ capture_output=True,
98
+ text=True,
99
+ ).stdout
100
+ assert "(I)" not in acl
101
+ assert "BUILTIN\\Users" not in acl
102
+ assert "Authenticated Users" not in acl
103
+
104
+
72
105
  def _scrub_env(monkeypatch):
73
106
  """Remove every proxy-related env var so each test starts from a known state."""
74
107
  for k in (
@@ -0,0 +1,124 @@
1
+ """Tests for the one-time v2.0.3 legacy-data repair."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import datetime
6
+ import uuid
7
+
8
+
9
+ def test_backfill_resolves_typed_page_and_persists_communities(db_engine):
10
+ from db.models import Entity, EntityRelationship, Investigation, Page
11
+ from db.session import get_session
12
+ from voidaccess.backfill import run_backfill
13
+
14
+ now = datetime.datetime.now(datetime.timezone.utc)
15
+ investigation_id = uuid.uuid4()
16
+ page_id = uuid.uuid4()
17
+ source_id, target_id = uuid.uuid4(), uuid.uuid4()
18
+
19
+ with get_session() as session:
20
+ session.add(
21
+ Investigation(
22
+ id=investigation_id,
23
+ query="legacy backfill test",
24
+ status="completed",
25
+ graph_status="built",
26
+ created_at=now,
27
+ )
28
+ )
29
+ session.add(
30
+ Page(
31
+ id=page_id,
32
+ url="https://example.test/legacy",
33
+ cleaned_text="Actor uses malware",
34
+ scrape_timestamp=now,
35
+ created_at=now,
36
+ )
37
+ )
38
+ session.add_all(
39
+ [
40
+ Entity(
41
+ id=source_id,
42
+ page_id=page_id,
43
+ investigation_id=investigation_id,
44
+ entity_type="THREAT_ACTOR_HANDLE",
45
+ value="actor-one",
46
+ canonical_value="actor-one",
47
+ confidence=0.95,
48
+ first_seen=now,
49
+ last_seen=now,
50
+ created_at=now,
51
+ ),
52
+ Entity(
53
+ id=target_id,
54
+ page_id=page_id,
55
+ investigation_id=investigation_id,
56
+ entity_type="MALWARE_FAMILY",
57
+ value="malware-one",
58
+ canonical_value="malware-one",
59
+ confidence=0.95,
60
+ first_seen=now,
61
+ last_seen=now,
62
+ created_at=now,
63
+ ),
64
+ ]
65
+ )
66
+ session.add(
67
+ EntityRelationship(
68
+ entity_a_id=source_id,
69
+ entity_b_id=target_id,
70
+ relationship_type="USES",
71
+ confidence=0.9,
72
+ investigation_id=investigation_id,
73
+ source_page_id=None,
74
+ first_seen=now,
75
+ )
76
+ )
77
+ session.commit()
78
+
79
+ result = run_backfill(session, investigation_id)
80
+ session.commit()
81
+
82
+ relationship = session.query(EntityRelationship).one()
83
+ investigation = session.get(Investigation, investigation_id)
84
+
85
+ assert result["typed_relationship_pages"]["resolved"] == 1
86
+ assert relationship.source_page_id == page_id
87
+ assert investigation.metadata_json["communities"]
88
+ assert investigation.metadata_json["community_count"] == 1
89
+
90
+
91
+ def test_backfill_does_not_assign_invalid_typed_relationship_page(db_engine):
92
+ from db.models import Entity, EntityRelationship, Investigation, Page
93
+ from db.session import get_session
94
+ from voidaccess.backfill import backfill_typed_relationship_page_ids
95
+
96
+ now = datetime.datetime.now(datetime.timezone.utc)
97
+ investigation_id = uuid.uuid4()
98
+ page_id = uuid.uuid4()
99
+ source_id, target_id = uuid.uuid4(), uuid.uuid4()
100
+
101
+ with get_session() as session:
102
+ session.add(
103
+ Investigation(
104
+ id=investigation_id,
105
+ query="invalid legacy relationship",
106
+ status="completed",
107
+ created_at=now,
108
+ )
109
+ )
110
+ session.add(Page(id=page_id, url="https://example.test/invalid", scrape_timestamp=now, created_at=now))
111
+ session.add_all(
112
+ [
113
+ Entity(id=source_id, page_id=page_id, investigation_id=investigation_id,
114
+ entity_type="ORGANIZATION_NAME", value="org", canonical_value="org"),
115
+ Entity(id=target_id, page_id=page_id, investigation_id=investigation_id,
116
+ entity_type="IP_ADDRESS", value="192.0.2.1", canonical_value="192.0.2.1"),
117
+ EntityRelationship(entity_a_id=source_id, entity_b_id=target_id,
118
+ relationship_type="TARGETS", investigation_id=investigation_id,
119
+ first_seen=now),
120
+ ]
121
+ )
122
+ session.commit()
123
+ result = backfill_typed_relationship_page_ids(session, investigation_id)
124
+ assert result == {"scanned": 1, "resolved": 0, "unresolved": 1}
@@ -0,0 +1,251 @@
1
+ """One-time repairs for legacy investigation data.
2
+
3
+ This module deliberately does not change the schema. It repairs two pieces of
4
+ derived data that were introduced after some investigations had already been
5
+ stored: the page provenance on typed relationship edges and the persisted
6
+ community partition used by the HTTP graph endpoint.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import logging
14
+ import os
15
+ import uuid
16
+ from collections import defaultdict
17
+ from typing import Any, Optional
18
+
19
+ from sqlalchemy.orm import joinedload
20
+
21
+ from extractor.relationship_extract import _LLM_REL_VOCAB, _is_compatible_relationship
22
+ from graph.builder import build_graph_from_db, detect_communities
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def _as_uuid(value: Any) -> Optional[uuid.UUID]:
28
+ if isinstance(value, uuid.UUID):
29
+ return value
30
+ try:
31
+ return uuid.UUID(str(value)) if value else None
32
+ except (TypeError, ValueError, AttributeError):
33
+ return None
34
+
35
+
36
+ def _metadata_dict(value: Any) -> dict[str, Any]:
37
+ if isinstance(value, dict):
38
+ return dict(value)
39
+ if isinstance(value, str) and value:
40
+ try:
41
+ decoded = json.loads(value)
42
+ except (TypeError, ValueError):
43
+ return {}
44
+ return dict(decoded) if isinstance(decoded, dict) else {}
45
+ return {}
46
+
47
+
48
+ def _validated_entities(session, investigation_id: uuid.UUID):
49
+ """Return the current entity set, including cross-investigation links."""
50
+ from db.models import Entity, InvestigationEntityLink
51
+
52
+ return (
53
+ session.query(Entity)
54
+ .outerjoin(
55
+ InvestigationEntityLink,
56
+ InvestigationEntityLink.entity_id == Entity.id,
57
+ )
58
+ .filter(
59
+ (Entity.investigation_id == investigation_id)
60
+ | (InvestigationEntityLink.investigation_id == investigation_id)
61
+ )
62
+ .options(joinedload(Entity.page))
63
+ .distinct()
64
+ .all()
65
+ )
66
+
67
+
68
+ def _resolve_source_page_id(
69
+ relationship,
70
+ entity_a,
71
+ entity_b,
72
+ validated_entities: list,
73
+ ) -> Optional[uuid.UUID]:
74
+ """Resolve provenance using the live pipeline's validated entity set.
75
+
76
+ Typed relationship direction is meaningful: the live pipeline persists the
77
+ page on which the claim was extracted. For a legacy row without that
78
+ field, the source endpoint's page is the strongest deterministic evidence;
79
+ the target page and a page containing both endpoints are conservative
80
+ fallbacks. No page outside the validated investigation set is accepted.
81
+ """
82
+ if not _is_compatible_relationship(
83
+ str(relationship.relationship_type or ""),
84
+ {"type": entity_a.entity_type},
85
+ {"type": entity_b.entity_type},
86
+ ):
87
+ return None
88
+
89
+ valid_page_ids = {
90
+ ent.page_id for ent in validated_entities if ent.page_id is not None
91
+ }
92
+ if not valid_page_ids:
93
+ return None
94
+
95
+ page_entity_ids: dict[uuid.UUID, set[uuid.UUID]] = defaultdict(set)
96
+ for ent in validated_entities:
97
+ if ent.page_id is not None:
98
+ page_entity_ids[ent.page_id].add(ent.id)
99
+
100
+ # The source endpoint is the direct equivalent of the page_id attached by
101
+ # relationship_extract.extract_relationships_from_results().
102
+ candidates = [
103
+ entity_a.page_id,
104
+ entity_b.page_id,
105
+ ]
106
+ for page_id, entity_ids in page_entity_ids.items():
107
+ if entity_a.id in entity_ids and entity_b.id in entity_ids:
108
+ candidates.append(page_id)
109
+ for page_id, entity_ids in page_entity_ids.items():
110
+ if entity_a.id in entity_ids:
111
+ candidates.append(page_id)
112
+
113
+ seen: set[uuid.UUID] = set()
114
+ for page_id in candidates:
115
+ if page_id is None or page_id in seen:
116
+ continue
117
+ seen.add(page_id)
118
+ if page_id in valid_page_ids:
119
+ return page_id
120
+ return None
121
+
122
+
123
+ def backfill_typed_relationship_page_ids(
124
+ session,
125
+ investigation_id: Optional[uuid.UUID] = None,
126
+ ) -> dict[str, int]:
127
+ """Populate missing ``source_page_id`` values on validated typed edges."""
128
+ from db.models import Entity, EntityRelationship
129
+
130
+ typed_types = set(_LLM_REL_VOCAB.values())
131
+ query = session.query(EntityRelationship).filter(
132
+ EntityRelationship.source_page_id.is_(None),
133
+ EntityRelationship.relationship_type.in_(typed_types),
134
+ )
135
+ if investigation_id is not None:
136
+ query = query.filter(EntityRelationship.investigation_id == investigation_id)
137
+
138
+ rows = query.options(
139
+ joinedload(EntityRelationship.entity_a).joinedload(Entity.page),
140
+ joinedload(EntityRelationship.entity_b).joinedload(Entity.page),
141
+ ).all()
142
+ entities_by_investigation: dict[uuid.UUID, list] = {}
143
+ scanned = resolved = unresolved = 0
144
+
145
+ for relationship in rows:
146
+ scanned += 1
147
+ entity_a = relationship.entity_a
148
+ entity_b = relationship.entity_b
149
+ target_investigation = (
150
+ _as_uuid(relationship.investigation_id)
151
+ or _as_uuid(getattr(entity_a, "investigation_id", None))
152
+ or _as_uuid(getattr(entity_b, "investigation_id", None))
153
+ )
154
+ if target_investigation is None:
155
+ unresolved += 1
156
+ continue
157
+ if target_investigation not in entities_by_investigation:
158
+ entities_by_investigation[target_investigation] = _validated_entities(
159
+ session, target_investigation
160
+ )
161
+ page_id = _resolve_source_page_id(
162
+ relationship,
163
+ entity_a,
164
+ entity_b,
165
+ entities_by_investigation[target_investigation],
166
+ )
167
+ if page_id is None:
168
+ unresolved += 1
169
+ continue
170
+ relationship.source_page_id = page_id
171
+ resolved += 1
172
+
173
+ if resolved:
174
+ session.flush()
175
+ return {"scanned": scanned, "resolved": resolved, "unresolved": unresolved}
176
+
177
+
178
+ def backfill_community_partitions(
179
+ session,
180
+ investigation_id: Optional[uuid.UUID] = None,
181
+ ) -> dict[str, int]:
182
+ """Persist detector output for completed investigations lacking a partition."""
183
+ from db.models import Investigation
184
+
185
+ query = session.query(Investigation).filter(Investigation.status == "completed")
186
+ if investigation_id is not None:
187
+ query = query.filter(Investigation.id == investigation_id)
188
+
189
+ scanned = persisted = skipped = 0
190
+ for investigation in query.order_by(Investigation.created_at).all():
191
+ scanned += 1
192
+ metadata = _metadata_dict(investigation.metadata_json)
193
+ existing = metadata.get("communities")
194
+ if isinstance(existing, dict) and existing:
195
+ skipped += 1
196
+ continue
197
+
198
+ graph = build_graph_from_db(investigation_id=investigation.id)
199
+ partition = {
200
+ str(node_id): int(community_id)
201
+ for node_id, community_id in detect_communities(graph).items()
202
+ }
203
+ metadata["communities"] = partition
204
+ metadata["community_count"] = len(set(partition.values())) if partition else 0
205
+ investigation.metadata_json = metadata
206
+ session.flush()
207
+ persisted += 1
208
+
209
+ return {"scanned": scanned, "persisted": persisted, "skipped": skipped}
210
+
211
+
212
+ def run_backfill(
213
+ session,
214
+ investigation_id: Optional[uuid.UUID] = None,
215
+ ) -> dict[str, dict[str, int]]:
216
+ """Run both repairs in one transaction."""
217
+ typed = backfill_typed_relationship_page_ids(session, investigation_id)
218
+ communities = backfill_community_partitions(session, investigation_id)
219
+ return {"typed_relationship_pages": typed, "community_partitions": communities}
220
+
221
+
222
+ def main(argv: Optional[list[str]] = None) -> int:
223
+ parser = argparse.ArgumentParser(description=__doc__)
224
+ parser.add_argument("--db-path", help="SQLite database path (defaults to VOIDACCESS_DB_PATH)")
225
+ parser.add_argument("--investigation-id", help="Limit the repair to one investigation")
226
+ parser.add_argument("--verbose", action="store_true")
227
+ args = parser.parse_args(argv)
228
+
229
+ if args.verbose:
230
+ logging.basicConfig(level=logging.INFO)
231
+
232
+ db_path = args.db_path or os.getenv("VOIDACCESS_DB_PATH")
233
+ if db_path:
234
+ db_path = os.path.abspath(os.path.expanduser(db_path))
235
+ os.environ["VOIDACCESS_DB_PATH"] = db_path
236
+ os.environ["DATABASE_URL"] = f"sqlite:///{db_path.replace(os.sep, '/') }"
237
+
238
+ target_id = _as_uuid(args.investigation_id)
239
+ if args.investigation_id and target_id is None:
240
+ parser.error("--investigation-id must be a UUID")
241
+
242
+ from db.session import get_session
243
+
244
+ with get_session() as session:
245
+ result = run_backfill(session, target_id)
246
+ print(json.dumps(result, indent=2, sort_keys=True))
247
+ return 0
248
+
249
+
250
+ if __name__ == "__main__":
251
+ raise SystemExit(main())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voidaccess
3
- Version: 2.0.1
3
+ Version: 2.0.3
4
4
  Summary: Dark web OSINT CLI — automated threat intelligence from query to report
5
5
  Author: VoidAccess
6
6
  License-Expression: MIT
@@ -186,6 +186,7 @@ tests/test_i18n.py
186
186
  tests/test_infostealer.py
187
187
  tests/test_ioc_package_export.py
188
188
  tests/test_ip_reputation.py
189
+ tests/test_legacy_backfill.py
189
190
  tests/test_llm.py
190
191
  tests/test_llm_utils.py
191
192
  tests/test_model_singleton.py
@@ -232,6 +233,7 @@ vector/model_singleton.py
232
233
  vector/search.py
233
234
  vector/store.py
234
235
  voidaccess/__init__.py
236
+ voidaccess/backfill.py
235
237
  voidaccess/config.py
236
238
  voidaccess/llm.py
237
239
  voidaccess/llm_utils.py
@@ -1,3 +1,3 @@
1
1
  """voidaccess CLI — dark-web OSINT command-line interface."""
2
2
 
3
- __version__ = "2.0.1"
3
+ __version__ = "2.0.3"