voidaccess 1.6.2__tar.gz → 1.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. {voidaccess-1.6.2/voidaccess.egg-info → voidaccess-1.7.0}/PKG-INFO +44 -4
  2. {voidaccess-1.6.2 → voidaccess-1.7.0}/README.md +8 -7
  3. {voidaccess-1.6.2 → voidaccess-1.7.0}/export/ioc_package.py +20 -2
  4. {voidaccess-1.6.2 → voidaccess-1.7.0}/export/stix.py +30 -7
  5. {voidaccess-1.6.2 → voidaccess-1.7.0}/extractor/llm_extract.py +2 -27
  6. {voidaccess-1.6.2 → voidaccess-1.7.0}/extractor/normalizer.py +165 -2
  7. {voidaccess-1.6.2 → voidaccess-1.7.0}/extractor/pipeline.py +26 -6
  8. {voidaccess-1.6.2 → voidaccess-1.7.0}/pyproject.toml +43 -2
  9. {voidaccess-1.6.2 → voidaccess-1.7.0}/scraper/scrape.py +14 -2
  10. voidaccess-1.7.0/sources/data/onion_seeds.json +513 -0
  11. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/proxy_client.py +4 -4
  12. voidaccess-1.7.0/tests/test_stix_export.py +19 -0
  13. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess/llm.py +9 -13
  14. {voidaccess-1.6.2 → voidaccess-1.7.0/voidaccess.egg-info}/PKG-INFO +44 -4
  15. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess.egg-info/SOURCES.txt +1 -0
  16. voidaccess-1.7.0/voidaccess.egg-info/requires.txt +71 -0
  17. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess_cli/__init__.py +1 -1
  18. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess_cli/adapters/sqlite.py +6 -3
  19. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess_cli/commands/configure.py +42 -13
  20. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess_cli/commands/export.py +6 -0
  21. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess_cli/config.py +81 -9
  22. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess_cli/main.py +21 -5
  23. voidaccess-1.6.2/sources/data/onion_seeds.json +0 -272
  24. voidaccess-1.6.2/voidaccess.egg-info/requires.txt +0 -27
  25. {voidaccess-1.6.2 → voidaccess-1.7.0}/LICENSE +0 -0
  26. {voidaccess-1.6.2 → voidaccess-1.7.0}/analysis/__init__.py +0 -0
  27. {voidaccess-1.6.2 → voidaccess-1.7.0}/analysis/opsec.py +0 -0
  28. {voidaccess-1.6.2 → voidaccess-1.7.0}/analysis/patterns.py +0 -0
  29. {voidaccess-1.6.2 → voidaccess-1.7.0}/analysis/temporal.py +0 -0
  30. {voidaccess-1.6.2 → voidaccess-1.7.0}/api/__init__.py +0 -0
  31. {voidaccess-1.6.2 → voidaccess-1.7.0}/api/auth.py +0 -0
  32. {voidaccess-1.6.2 → voidaccess-1.7.0}/api/main.py +0 -0
  33. {voidaccess-1.6.2 → voidaccess-1.7.0}/api/routes/__init__.py +0 -0
  34. {voidaccess-1.6.2 → voidaccess-1.7.0}/api/routes/actors.py +0 -0
  35. {voidaccess-1.6.2 → voidaccess-1.7.0}/api/routes/admin.py +0 -0
  36. {voidaccess-1.6.2 → voidaccess-1.7.0}/api/routes/auth.py +0 -0
  37. {voidaccess-1.6.2 → voidaccess-1.7.0}/api/routes/entities.py +0 -0
  38. {voidaccess-1.6.2 → voidaccess-1.7.0}/api/routes/export.py +0 -0
  39. {voidaccess-1.6.2 → voidaccess-1.7.0}/api/routes/investigations.py +0 -0
  40. {voidaccess-1.6.2 → voidaccess-1.7.0}/api/routes/monitors.py +0 -0
  41. {voidaccess-1.6.2 → voidaccess-1.7.0}/api/routes/search.py +0 -0
  42. {voidaccess-1.6.2 → voidaccess-1.7.0}/api/routes/settings.py +0 -0
  43. {voidaccess-1.6.2 → voidaccess-1.7.0}/auth/__init__.py +0 -0
  44. {voidaccess-1.6.2 → voidaccess-1.7.0}/auth/token_blacklist.py +0 -0
  45. {voidaccess-1.6.2 → voidaccess-1.7.0}/config.py +0 -0
  46. {voidaccess-1.6.2 → voidaccess-1.7.0}/crawler/__init__.py +0 -0
  47. {voidaccess-1.6.2 → voidaccess-1.7.0}/crawler/dedup.py +0 -0
  48. {voidaccess-1.6.2 → voidaccess-1.7.0}/crawler/frontier.py +0 -0
  49. {voidaccess-1.6.2 → voidaccess-1.7.0}/crawler/spider.py +0 -0
  50. {voidaccess-1.6.2 → voidaccess-1.7.0}/crawler/utils.py +0 -0
  51. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/__init__.py +0 -0
  52. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/__init__.py +0 -0
  53. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/env.py +0 -0
  54. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0001_initial_schema.py +0 -0
  55. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0002_add_investigation_status_column.py +0 -0
  56. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0002_add_missing_tables.py +0 -0
  57. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0003_add_canonical_value_and_entity_links.py +0 -0
  58. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0004_add_page_posted_at.py +0 -0
  59. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0005_add_extraction_method.py +0 -0
  60. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0006_add_monitor_alerts.py +0 -0
  61. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0007_add_actor_style_profiles.py +0 -0
  62. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0008_add_users_table.py +0 -0
  63. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0009_add_investigation_id_to_relationships.py +0 -0
  64. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0010_add_composite_index_entity_relationships.py +0 -0
  65. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0011_add_page_extraction_cache.py +0 -0
  66. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0013_add_graph_status.py +0 -0
  67. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0015_add_progress_fields.py +0 -0
  68. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0016_backfill_graph_status.py +0 -0
  69. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0017_add_user_api_keys.py +0 -0
  70. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0018_add_user_id_to_investigations.py +0 -0
  71. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0019_add_content_safety_log.py +0 -0
  72. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0020_add_entity_source_tracking.py +0 -0
  73. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0021_add_search_engine_stats.py +0 -0
  74. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0022_add_actor_profiles.py +0 -0
  75. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/migrations/versions/0023_add_investigation_metadata.py +0 -0
  76. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/models.py +0 -0
  77. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/queries.py +0 -0
  78. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/search_engine_stats.py +0 -0
  79. {voidaccess-1.6.2 → voidaccess-1.7.0}/db/session.py +0 -0
  80. {voidaccess-1.6.2 → voidaccess-1.7.0}/export/__init__.py +0 -0
  81. {voidaccess-1.6.2 → voidaccess-1.7.0}/export/misp.py +0 -0
  82. {voidaccess-1.6.2 → voidaccess-1.7.0}/export/sigma.py +0 -0
  83. {voidaccess-1.6.2 → voidaccess-1.7.0}/export/snort_export.py +0 -0
  84. {voidaccess-1.6.2 → voidaccess-1.7.0}/export/yara_export.py +0 -0
  85. {voidaccess-1.6.2 → voidaccess-1.7.0}/extractor/__init__.py +0 -0
  86. {voidaccess-1.6.2 → voidaccess-1.7.0}/extractor/ner.py +0 -0
  87. {voidaccess-1.6.2 → voidaccess-1.7.0}/extractor/regex_patterns.py +0 -0
  88. {voidaccess-1.6.2 → voidaccess-1.7.0}/fingerprint/__init__.py +0 -0
  89. {voidaccess-1.6.2 → voidaccess-1.7.0}/fingerprint/profiler.py +0 -0
  90. {voidaccess-1.6.2 → voidaccess-1.7.0}/fingerprint/stylometry.py +0 -0
  91. {voidaccess-1.6.2 → voidaccess-1.7.0}/graph/__init__.py +0 -0
  92. {voidaccess-1.6.2 → voidaccess-1.7.0}/graph/builder.py +0 -0
  93. {voidaccess-1.6.2 → voidaccess-1.7.0}/graph/export.py +0 -0
  94. {voidaccess-1.6.2 → voidaccess-1.7.0}/graph/model.py +0 -0
  95. {voidaccess-1.6.2 → voidaccess-1.7.0}/graph/queries.py +0 -0
  96. {voidaccess-1.6.2 → voidaccess-1.7.0}/graph/visualize.py +0 -0
  97. {voidaccess-1.6.2 → voidaccess-1.7.0}/i18n/__init__.py +0 -0
  98. {voidaccess-1.6.2 → voidaccess-1.7.0}/i18n/detect.py +0 -0
  99. {voidaccess-1.6.2 → voidaccess-1.7.0}/i18n/query_expand.py +0 -0
  100. {voidaccess-1.6.2 → voidaccess-1.7.0}/i18n/translate.py +0 -0
  101. {voidaccess-1.6.2 → voidaccess-1.7.0}/monitor/__init__.py +0 -0
  102. {voidaccess-1.6.2 → voidaccess-1.7.0}/monitor/_db.py +0 -0
  103. {voidaccess-1.6.2 → voidaccess-1.7.0}/monitor/alerts.py +0 -0
  104. {voidaccess-1.6.2 → voidaccess-1.7.0}/monitor/config.py +0 -0
  105. {voidaccess-1.6.2 → voidaccess-1.7.0}/monitor/diff.py +0 -0
  106. {voidaccess-1.6.2 → voidaccess-1.7.0}/monitor/jobs.py +0 -0
  107. {voidaccess-1.6.2 → voidaccess-1.7.0}/monitor/scheduler.py +0 -0
  108. {voidaccess-1.6.2 → voidaccess-1.7.0}/scraper/__init__.py +0 -0
  109. {voidaccess-1.6.2 → voidaccess-1.7.0}/scraper/scrape_js.py +0 -0
  110. {voidaccess-1.6.2 → voidaccess-1.7.0}/search/__init__.py +0 -0
  111. {voidaccess-1.6.2 → voidaccess-1.7.0}/search/circuit_breaker.py +0 -0
  112. {voidaccess-1.6.2 → voidaccess-1.7.0}/search/query_builder.py +0 -0
  113. {voidaccess-1.6.2 → voidaccess-1.7.0}/search/search.py +0 -0
  114. {voidaccess-1.6.2 → voidaccess-1.7.0}/setup.cfg +0 -0
  115. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/__init__.py +0 -0
  116. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/actor_profiles.py +0 -0
  117. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/blockchain.py +0 -0
  118. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/cache.py +0 -0
  119. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/cisa.py +0 -0
  120. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/data/__init__.py +0 -0
  121. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/dns_enrichment.py +0 -0
  122. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/domain_reputation.py +0 -0
  123. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/email_reputation.py +0 -0
  124. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/engines.py +0 -0
  125. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/enrichment.py +0 -0
  126. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/github_scraper.py +0 -0
  127. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/gitlab_scraper.py +0 -0
  128. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/hash_reputation.py +0 -0
  129. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/historical_intel.py +0 -0
  130. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/ip_reputation.py +0 -0
  131. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/paste_scraper.py +0 -0
  132. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/pastes.py +0 -0
  133. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/rss_scraper.py +0 -0
  134. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/seed_manager.py +0 -0
  135. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/seeds.py +0 -0
  136. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/shodan.py +0 -0
  137. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/telegram.py +0 -0
  138. {voidaccess-1.6.2 → voidaccess-1.7.0}/sources/virustotal.py +0 -0
  139. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_analysis_opsec.py +0 -0
  140. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_analysis_stylometry.py +0 -0
  141. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_analysis_temporal.py +0 -0
  142. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_api.py +0 -0
  143. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_api_monitors.py +0 -0
  144. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_blockchain.py +0 -0
  145. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_cli_proxy_config.py +0 -0
  146. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_config.py +0 -0
  147. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_crawler.py +0 -0
  148. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_db.py +0 -0
  149. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_dns_enrichment.py +0 -0
  150. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_domain_reputation.py +0 -0
  151. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_email_reputation.py +0 -0
  152. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_filter_e2e_manual.py +0 -0
  153. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_filter_parser_manual.py +0 -0
  154. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_fingerprint.py +0 -0
  155. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_github_scraper.py +0 -0
  156. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_gitlab_scraper.py +0 -0
  157. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_graph.py +0 -0
  158. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_hash_reputation.py +0 -0
  159. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_i18n.py +0 -0
  160. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_ip_reputation.py +0 -0
  161. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_llm.py +0 -0
  162. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_llm_utils.py +0 -0
  163. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_model_singleton.py +0 -0
  164. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_monitor.py +0 -0
  165. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_pagination.py +0 -0
  166. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_paste_scraper.py +0 -0
  167. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_proxy_client.py +0 -0
  168. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_regex_patterns.py +0 -0
  169. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_rss_scraper.py +0 -0
  170. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_scrape_js.py +0 -0
  171. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_settings.py +0 -0
  172. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_snort_export.py +0 -0
  173. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_sources.py +0 -0
  174. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_sources_enrichment_new.py +0 -0
  175. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_vector.py +0 -0
  176. {voidaccess-1.6.2 → voidaccess-1.7.0}/tests/test_yara_export.py +0 -0
  177. {voidaccess-1.6.2 → voidaccess-1.7.0}/utils/__init__.py +0 -0
  178. {voidaccess-1.6.2 → voidaccess-1.7.0}/utils/async_utils.py +0 -0
  179. {voidaccess-1.6.2 → voidaccess-1.7.0}/utils/content_safety.py +0 -0
  180. {voidaccess-1.6.2 → voidaccess-1.7.0}/utils/defang.py +0 -0
  181. {voidaccess-1.6.2 → voidaccess-1.7.0}/utils/encryption.py +0 -0
  182. {voidaccess-1.6.2 → voidaccess-1.7.0}/utils/enrichment_cache.py +0 -0
  183. {voidaccess-1.6.2 → voidaccess-1.7.0}/utils/ioc_freshness.py +0 -0
  184. {voidaccess-1.6.2 → voidaccess-1.7.0}/utils/user_keys.py +0 -0
  185. {voidaccess-1.6.2 → voidaccess-1.7.0}/vector/__init__.py +0 -0
  186. {voidaccess-1.6.2 → voidaccess-1.7.0}/vector/embedder.py +0 -0
  187. {voidaccess-1.6.2 → voidaccess-1.7.0}/vector/model_singleton.py +0 -0
  188. {voidaccess-1.6.2 → voidaccess-1.7.0}/vector/search.py +0 -0
  189. {voidaccess-1.6.2 → voidaccess-1.7.0}/vector/store.py +0 -0
  190. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess/__init__.py +0 -0
  191. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess/config.py +0 -0
  192. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess/llm_utils.py +0 -0
  193. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess.egg-info/dependency_links.txt +0 -0
  194. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess.egg-info/entry_points.txt +0 -0
  195. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess.egg-info/top_level.txt +0 -0
  196. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess_cli/adapters/__init__.py +0 -0
  197. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess_cli/browser.py +0 -0
  198. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess_cli/commands/__init__.py +0 -0
  199. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess_cli/commands/actors.py +0 -0
  200. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess_cli/commands/enrich.py +0 -0
  201. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess_cli/commands/investigate.py +0 -0
  202. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess_cli/commands/show.py +0 -0
  203. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess_cli/display.py +0 -0
  204. {voidaccess-1.6.2 → voidaccess-1.7.0}/voidaccess_cli/tor_detect.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voidaccess
3
- Version: 1.6.2
3
+ Version: 1.7.0
4
4
  Summary: Dark web OSINT CLI — automated threat intelligence from query to report
5
5
  Author: VoidAccess
6
6
  License-Expression: MIT
@@ -19,10 +19,17 @@ License-File: LICENSE
19
19
  Requires-Dist: typer>=0.12
20
20
  Requires-Dist: rich>=13
21
21
  Requires-Dist: textual>=0.60
22
+ Requires-Dist: click>=8.0
23
+ Requires-Dist: fastapi>=0.115
22
24
  Requires-Dist: aiohttp>=3.9
23
25
  Requires-Dist: aiohttp-socks>=0.8
26
+ Requires-Dist: alembic>=1.13
27
+ Requires-Dist: apscheduler>=3.10
28
+ Requires-Dist: pillow>=10.0
24
29
  Requires-Dist: sqlalchemy>=2.0
25
30
  Requires-Dist: aiosqlite>=0.20
31
+ Requires-Dist: filelock>=3.14
32
+ Requires-Dist: numpy>=1.26
26
33
  Requires-Dist: langchain>=0.2
27
34
  Requires-Dist: langchain-openai>=0.1
28
35
  Requires-Dist: langchain-anthropic>=0.1
@@ -34,14 +41,46 @@ Requires-Dist: httpx>=0.27
34
41
  Requires-Dist: spacy>=3.7
35
42
  Requires-Dist: beautifulsoup4>=4.12
36
43
  Requires-Dist: feedparser>=6.0
44
+ Requires-Dist: networkx>=3.0
45
+ Requires-Dist: pydantic>=2.7
46
+ Requires-Dist: python-jose[cryptography]>=3.3
47
+ Requires-Dist: passlib[bcrypt]>=1.7
48
+ Requires-Dist: slowapi>=0.1.9
49
+ Requires-Dist: redis>=5.0
50
+ Requires-Dist: pyyaml>=6.0
51
+ Requires-Dist: openai>=1.30
52
+ Requires-Dist: cryptography>=42.0
37
53
  Requires-Dist: python-dateutil>=2.9
54
+ Requires-Dist: langdetect>=1.0
55
+ Requires-Dist: pyvis>=0.3
56
+ Requires-Dist: urllib3>=2.2
38
57
  Requires-Dist: trafilatura>=1.6
39
58
  Requires-Dist: requests>=2.31
40
59
  Requires-Dist: python-socks>=2.4
41
60
  Requires-Dist: tldextract>=5.1
61
+ Requires-Dist: stix2>=3.0.2
42
62
  Provides-Extra: dev
43
63
  Requires-Dist: pytest; extra == "dev"
44
64
  Requires-Dist: pytest-asyncio; extra == "dev"
65
+ Provides-Extra: nlp
66
+ Requires-Dist: sentence-transformers>=3.0; extra == "nlp"
67
+ Requires-Dist: torch>=2.3; extra == "nlp"
68
+ Requires-Dist: transformers>=4.42; extra == "nlp"
69
+ Provides-Extra: vector
70
+ Requires-Dist: chromadb>=0.5; extra == "vector"
71
+ Requires-Dist: sentence-transformers>=3.0; extra == "vector"
72
+ Requires-Dist: torch>=2.3; extra == "vector"
73
+ Provides-Extra: telegram
74
+ Requires-Dist: telethon>=1.36; extra == "telegram"
75
+ Provides-Extra: js
76
+ Requires-Dist: playwright>=1.45; extra == "js"
77
+ Provides-Extra: all
78
+ Requires-Dist: chromadb>=0.5; extra == "all"
79
+ Requires-Dist: playwright>=1.45; extra == "all"
80
+ Requires-Dist: sentence-transformers>=3.0; extra == "all"
81
+ Requires-Dist: telethon>=1.36; extra == "all"
82
+ Requires-Dist: torch>=2.3; extra == "all"
83
+ Requires-Dist: transformers>=4.42; extra == "all"
45
84
  Dynamic: license-file
46
85
 
47
86
  <div align="center">
@@ -65,8 +104,9 @@ Commercial threat intelligence platforms often charge prohibitive annual fees fo
65
104
 
66
105
  ---
67
106
 
68
- ## What's New in v1.6.0
107
+ ## What's New in v1.7.0
69
108
 
109
+ - **Dependency optional-extras split** — `sentence-transformers`, `torch`, `transformers`, `telethon`, and `playwright` are now optional extras: `voidaccess[nlp]`, `voidaccess[telegram]`, `voidaccess[js]`, and `voidaccess[all]`.
70
110
  - **Optional clearnet ScrapingAnt integration** — paste sites and RSS feeds can now be routed through ScrapingAnt. Affects clearnet scraping only; Tor, `.onion`, GitHub, and GitLab traffic are never affected.
71
111
  - **Three independent products** — Web Scraping API, Residential Proxy transport, and Datacenter Proxy transport are documented separately so the API key is not conflated with the proxy credentials.
72
112
  - **One-shot CLI flags** — `voidaccess investigate --use-scraping-api` and `voidaccess investigate --use-proxies` select the REST API or proxy transport for a single run.
@@ -76,7 +116,7 @@ Commercial threat intelligence platforms often charge prohibitive annual fees fo
76
116
 
77
117
  ---
78
118
 
79
- ## What's New in v1.5.0
119
+ ## What's New in v1.6.4
80
120
 
81
121
  - 37 new entity types across crypto, credentials, messaging, and network/forensic indicators.
82
122
  - YARA, Snort, Suricata, and IOC package ZIP exports.
@@ -270,7 +310,7 @@ Export formats:
270
310
 
271
311
  ## Actor Intelligence
272
312
 
273
- VoidAccess v1.5.0 persists actor profiles across investigations in `actor_profiles`, with linked aliases and infrastructure in `actor_aliases` and `actor_infrastructure`. Profiles are populated from threat actor, ransomware group, and handle entities, then enriched with co-occurring infrastructure and timeline events.
313
+ VoidAccess v1.7.0 persists actor profiles across investigations in `actor_profiles`, with linked aliases and infrastructure in `actor_aliases` and `actor_infrastructure`. Profiles are populated from threat actor, ransomware group, and handle entities, then enriched with co-occurring infrastructure and timeline events.
274
314
 
275
315
  Cross-alias resolution scores five signals: shared infrastructure, shared PGP, string similarity, temporal co-activity, and co-investigation. Use `voidaccess actors` to list profiles, `voidaccess actor <handle>` for the full profile, `voidaccess actor <handle> --timeline` for chronology, and `voidaccess actor <handle> --note "text"` for analyst notes.
276
316
 
@@ -19,8 +19,9 @@ Commercial threat intelligence platforms often charge prohibitive annual fees fo
19
19
 
20
20
  ---
21
21
 
22
- ## What's New in v1.6.0
22
+ ## What's New in v1.7.0
23
23
 
24
+ - **Dependency optional-extras split** — `sentence-transformers`, `torch`, `transformers`, `telethon`, and `playwright` are now optional extras: `voidaccess[nlp]`, `voidaccess[telegram]`, `voidaccess[js]`, and `voidaccess[all]`.
24
25
  - **Optional clearnet ScrapingAnt integration** — paste sites and RSS feeds can now be routed through ScrapingAnt. Affects clearnet scraping only; Tor, `.onion`, GitHub, and GitLab traffic are never affected.
25
26
  - **Three independent products** — Web Scraping API, Residential Proxy transport, and Datacenter Proxy transport are documented separately so the API key is not conflated with the proxy credentials.
26
27
  - **One-shot CLI flags** — `voidaccess investigate --use-scraping-api` and `voidaccess investigate --use-proxies` select the REST API or proxy transport for a single run.
@@ -30,7 +31,7 @@ Commercial threat intelligence platforms often charge prohibitive annual fees fo
30
31
 
31
32
  ---
32
33
 
33
- ## What's New in v1.5.0
34
+ ## What's New in v1.6.4
34
35
 
35
36
  - 37 new entity types across crypto, credentials, messaging, and network/forensic indicators.
36
37
  - YARA, Snort, Suricata, and IOC package ZIP exports.
@@ -224,7 +225,7 @@ Export formats:
224
225
 
225
226
  ## Actor Intelligence
226
227
 
227
- VoidAccess v1.5.0 persists actor profiles across investigations in `actor_profiles`, with linked aliases and infrastructure in `actor_aliases` and `actor_infrastructure`. Profiles are populated from threat actor, ransomware group, and handle entities, then enriched with co-occurring infrastructure and timeline events.
228
+ VoidAccess v1.7.0 persists actor profiles across investigations in `actor_profiles`, with linked aliases and infrastructure in `actor_aliases` and `actor_infrastructure`. Profiles are populated from threat actor, ransomware group, and handle entities, then enriched with co-occurring infrastructure and timeline events.
228
229
 
229
230
  Cross-alias resolution scores five signals: shared infrastructure, shared PGP, string similarity, temporal co-activity, and co-investigation. Use `voidaccess actors` to list profiles, `voidaccess actor <handle>` for the full profile, `voidaccess actor <handle> --timeline` for chronology, and `voidaccess actor <handle> --note "text"` for analyst notes.
230
231
 
@@ -490,7 +491,7 @@ To report a security vulnerability, see [docs/SECURITY.md](docs/SECURITY.md).
490
491
  ## License
491
492
 
492
493
  MIT License. See [LICENSE](LICENSE) for details.
493
-
494
-
495
-
496
-
494
+
495
+
496
+
497
+
@@ -54,7 +54,7 @@ logger = logging.getLogger(__name__)
54
54
  # ---------------------------------------------------------------------------
55
55
 
56
56
  PACKAGE_FORMAT = "voidaccess-ioc-v1"
57
- PACKAGE_VERSION = "1.5.0"
57
+ PACKAGE_VERSION = "1.7.0"
58
58
  SNORT_SID_BASE = 9000001 # 9xxxxxx reserved for VoidAccess-generated rules
59
59
 
60
60
  # Map extractor upper-case entity types to the per-file bucket they belong to.
@@ -461,7 +461,15 @@ def _write_emails(entities: Iterable[Any]) -> str:
461
461
  v = _entity_value(e).strip()
462
462
  if v:
463
463
  items.append(v)
464
- return "\n".join(items).rstrip() + ("\n" if items else "")
464
+ content = "\n".join(items).rstrip()
465
+ # v1.7 MED-4: if entities matched but all had empty values, write a
466
+ # header comment so the file is never 0 bytes (0-byte files inside a
467
+ # ZIP are technically valid but confusing to downstream tooling).
468
+ if items and not content:
469
+ return "# Email addresses extracted by VoidAccess\n"
470
+ if content:
471
+ return content + "\n"
472
+ return ""
465
473
 
466
474
 
467
475
  def _write_urls(entities: Iterable[Any]) -> str:
@@ -654,6 +662,16 @@ def _safe_stix_json(entities: list[Any], investigation_id: Any) -> str:
654
662
  # session. When running with dict entities (e.g. CLI export), we
655
663
  # can't pass them through that path — fall back to the empty bundle.
656
664
  bundle = investigation_to_stix_bundle(investigation_id)
665
+ try:
666
+ from export.stix import get_last_relationship_warning
667
+ warning = get_last_relationship_warning()
668
+ if warning:
669
+ logger.warning(
670
+ "STIX relationships were not included in IOC package: %s",
671
+ warning,
672
+ )
673
+ except Exception:
674
+ pass
657
675
  return bundle_to_json(bundle)
658
676
  except Exception as exc:
659
677
  logger.warning("STIX generation for IOC package failed: %s", exc)
@@ -18,10 +18,11 @@ from __future__ import annotations
18
18
  import json
19
19
  import logging
20
20
  import os
21
- from typing import Any, Optional, Union
21
+ from typing import Any, Optional
22
22
  import uuid
23
23
 
24
24
  logger = logging.getLogger(__name__)
25
+ _LAST_RELATIONSHIP_WARNING: Optional[str] = None
25
26
 
26
27
  # ---------------------------------------------------------------------------
27
28
  # Graceful import of stix2
@@ -196,6 +197,7 @@ def investigation_to_stix_bundle(
196
197
  - investigation not found
197
198
  """
198
199
  if not _STIX2_AVAILABLE:
200
+ _set_relationship_warning("stix2 is not installed")
199
201
  return _empty_bundle()
200
202
 
201
203
  filter_uuids: Optional[list[uuid.UUID]] = None
@@ -233,7 +235,13 @@ def investigation_to_stix_bundle(
233
235
  stix_id_map.setdefault(entity.value, actor.id)
234
236
 
235
237
  if include_relationships and stix_objects:
236
- stix_objects.extend(_build_stix_relationships(investigation_id, stix_id_map))
238
+ relationships, relationship_warning = _build_stix_relationships(
239
+ investigation_id, stix_id_map
240
+ )
241
+ stix_objects.extend(relationships)
242
+ _set_relationship_warning(relationship_warning)
243
+ else:
244
+ _set_relationship_warning(None)
237
245
 
238
246
  try:
239
247
  return stix2.Bundle(*stix_objects, allow_custom=True)
@@ -280,6 +288,16 @@ def _empty_bundle() -> Any:
280
288
  return stix2.Bundle()
281
289
 
282
290
 
291
+ def _set_relationship_warning(message: Optional[str]) -> None:
292
+ global _LAST_RELATIONSHIP_WARNING
293
+ _LAST_RELATIONSHIP_WARNING = message
294
+
295
+
296
+ def get_last_relationship_warning() -> Optional[str]:
297
+ """Return the last relationship-build warning, if any."""
298
+ return _LAST_RELATIONSHIP_WARNING
299
+
300
+
283
301
  def _load_entities_for_investigation(
284
302
  investigation_id: Any,
285
303
  entity_ids: Optional[list[uuid.UUID]] = None,
@@ -310,6 +328,11 @@ def _load_entities_for_investigation(
310
328
  if inv is None:
311
329
  return []
312
330
 
331
+ # v1.7 MED-9: use .c.column explicitly for the IN clause subquery.
332
+ # In SQLAlchemy 2.x, passing a plain subquery object to Column.in_()
333
+ # triggers a "Subquery is not yet a select()" coercion warning.
334
+ # Using subquery.c.column accesses the explicit column object so
335
+ # the ORM knows exactly what to coerce — no suppression needed.
313
336
  linked_ids_subq = (
314
337
  session.query(InvestigationEntityLink.entity_id)
315
338
  .filter(InvestigationEntityLink.investigation_id == inv.id)
@@ -317,7 +340,7 @@ def _load_entities_for_investigation(
317
340
  )
318
341
  q = session.query(Entity).filter(
319
342
  (Entity.investigation_id == inv.id)
320
- | Entity.id.in_(linked_ids_subq)
343
+ | Entity.id.in_(linked_ids_subq.c.entity_id)
321
344
  )
322
345
  db_entities = q.all()
323
346
 
@@ -353,14 +376,14 @@ def _load_entities_for_investigation(
353
376
  def _build_stix_relationships(
354
377
  investigation_id: Any,
355
378
  stix_id_map: dict[str, str],
356
- ) -> list[Any]:
379
+ ) -> tuple[list[Any], Optional[str]]:
357
380
  """
358
381
  Build STIX Relationship objects from graph edges for the investigation.
359
382
 
360
383
  Returns [] on any error.
361
384
  """
362
385
  if not _STIX2_AVAILABLE:
363
- return []
386
+ return [], "stix2 is not installed"
364
387
  try:
365
388
  from graph.builder import build_graph_from_db # noqa: PLC0415
366
389
 
@@ -385,10 +408,10 @@ def _build_stix_relationships(
385
408
  relationships.append(rel)
386
409
  except Exception:
387
410
  continue
388
- return relationships
411
+ return relationships, None
389
412
  except Exception as exc:
390
413
  logger.warning("_build_stix_relationships failed: %s", exc)
391
- return []
414
+ return [], f"_build_stix_relationships failed: {exc}"
392
415
 
393
416
 
394
417
  def _edge_type_to_stix(edge_type: str) -> str:
@@ -404,34 +404,9 @@ async def _extract_chunk(chunk: str, llm) -> str:
404
404
  generate_summary) keep their streaming output unchanged.
405
405
  """
406
406
  try:
407
- from langchain_core.callbacks import BaseCallbackHandler
408
-
409
- class _SilentHandler(BaseCallbackHandler):
410
- """No-op callback that suppresses the streaming handler's stdout
411
- prints for this single extraction call. Every hook is a
412
- no-op; we only care about preventing the parent's stdout
413
- noise — we still want ``ainvoke`` to return the full response
414
- content as before, just without the per-token terminal spam.
415
- """
416
-
417
- def on_llm_new_token(self, token: str, **kwargs) -> None: # noqa: D401
418
- return None
419
-
420
- def on_llm_end(self, response, **kwargs) -> None: # noqa: D401
421
- return None
422
-
423
- def on_llm_error(self, error, **kwargs) -> None: # noqa: D401
424
- return None
425
-
426
407
  prompt = _PROMPT_TEMPLATE.format(chunk=chunk)
427
- # Note: LangChain merges this callback config with the runnable's
428
- # default callbacks for THIS call only. Default callbacks on
429
- # other call sites (refine_query, filter_results, summary) are
430
- # not affected.
431
- response = await llm.ainvoke(
432
- prompt,
433
- config={"callbacks": [_SilentHandler()]},
434
- )
408
+ silent_llm = llm.bind(streaming=False, callbacks=[])
409
+ response = await silent_llm.ainvoke(prompt)
435
410
  content = response.content if hasattr(response, "content") else str(response)
436
411
  return content.strip()
437
412
  except Exception as exc:
@@ -20,8 +20,7 @@ import os
20
20
  import re
21
21
  import unicodedata
22
22
  from dataclasses import dataclass, field
23
- from datetime import datetime, timezone
24
- from typing import Optional, List
23
+ from typing import Optional
25
24
  import uuid
26
25
 
27
26
  logger = logging.getLogger(__name__)
@@ -734,6 +733,130 @@ def is_blocked_entity(entity_type: str, entity_value: str) -> bool:
734
733
  return False
735
734
 
736
735
 
736
+ # ---------------------------------------------------------------------------
737
+ # Reserved-range / placeholder blocklists (v1.7 — Q-1/Q-2 fixes)
738
+ # ---------------------------------------------------------------------------
739
+ #
740
+ # RFC 5737 documentation IP ranges — reserved for use in examples/docs,
741
+ # never real routable addresses.
742
+ # 192.0.2.0/24 (TEST-NET-1)
743
+ # 198.51.100.0/24 (TEST-NET-2)
744
+ # 203.0.113.0/24 (TEST-NET-3)
745
+ _RFC5737_NETWORKS: list[tuple[str, str]] = [
746
+ ("192.0.2.0", "255.255.255.0"), # 192.0.2.0/24
747
+ ("198.51.100.0", "255.255.255.0"), # 198.51.100.0/24
748
+ ("203.0.113.0", "255.255.255.0"), # 203.0.113.0/24
749
+ ]
750
+
751
+ # RFC 2606 reserved domain names.
752
+ _RFC2606_DOMAINS: frozenset[str] = frozenset({
753
+ "example.com",
754
+ "example.net",
755
+ "example.org",
756
+ "example.edu",
757
+ "test",
758
+ })
759
+
760
+ # Well-known placeholder email patterns that appear in examples/docs.
761
+ _PLACEHOLDER_EMAIL_RE = re.compile(
762
+ r"(?i)^(test|user|admin|example|foo|bar|noreply|no.reply)"
763
+ r"@(example\.com|example\.net|example\.org|domain\.com|localhost)$"
764
+ )
765
+
766
+
767
+ def _is_rfc5737_ip(value: str) -> bool:
768
+ """Return True if value is an IPv4 address in an RFC 5737 documentation range."""
769
+ try:
770
+ ip = __import__("ipaddress").ip_address(value.strip())
771
+ if not isinstance(ip, __import__("ipaddress").IPv4Address):
772
+ return False
773
+ for net_str, mask_str in _RFC5737_NETWORKS:
774
+ network = __import__("ipaddress").IPv4Network(f"{net_str}/{mask_str}", strict=False)
775
+ if ip in network:
776
+ return True
777
+ return False
778
+ except Exception:
779
+ return False
780
+
781
+
782
+ def _is_rfc2606_domain(value: str) -> bool:
783
+ """Return True if value is an RFC 2606 reserved domain."""
784
+ v = value.strip().lower()
785
+ # Exact match
786
+ if v in _RFC2606_DOMAINS:
787
+ return True
788
+ # Suffix match: "foo.example.com" etc.
789
+ parts = v.split(".")
790
+ for i in range(1, len(parts)):
791
+ suffix = ".".join(parts[i:])
792
+ if suffix in _RFC2606_DOMAINS:
793
+ return True
794
+ return False
795
+
796
+
797
+ def _is_placeholder_email(value: str) -> bool:
798
+ """Return True if value looks like a documentation/example email address."""
799
+ return bool(_PLACEHOLDER_EMAIL_RE.match(value.strip()))
800
+
801
+
802
+ # ---------------------------------------------------------------------------
803
+ # Source quality assignment (v1.7 — Q-2: GitHub source tier)
804
+ # ---------------------------------------------------------------------------
805
+ #
806
+ # Known low-trust URL patterns that are documentation/example content, not
807
+ # actual threat intelligence. Entities extracted from these are given
808
+ # source_quality=0.6 and are further de-prioritised in the entity cap.
809
+
810
+ _LOW_TRUST_URL_PATH_RE = re.compile(
811
+ r"(?i)"
812
+ # README, CHANGELOG, LICENSE, CONTRIBUTING, SECURITY, INSTALL, docs/
813
+ # anything at the repo root that looks like documentation
814
+ r"(^|/)(README|CHANGELOG|LICENSE|CONTRIBUTING|SECURITY|"
815
+ r"INSTALL|USER_GUIDE|DOCUMENTATION"
816
+ r"(\.md|\.txt|\.rst|\.html)?"
817
+ r"|(^|/)(docs?|examples?|specs?|schemas?)"
818
+ r"|(\.github[/\\])" # GitHub Actions / stored workflow files
819
+ r"|((^|/)examples?/)" # example/ directory
820
+ r"|(/issues/[0-9]+)" # GitHub issue comment — moderate signal
821
+ r"|(/pull/[0-9]+)" # PR comment — moderate signal
822
+ r"|(\.git)" # git metadata URLs
823
+ r")"
824
+ )
825
+
826
+
827
+ def _source_quality_from_url(url: str) -> float:
828
+ """
829
+ Return a source-quality multiplier (0.0-1.0) for a given source URL.
830
+
831
+ Higher values = more trustworthy source. Used to de-prioritise
832
+ documentation and example content vs. actual threat intel sources.
833
+ """
834
+ if not url:
835
+ return 1.0
836
+ url_lower = url.lower()
837
+
838
+ # GitHub
839
+ if "github.com" in url_lower:
840
+ if _LOW_TRUST_URL_PATH_RE.search(url):
841
+ return 0.6
842
+ # GitHub issue / PR comments — moderate value
843
+ if "/issues/" in url_lower or "/pull/" in url_lower:
844
+ return 0.7
845
+ # Default GitHub pages (raw blob from a repo)
846
+ return 0.6
847
+
848
+ # GitLab
849
+ if "gitlab.com" in url_lower:
850
+ if _LOW_TRUST_URL_PATH_RE.search(url):
851
+ return 0.6
852
+ if "/issues/" in url_lower or "/merge_requests/" in url_lower:
853
+ return 0.7
854
+ return 0.6
855
+
856
+ # Default: primary threat source
857
+ return 1.0
858
+
859
+
737
860
  _ORG_NOISE_RE = re.compile(
738
861
  r'\b(paid|bought|sold|hacked|leaked)\b'
739
862
  r'|\b(attempt|operations|disrupted)\b'
@@ -770,11 +893,22 @@ class NormalizedEntity:
770
893
  page_id: Optional[uuid.UUID]
771
894
  context_snippet: str = field(default="")
772
895
  extraction_method: str = field(default="")
896
+ # v1.7 — source_quality reflects the trustworthiness of the source.
897
+ # Higher = more credible. Used to downgrade README / documentation sources.
898
+ # 1.0 = primary threat source (dark web, paste sites, RSS, forum posts)
899
+ # 0.6 = secondary source (GitHub README, GitLab repo files)
900
+ # 0.0 = placeholder / reserved range (RFC 5737 IPs, RFC 2606 domains)
901
+ source_quality: float = field(default=1.0)
773
902
 
774
903
  @property
775
904
  def canonical_value(self) -> str:
776
905
  return self.value
777
906
 
907
+ @property
908
+ def is_placeholder(self) -> bool:
909
+ """True when the entity is a known reserved-range value, not real intel."""
910
+ return self.source_quality == 0.0
911
+
778
912
  def get(self, key: str, default=None):
779
913
  return getattr(self, key, default)
780
914
 
@@ -1152,6 +1286,34 @@ def normalize_entities(
1152
1286
  if dedup_key in seen_values:
1153
1287
  continue
1154
1288
  seen_values.add(dedup_key)
1289
+
1290
+ # v1.7 Q-1 — reserved-range / placeholder filtering.
1291
+ # Assign source_quality=0.0 so these are excluded from the entity
1292
+ # store (pipeline.py's apply_entity_cap drops confidence < 0.80,
1293
+ # and source_quality=0.0 propagates a near-zero effective score).
1294
+ source_quality = _source_quality_from_url(page_url)
1295
+
1296
+ if entity_type == "IP_ADDRESS" and _is_rfc5737_ip(canonical):
1297
+ source_quality = 0.0
1298
+ logger.debug(
1299
+ "RFC 5737 IP rejected (reserved documentation range): %s", canonical
1300
+ )
1301
+ continue
1302
+
1303
+ if entity_type == "DOMAIN" and _is_rfc2606_domain(canonical):
1304
+ source_quality = 0.0
1305
+ logger.debug(
1306
+ "RFC 2606 domain rejected (reserved documentation domain): %s", canonical
1307
+ )
1308
+ continue
1309
+
1310
+ if entity_type == "EMAIL_ADDRESS" and _is_placeholder_email(canonical):
1311
+ source_quality = 0.0
1312
+ logger.debug(
1313
+ "Placeholder email rejected (documentation/example address): %s", canonical
1314
+ )
1315
+ continue
1316
+
1155
1317
  snip = _context_snippet(page_text, canonical) if page_text else ""
1156
1318
  result.append(
1157
1319
  NormalizedEntity(
@@ -1162,6 +1324,7 @@ def normalize_entities(
1162
1324
  page_id=page_id,
1163
1325
  context_snippet=snip,
1164
1326
  extraction_method=_extraction_method_for(entity_type),
1327
+ source_quality=source_quality,
1165
1328
  )
1166
1329
  )
1167
1330
 
@@ -42,6 +42,13 @@ PER_TYPE_CAPS = {
42
42
  "PERSON_NAME": 30,
43
43
  "LOCATION": 20,
44
44
  "THREAT_ACTOR_HANDLE": 80,
45
+ # v1.7 MED-3: DATE entities from RSS changelogs and GitHub commit
46
+ # timestamps dominated one run (85/182 entities). A hard sub-cap keeps
47
+ # genuinely relevant dates (ransomware leak announcement, CVE publication)
48
+ # while suppressing changelog/commit noise. 20 dates per investigation is
49
+ # far more than any analyst needs; the global entity_cap (400) handles
50
+ # the ceiling for all types combined.
51
+ "DATE": 20,
45
52
  }
46
53
 
47
54
  # ---------------------------------------------------------------------------
@@ -545,27 +552,40 @@ def apply_entity_cap(
545
552
  """
546
553
  original_count = len(entities)
547
554
 
548
- # Step a: confidence filter
555
+ # Step a: confidence filter — also drops placeholder entities (source_quality=0.0)
549
556
  filtered = [e for e in entities if e.confidence >= 0.80]
550
- removed_confidence = original_count - len(filtered)
557
+ placeholder_count = sum(1 for e in entities if getattr(e, "source_quality", 1.0) == 0.0)
558
+ filtered = [e for e in filtered if getattr(e, "source_quality", 1.0) != 0.0]
559
+ removed_confidence = original_count - placeholder_count - len(filtered)
551
560
  if removed_confidence:
552
- logger.warning(f"Entity confidence filter removed {removed_confidence} low-confidence entities")
561
+ logger.warning(f"Entity confidence/placeholder filter removed {removed_confidence} low-confidence entities")
562
+ if placeholder_count:
563
+ logger.info(f"Entity placeholder filter dropped {placeholder_count} reserved-range entities")
553
564
 
554
565
  # Count occurrences per entity (by type+value) and boost confidence
566
+ # v1.7 Q-2: multiply the boost by source_quality so low-quality sources
567
+ # (GitHub README at 0.6) get a smaller boost than primary sources (1.0).
555
568
  total_pages = len(set(e.source_url for e in filtered)) or 1
556
569
  for ent in filtered:
557
570
  occ = _occurrence_count(ent, filtered)
558
571
  ent._occurrence = occ
559
572
  occurrence_ratio = occ / total_pages
560
- confidence_boost = min(occurrence_ratio * 0.10, 0.10)
573
+ source_quality = getattr(ent, "source_quality", 1.0)
574
+ confidence_boost = min(occurrence_ratio * 0.10, 0.10) * source_quality
561
575
  ent.confidence = min(ent.confidence + confidence_boost, 1.0)
562
576
 
563
577
  # Step b: per-type sub-caps
564
578
  filtered = _apply_per_type_caps(filtered)
565
579
 
566
- # Step c: sort and cap
580
+ # Step c: sort and cap — source_quality is a secondary sort key so
581
+ # GitHub/README entities with equal confidence are ranked below primary sources.
567
582
  if len(filtered) > cap:
568
- filtered.sort(key=lambda e: (-e.confidence, _type_priority(e.entity_type), -e._occurrence))
583
+ filtered.sort(key=lambda e: (
584
+ -e.confidence,
585
+ -getattr(e, "source_quality", 1.0), # higher quality = higher rank
586
+ _type_priority(e.entity_type),
587
+ -e._occurrence,
588
+ ))
569
589
  filtered = filtered[:cap]
570
590
  logger.warning(
571
591
  f"Entity cap applied: {original_count} entities reduced to {len(filtered)} "
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "voidaccess"
7
- version = "1.6.2"
7
+ version = "1.7.0"
8
8
  description = "Dark web OSINT CLI — automated threat intelligence from query to report"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -25,10 +25,17 @@ dependencies = [
25
25
  "typer>=0.12",
26
26
  "rich>=13",
27
27
  "textual>=0.60",
28
+ "click>=8.0",
29
+ "fastapi>=0.115",
28
30
  "aiohttp>=3.9",
29
31
  "aiohttp-socks>=0.8",
32
+ "alembic>=1.13",
33
+ "apscheduler>=3.10",
34
+ "pillow>=10.0",
30
35
  "sqlalchemy>=2.0",
31
36
  "aiosqlite>=0.20",
37
+ "filelock>=3.14",
38
+ "numpy>=1.26",
32
39
  "langchain>=0.2",
33
40
  "langchain-openai>=0.1",
34
41
  "langchain-anthropic>=0.1",
@@ -40,15 +47,49 @@ dependencies = [
40
47
  "spacy>=3.7",
41
48
  "beautifulsoup4>=4.12",
42
49
  "feedparser>=6.0",
50
+ "networkx>=3.0",
51
+ "pydantic>=2.7",
52
+ "python-jose[cryptography]>=3.3",
53
+ "passlib[bcrypt]>=1.7",
54
+ "slowapi>=0.1.9",
55
+ "redis>=5.0",
56
+ "pyyaml>=6.0",
57
+ "openai>=1.30",
58
+ "cryptography>=42.0",
43
59
  "python-dateutil>=2.9",
60
+ "langdetect>=1.0",
61
+ "pyvis>=0.3",
62
+ "urllib3>=2.2",
44
63
  "trafilatura>=1.6",
45
64
  "requests>=2.31",
46
65
  "python-socks>=2.4",
47
66
  "tldextract>=5.1",
67
+ "stix2>=3.0.2",
48
68
  ]
49
69
 
50
70
  [project.optional-dependencies]
51
71
  dev = ["pytest", "pytest-asyncio"]
72
+ nlp = [
73
+ "sentence-transformers>=3.0",
74
+ "torch>=2.3",
75
+ "transformers>=4.42",
76
+ ]
77
+ vector = [
78
+ "chromadb>=0.5",
79
+ "sentence-transformers>=3.0",
80
+ "torch>=2.3",
81
+ ]
82
+ telegram = ["telethon>=1.36"]
83
+ js = ["playwright>=1.45"]
84
+ # Convenience: all optional packages at once
85
+ all = [
86
+ "chromadb>=0.5",
87
+ "playwright>=1.45",
88
+ "sentence-transformers>=3.0",
89
+ "telethon>=1.36",
90
+ "torch>=2.3",
91
+ "transformers>=4.42",
92
+ ]
52
93
 
53
94
  [project.scripts]
54
95
  voidaccess = "voidaccess_cli.main:app"
@@ -92,7 +133,7 @@ exclude = [
92
133
  "__pycache__*",
93
134
  ]
94
135
 
95
- # v1.6.2 — ship data/onion_seeds.json inside the installed package.
136
+ # v1.6.3 — ship data/onion_seeds.json inside the installed package.
96
137
  # Without this entry, ``pip install voidaccess`` produces a package
97
138
  # directory that does NOT contain the curated seed list — every
98
139
  # CLI investigation silently gets zero seed injection, which is the