voidaccess 2.0.2__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.2 → voidaccess-2.0.3}/PKG-INFO +1 -1
  2. {voidaccess-2.0.2 → voidaccess-2.0.3}/api/main.py +1 -1
  3. {voidaccess-2.0.2 → voidaccess-2.0.3}/export/ioc_package.py +1 -1
  4. {voidaccess-2.0.2 → voidaccess-2.0.3}/pyproject.toml +1 -1
  5. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_cli_proxy_config.py +33 -0
  6. voidaccess-2.0.3/tests/test_legacy_backfill.py +124 -0
  7. voidaccess-2.0.3/voidaccess/backfill.py +251 -0
  8. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess.egg-info/PKG-INFO +1 -1
  9. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess.egg-info/SOURCES.txt +2 -0
  10. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/__init__.py +1 -1
  11. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/config.py +15 -0
  12. {voidaccess-2.0.2 → voidaccess-2.0.3}/LICENSE +0 -0
  13. {voidaccess-2.0.2 → voidaccess-2.0.3}/README.md +0 -0
  14. {voidaccess-2.0.2 → voidaccess-2.0.3}/alembic.ini +0 -0
  15. {voidaccess-2.0.2 → voidaccess-2.0.3}/analysis/__init__.py +0 -0
  16. {voidaccess-2.0.2 → voidaccess-2.0.3}/analysis/opsec.py +0 -0
  17. {voidaccess-2.0.2 → voidaccess-2.0.3}/analysis/patterns.py +0 -0
  18. {voidaccess-2.0.2 → voidaccess-2.0.3}/analysis/temporal.py +0 -0
  19. {voidaccess-2.0.2 → voidaccess-2.0.3}/api/__init__.py +0 -0
  20. {voidaccess-2.0.2 → voidaccess-2.0.3}/api/auth.py +0 -0
  21. {voidaccess-2.0.2 → voidaccess-2.0.3}/api/errors.py +0 -0
  22. {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/__init__.py +0 -0
  23. {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/actors.py +0 -0
  24. {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/admin.py +0 -0
  25. {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/auth.py +0 -0
  26. {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/entities.py +0 -0
  27. {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/export.py +0 -0
  28. {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/investigations.py +0 -0
  29. {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/monitors.py +0 -0
  30. {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/search.py +0 -0
  31. {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/settings.py +0 -0
  32. {voidaccess-2.0.2 → voidaccess-2.0.3}/auth/__init__.py +0 -0
  33. {voidaccess-2.0.2 → voidaccess-2.0.3}/auth/token_blacklist.py +0 -0
  34. {voidaccess-2.0.2 → voidaccess-2.0.3}/config.py +0 -0
  35. {voidaccess-2.0.2 → voidaccess-2.0.3}/crawler/__init__.py +0 -0
  36. {voidaccess-2.0.2 → voidaccess-2.0.3}/crawler/dedup.py +0 -0
  37. {voidaccess-2.0.2 → voidaccess-2.0.3}/crawler/frontier.py +0 -0
  38. {voidaccess-2.0.2 → voidaccess-2.0.3}/crawler/spider.py +0 -0
  39. {voidaccess-2.0.2 → voidaccess-2.0.3}/crawler/utils.py +0 -0
  40. {voidaccess-2.0.2 → voidaccess-2.0.3}/data/__init__.py +0 -0
  41. {voidaccess-2.0.2 → voidaccess-2.0.3}/data/common_words_en.txt +0 -0
  42. {voidaccess-2.0.2 → voidaccess-2.0.3}/data/domain_stopwords_en.txt +0 -0
  43. {voidaccess-2.0.2 → voidaccess-2.0.3}/data/monitors.yaml +0 -0
  44. {voidaccess-2.0.2 → voidaccess-2.0.3}/data/seed_data.json.gz +0 -0
  45. {voidaccess-2.0.2 → voidaccess-2.0.3}/data/threat_gazetteer.json +0 -0
  46. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/__init__.py +0 -0
  47. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/__init__.py +0 -0
  48. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/env.py +0 -0
  49. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0001_initial_schema.py +0 -0
  50. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0002_add_investigation_status_column.py +0 -0
  51. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0002_add_missing_tables.py +0 -0
  52. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0003_add_canonical_value_and_entity_links.py +0 -0
  53. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0004_add_page_posted_at.py +0 -0
  54. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0005_add_extraction_method.py +0 -0
  55. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0006_add_monitor_alerts.py +0 -0
  56. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0007_add_actor_style_profiles.py +0 -0
  57. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0008_add_users_table.py +0 -0
  58. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0009_add_investigation_id_to_relationships.py +0 -0
  59. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0010_add_composite_index_entity_relationships.py +0 -0
  60. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0011_add_page_extraction_cache.py +0 -0
  61. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0013_add_graph_status.py +0 -0
  62. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0015_add_progress_fields.py +0 -0
  63. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0016_backfill_graph_status.py +0 -0
  64. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0017_add_user_api_keys.py +0 -0
  65. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0018_add_user_id_to_investigations.py +0 -0
  66. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0019_add_content_safety_log.py +0 -0
  67. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0020_add_entity_source_tracking.py +0 -0
  68. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0021_add_search_engine_stats.py +0 -0
  69. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0022_add_actor_profiles.py +0 -0
  70. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0023_add_investigation_metadata.py +0 -0
  71. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0024_add_investigation_step_metrics.py +0 -0
  72. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0025_add_cancellation_requested.py +0 -0
  73. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0026_add_entity_investigation_count.py +0 -0
  74. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/models.py +0 -0
  75. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/queries.py +0 -0
  76. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/search_engine_stats.py +0 -0
  77. {voidaccess-2.0.2 → voidaccess-2.0.3}/db/session.py +0 -0
  78. {voidaccess-2.0.2 → voidaccess-2.0.3}/export/__init__.py +0 -0
  79. {voidaccess-2.0.2 → voidaccess-2.0.3}/export/_entity_loading.py +0 -0
  80. {voidaccess-2.0.2 → voidaccess-2.0.3}/export/misp.py +0 -0
  81. {voidaccess-2.0.2 → voidaccess-2.0.3}/export/relationships.py +0 -0
  82. {voidaccess-2.0.2 → voidaccess-2.0.3}/export/sigma.py +0 -0
  83. {voidaccess-2.0.2 → voidaccess-2.0.3}/export/snort_export.py +0 -0
  84. {voidaccess-2.0.2 → voidaccess-2.0.3}/export/stix.py +0 -0
  85. {voidaccess-2.0.2 → voidaccess-2.0.3}/export/yara_export.py +0 -0
  86. {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/__init__.py +0 -0
  87. {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/confidence.py +0 -0
  88. {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/dependency_relationship.py +0 -0
  89. {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/entity_shape.py +0 -0
  90. {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/gazetteer.py +0 -0
  91. {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/identity.py +0 -0
  92. {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/llm_extract.py +0 -0
  93. {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/ner.py +0 -0
  94. {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/normalizer.py +0 -0
  95. {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/pipeline.py +0 -0
  96. {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/regex_patterns.py +0 -0
  97. {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/relationship_extract.py +0 -0
  98. {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/software_suppression.py +0 -0
  99. {voidaccess-2.0.2 → voidaccess-2.0.3}/fingerprint/__init__.py +0 -0
  100. {voidaccess-2.0.2 → voidaccess-2.0.3}/fingerprint/calibration.py +0 -0
  101. {voidaccess-2.0.2 → voidaccess-2.0.3}/fingerprint/profiler.py +0 -0
  102. {voidaccess-2.0.2 → voidaccess-2.0.3}/fingerprint/stylometry.py +0 -0
  103. {voidaccess-2.0.2 → voidaccess-2.0.3}/graph/__init__.py +0 -0
  104. {voidaccess-2.0.2 → voidaccess-2.0.3}/graph/builder.py +0 -0
  105. {voidaccess-2.0.2 → voidaccess-2.0.3}/graph/export.py +0 -0
  106. {voidaccess-2.0.2 → voidaccess-2.0.3}/graph/model.py +0 -0
  107. {voidaccess-2.0.2 → voidaccess-2.0.3}/graph/queries.py +0 -0
  108. {voidaccess-2.0.2 → voidaccess-2.0.3}/graph/visualize.py +0 -0
  109. {voidaccess-2.0.2 → voidaccess-2.0.3}/i18n/__init__.py +0 -0
  110. {voidaccess-2.0.2 → voidaccess-2.0.3}/i18n/detect.py +0 -0
  111. {voidaccess-2.0.2 → voidaccess-2.0.3}/i18n/query_expand.py +0 -0
  112. {voidaccess-2.0.2 → voidaccess-2.0.3}/i18n/translate.py +0 -0
  113. {voidaccess-2.0.2 → voidaccess-2.0.3}/monitor/__init__.py +0 -0
  114. {voidaccess-2.0.2 → voidaccess-2.0.3}/monitor/_db.py +0 -0
  115. {voidaccess-2.0.2 → voidaccess-2.0.3}/monitor/alerts.py +0 -0
  116. {voidaccess-2.0.2 → voidaccess-2.0.3}/monitor/config.py +0 -0
  117. {voidaccess-2.0.2 → voidaccess-2.0.3}/monitor/diff.py +0 -0
  118. {voidaccess-2.0.2 → voidaccess-2.0.3}/monitor/jobs.py +0 -0
  119. {voidaccess-2.0.2 → voidaccess-2.0.3}/monitor/scheduler.py +0 -0
  120. {voidaccess-2.0.2 → voidaccess-2.0.3}/pacing/__init__.py +0 -0
  121. {voidaccess-2.0.2 → voidaccess-2.0.3}/scraper/__init__.py +0 -0
  122. {voidaccess-2.0.2 → voidaccess-2.0.3}/scraper/scrape.py +0 -0
  123. {voidaccess-2.0.2 → voidaccess-2.0.3}/scraper/scrape_js.py +0 -0
  124. {voidaccess-2.0.2 → voidaccess-2.0.3}/scraper/tor_pool.py +0 -0
  125. {voidaccess-2.0.2 → voidaccess-2.0.3}/search/__init__.py +0 -0
  126. {voidaccess-2.0.2 → voidaccess-2.0.3}/search/circuit_breaker.py +0 -0
  127. {voidaccess-2.0.2 → voidaccess-2.0.3}/search/config.py +0 -0
  128. {voidaccess-2.0.2 → voidaccess-2.0.3}/search/engine_catalog.py +0 -0
  129. {voidaccess-2.0.2 → voidaccess-2.0.3}/search/query_builder.py +0 -0
  130. {voidaccess-2.0.2 → voidaccess-2.0.3}/search/search.py +0 -0
  131. {voidaccess-2.0.2 → voidaccess-2.0.3}/search/tor_isolation.py +0 -0
  132. {voidaccess-2.0.2 → voidaccess-2.0.3}/setup.cfg +0 -0
  133. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/__init__.py +0 -0
  134. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/actor_profiles.py +0 -0
  135. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/blockchain.py +0 -0
  136. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/breach_lookup.py +0 -0
  137. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/cache.py +0 -0
  138. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/cisa.py +0 -0
  139. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/data/__init__.py +0 -0
  140. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/data/onion_seeds.json +0 -0
  141. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/dns_enrichment.py +0 -0
  142. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/domain_reputation.py +0 -0
  143. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/email_reputation.py +0 -0
  144. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/engines.py +0 -0
  145. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/enrichment.py +0 -0
  146. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/github_scraper.py +0 -0
  147. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/gitlab_scraper.py +0 -0
  148. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/hash_reputation.py +0 -0
  149. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/historical_intel.py +0 -0
  150. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/infostealer.py +0 -0
  151. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/ip_reputation.py +0 -0
  152. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/nvd.py +0 -0
  153. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/paste_scraper.py +0 -0
  154. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/pastes.py +0 -0
  155. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/proxy_client.py +0 -0
  156. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/rss_scraper.py +0 -0
  157. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/seed_manager.py +0 -0
  158. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/seeds.py +0 -0
  159. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/shodan.py +0 -0
  160. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/telegram.py +0 -0
  161. {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/virustotal.py +0 -0
  162. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_actor_extraction_gap.py +0 -0
  163. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_analysis_opsec.py +0 -0
  164. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_analysis_stylometry.py +0 -0
  165. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_analysis_temporal.py +0 -0
  166. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_api.py +0 -0
  167. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_api_monitors.py +0 -0
  168. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_blockchain.py +0 -0
  169. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_breach_lookup.py +0 -0
  170. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_cli_manifest.py +0 -0
  171. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_community_detection_parity.py +0 -0
  172. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_confidence_calibration.py +0 -0
  173. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_config.py +0 -0
  174. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_consolidated_contracts.py +0 -0
  175. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_crawler.py +0 -0
  176. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_db.py +0 -0
  177. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_dependency_relationship.py +0 -0
  178. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_dns_enrichment.py +0 -0
  179. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_domain_reputation.py +0 -0
  180. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_email_reputation.py +0 -0
  181. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_entity_priority.py +0 -0
  182. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_extractor_llm_pipeline.py +0 -0
  183. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_filter_e2e_manual.py +0 -0
  184. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_filter_parser_manual.py +0 -0
  185. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_fingerprint.py +0 -0
  186. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_gazetteer.py +0 -0
  187. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_github_scraper.py +0 -0
  188. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_gitlab_scraper.py +0 -0
  189. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_graph.py +0 -0
  190. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_graph_integration.py +0 -0
  191. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_hash_reputation.py +0 -0
  192. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_heuristic_expansion.py +0 -0
  193. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_i18n.py +0 -0
  194. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_infostealer.py +0 -0
  195. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_ioc_package_export.py +0 -0
  196. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_ip_reputation.py +0 -0
  197. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_llm.py +0 -0
  198. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_llm_utils.py +0 -0
  199. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_model_singleton.py +0 -0
  200. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_monitor.py +0 -0
  201. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_monitor_integration.py +0 -0
  202. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_nvd.py +0 -0
  203. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_pacing.py +0 -0
  204. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_pacing_enrichment.py +0 -0
  205. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_pacing_quotas.py +0 -0
  206. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_pagination.py +0 -0
  207. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_paste_scraper.py +0 -0
  208. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_priority_score_cli_api_parity.py +0 -0
  209. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_proxy_client.py +0 -0
  210. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_ransomlook.py +0 -0
  211. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_regex_patterns.py +0 -0
  212. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_relationship_extract.py +0 -0
  213. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_requirement_bugs.py +0 -0
  214. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_rss_scraper.py +0 -0
  215. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_scrape_isolation.py +0 -0
  216. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_scrape_js.py +0 -0
  217. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_settings.py +0 -0
  218. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_sigma_export.py +0 -0
  219. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_snort_export.py +0 -0
  220. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_software_suppression.py +0 -0
  221. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_sources.py +0 -0
  222. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_sources_enrichment_new.py +0 -0
  223. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_stix_export.py +0 -0
  224. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_vector.py +0 -0
  225. {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_yara_export.py +0 -0
  226. {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/__init__.py +0 -0
  227. {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/async_utils.py +0 -0
  228. {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/content_dedup.py +0 -0
  229. {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/content_safety.py +0 -0
  230. {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/defang.py +0 -0
  231. {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/encryption.py +0 -0
  232. {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/enrichment_cache.py +0 -0
  233. {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/entity_priority.py +0 -0
  234. {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/investigation_metrics.py +0 -0
  235. {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/ioc_freshness.py +0 -0
  236. {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/user_keys.py +0 -0
  237. {voidaccess-2.0.2 → voidaccess-2.0.3}/vector/__init__.py +0 -0
  238. {voidaccess-2.0.2 → voidaccess-2.0.3}/vector/embedder.py +0 -0
  239. {voidaccess-2.0.2 → voidaccess-2.0.3}/vector/model_singleton.py +0 -0
  240. {voidaccess-2.0.2 → voidaccess-2.0.3}/vector/search.py +0 -0
  241. {voidaccess-2.0.2 → voidaccess-2.0.3}/vector/store.py +0 -0
  242. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess/__init__.py +0 -0
  243. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess/config.py +0 -0
  244. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess/llm.py +0 -0
  245. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess/llm_utils.py +0 -0
  246. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess.egg-info/dependency_links.txt +0 -0
  247. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess.egg-info/entry_points.txt +0 -0
  248. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess.egg-info/requires.txt +0 -0
  249. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess.egg-info/top_level.txt +0 -0
  250. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/adapters/__init__.py +0 -0
  251. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/adapters/sqlite.py +0 -0
  252. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/browser.py +0 -0
  253. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/commands/__init__.py +0 -0
  254. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/commands/actors.py +0 -0
  255. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/commands/configure.py +0 -0
  256. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/commands/enrich.py +0 -0
  257. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/commands/export.py +0 -0
  258. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/commands/investigate.py +0 -0
  259. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/commands/show.py +0 -0
  260. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/display.py +0 -0
  261. {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/main.py +0 -0
  262. {voidaccess-2.0.2 → 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.2
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.2",
335
+ version="2.0.3",
336
336
  docs_url="/docs",
337
337
  redoc_url="/redoc",
338
338
  lifespan=lifespan,
@@ -57,7 +57,7 @@ logger = logging.getLogger(__name__)
57
57
  # ---------------------------------------------------------------------------
58
58
 
59
59
  PACKAGE_FORMAT = "voidaccess-ioc-v1"
60
- PACKAGE_VERSION = "2.0.2"
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.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "voidaccess"
7
- version = "2.0.2"
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"
@@ -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.2
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.2"
3
+ __version__ = "2.0.3"
@@ -20,6 +20,7 @@ _WARNED_CONFIG_KEYS: set[str] = set()
20
20
 
21
21
  CLI_HOME = Path(os.path.expanduser("~/.voidaccess"))
22
22
  CONFIG_PATH = CLI_HOME / "config.json"
23
+ ENV_PATH = Path.cwd() / ".env"
23
24
  DB_PATH = CLI_HOME / "investigations.db"
24
25
  DEFAULT_OUTPUT_DIR = CLI_HOME / "results"
25
26
 
@@ -248,6 +249,19 @@ def _lock_file(path: Path) -> None:
248
249
  pass
249
250
 
250
251
 
252
+ def lock_env_file() -> None:
253
+ """Apply the same owner-only protection to the project ``.env`` file.
254
+
255
+ ``voidaccess configure`` stores its CLI settings separately in
256
+ ``~/.voidaccess/config.json``, but a self-hosted checkout may also have a
257
+ project-level ``.env`` containing the API/database credentials consumed by
258
+ the runtime. Lock it whenever configuration is saved, while keeping the
259
+ operation a no-op for CLI-only installs that have no ``.env``.
260
+ """
261
+ if ENV_PATH.is_file():
262
+ _lock_file(ENV_PATH)
263
+
264
+
251
265
  def save_config(config: dict[str, Any]) -> None:
252
266
  features = config.setdefault("features", {})
253
267
  # Keep the legacy aliases readable for older clients. When a caller
@@ -271,6 +285,7 @@ def save_config(config: dict[str, Any]) -> None:
271
285
  encoding="utf-8",
272
286
  )
273
287
  _lock_file(CONFIG_PATH)
288
+ lock_env_file()
274
289
 
275
290
 
276
291
  def is_configured() -> bool:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes