voidaccess 1.9.2__tar.gz → 1.9.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. {voidaccess-1.9.2 → voidaccess-1.9.3}/PKG-INFO +1 -1
  2. {voidaccess-1.9.2 → voidaccess-1.9.3}/api/main.py +1 -1
  3. {voidaccess-1.9.2 → voidaccess-1.9.3}/api/routes/investigations.py +1 -1
  4. {voidaccess-1.9.2 → voidaccess-1.9.3}/export/ioc_package.py +1 -1
  5. {voidaccess-1.9.2 → voidaccess-1.9.3}/export/stix.py +110 -5
  6. {voidaccess-1.9.2 → voidaccess-1.9.3}/pyproject.toml +1 -1
  7. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/email_reputation.py +4 -2
  8. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_api.py +62 -48
  9. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_confidence_calibration.py +10 -8
  10. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_db.py +1 -0
  11. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_llm.py +10 -0
  12. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_llm_utils.py +1 -1
  13. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_regex_patterns.py +21 -21
  14. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_snort_export.py +28 -28
  15. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_stix_export.py +20 -0
  16. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_yara_export.py +17 -17
  17. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess/llm.py +15 -10
  18. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess.egg-info/PKG-INFO +1 -1
  19. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess_cli/__init__.py +1 -1
  20. {voidaccess-1.9.2 → voidaccess-1.9.3}/LICENSE +0 -0
  21. {voidaccess-1.9.2 → voidaccess-1.9.3}/README.md +0 -0
  22. {voidaccess-1.9.2 → voidaccess-1.9.3}/analysis/__init__.py +0 -0
  23. {voidaccess-1.9.2 → voidaccess-1.9.3}/analysis/opsec.py +0 -0
  24. {voidaccess-1.9.2 → voidaccess-1.9.3}/analysis/patterns.py +0 -0
  25. {voidaccess-1.9.2 → voidaccess-1.9.3}/analysis/temporal.py +0 -0
  26. {voidaccess-1.9.2 → voidaccess-1.9.3}/api/__init__.py +0 -0
  27. {voidaccess-1.9.2 → voidaccess-1.9.3}/api/auth.py +0 -0
  28. {voidaccess-1.9.2 → voidaccess-1.9.3}/api/errors.py +0 -0
  29. {voidaccess-1.9.2 → voidaccess-1.9.3}/api/routes/__init__.py +0 -0
  30. {voidaccess-1.9.2 → voidaccess-1.9.3}/api/routes/actors.py +0 -0
  31. {voidaccess-1.9.2 → voidaccess-1.9.3}/api/routes/admin.py +0 -0
  32. {voidaccess-1.9.2 → voidaccess-1.9.3}/api/routes/auth.py +0 -0
  33. {voidaccess-1.9.2 → voidaccess-1.9.3}/api/routes/entities.py +0 -0
  34. {voidaccess-1.9.2 → voidaccess-1.9.3}/api/routes/export.py +2 -2
  35. {voidaccess-1.9.2 → voidaccess-1.9.3}/api/routes/monitors.py +0 -0
  36. {voidaccess-1.9.2 → voidaccess-1.9.3}/api/routes/search.py +0 -0
  37. {voidaccess-1.9.2 → voidaccess-1.9.3}/api/routes/settings.py +0 -0
  38. {voidaccess-1.9.2 → voidaccess-1.9.3}/auth/__init__.py +0 -0
  39. {voidaccess-1.9.2 → voidaccess-1.9.3}/auth/token_blacklist.py +0 -0
  40. {voidaccess-1.9.2 → voidaccess-1.9.3}/config.py +0 -0
  41. {voidaccess-1.9.2 → voidaccess-1.9.3}/crawler/__init__.py +0 -0
  42. {voidaccess-1.9.2 → voidaccess-1.9.3}/crawler/dedup.py +0 -0
  43. {voidaccess-1.9.2 → voidaccess-1.9.3}/crawler/frontier.py +0 -0
  44. {voidaccess-1.9.2 → voidaccess-1.9.3}/crawler/spider.py +0 -0
  45. {voidaccess-1.9.2 → voidaccess-1.9.3}/crawler/utils.py +0 -0
  46. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/__init__.py +0 -0
  47. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/__init__.py +0 -0
  48. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/env.py +0 -0
  49. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0001_initial_schema.py +0 -0
  50. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0002_add_investigation_status_column.py +0 -0
  51. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0002_add_missing_tables.py +0 -0
  52. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0003_add_canonical_value_and_entity_links.py +0 -0
  53. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0004_add_page_posted_at.py +0 -0
  54. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0005_add_extraction_method.py +0 -0
  55. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0006_add_monitor_alerts.py +0 -0
  56. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0007_add_actor_style_profiles.py +0 -0
  57. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0008_add_users_table.py +0 -0
  58. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0009_add_investigation_id_to_relationships.py +0 -0
  59. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0010_add_composite_index_entity_relationships.py +0 -0
  60. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0011_add_page_extraction_cache.py +0 -0
  61. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0013_add_graph_status.py +0 -0
  62. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0015_add_progress_fields.py +0 -0
  63. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0016_backfill_graph_status.py +0 -0
  64. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0017_add_user_api_keys.py +0 -0
  65. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0018_add_user_id_to_investigations.py +0 -0
  66. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0019_add_content_safety_log.py +0 -0
  67. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0020_add_entity_source_tracking.py +0 -0
  68. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0021_add_search_engine_stats.py +0 -0
  69. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0022_add_actor_profiles.py +0 -0
  70. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0023_add_investigation_metadata.py +0 -0
  71. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0024_add_investigation_step_metrics.py +0 -0
  72. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/migrations/versions/0025_add_cancellation_requested.py +0 -0
  73. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/models.py +0 -0
  74. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/queries.py +0 -0
  75. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/search_engine_stats.py +0 -0
  76. {voidaccess-1.9.2 → voidaccess-1.9.3}/db/session.py +0 -0
  77. {voidaccess-1.9.2 → voidaccess-1.9.3}/export/__init__.py +0 -0
  78. {voidaccess-1.9.2 → voidaccess-1.9.3}/export/_entity_loading.py +0 -0
  79. {voidaccess-1.9.2 → voidaccess-1.9.3}/export/misp.py +0 -0
  80. {voidaccess-1.9.2 → voidaccess-1.9.3}/export/relationships.py +0 -0
  81. {voidaccess-1.9.2 → voidaccess-1.9.3}/export/sigma.py +0 -0
  82. {voidaccess-1.9.2 → voidaccess-1.9.3}/export/snort_export.py +0 -0
  83. {voidaccess-1.9.2 → voidaccess-1.9.3}/export/yara_export.py +0 -0
  84. {voidaccess-1.9.2 → voidaccess-1.9.3}/extractor/__init__.py +0 -0
  85. {voidaccess-1.9.2 → voidaccess-1.9.3}/extractor/confidence.py +0 -0
  86. {voidaccess-1.9.2 → voidaccess-1.9.3}/extractor/entity_shape.py +0 -0
  87. {voidaccess-1.9.2 → voidaccess-1.9.3}/extractor/gazetteer.py +0 -0
  88. {voidaccess-1.9.2 → voidaccess-1.9.3}/extractor/identity.py +0 -0
  89. {voidaccess-1.9.2 → voidaccess-1.9.3}/extractor/llm_extract.py +0 -0
  90. {voidaccess-1.9.2 → voidaccess-1.9.3}/extractor/ner.py +0 -0
  91. {voidaccess-1.9.2 → voidaccess-1.9.3}/extractor/normalizer.py +0 -0
  92. {voidaccess-1.9.2 → voidaccess-1.9.3}/extractor/pipeline.py +0 -0
  93. {voidaccess-1.9.2 → voidaccess-1.9.3}/extractor/regex_patterns.py +0 -0
  94. {voidaccess-1.9.2 → voidaccess-1.9.3}/extractor/relationship_extract.py +0 -0
  95. {voidaccess-1.9.2 → voidaccess-1.9.3}/fingerprint/__init__.py +0 -0
  96. {voidaccess-1.9.2 → voidaccess-1.9.3}/fingerprint/calibration.py +0 -0
  97. {voidaccess-1.9.2 → voidaccess-1.9.3}/fingerprint/profiler.py +0 -0
  98. {voidaccess-1.9.2 → voidaccess-1.9.3}/fingerprint/stylometry.py +0 -0
  99. {voidaccess-1.9.2 → voidaccess-1.9.3}/graph/__init__.py +0 -0
  100. {voidaccess-1.9.2 → voidaccess-1.9.3}/graph/builder.py +0 -0
  101. {voidaccess-1.9.2 → voidaccess-1.9.3}/graph/export.py +0 -0
  102. {voidaccess-1.9.2 → voidaccess-1.9.3}/graph/model.py +0 -0
  103. {voidaccess-1.9.2 → voidaccess-1.9.3}/graph/queries.py +0 -0
  104. {voidaccess-1.9.2 → voidaccess-1.9.3}/graph/visualize.py +0 -0
  105. {voidaccess-1.9.2 → voidaccess-1.9.3}/i18n/__init__.py +0 -0
  106. {voidaccess-1.9.2 → voidaccess-1.9.3}/i18n/detect.py +0 -0
  107. {voidaccess-1.9.2 → voidaccess-1.9.3}/i18n/query_expand.py +0 -0
  108. {voidaccess-1.9.2 → voidaccess-1.9.3}/i18n/translate.py +0 -0
  109. {voidaccess-1.9.2 → voidaccess-1.9.3}/monitor/__init__.py +0 -0
  110. {voidaccess-1.9.2 → voidaccess-1.9.3}/monitor/_db.py +0 -0
  111. {voidaccess-1.9.2 → voidaccess-1.9.3}/monitor/alerts.py +0 -0
  112. {voidaccess-1.9.2 → voidaccess-1.9.3}/monitor/config.py +0 -0
  113. {voidaccess-1.9.2 → voidaccess-1.9.3}/monitor/diff.py +0 -0
  114. {voidaccess-1.9.2 → voidaccess-1.9.3}/monitor/jobs.py +0 -0
  115. {voidaccess-1.9.2 → voidaccess-1.9.3}/monitor/scheduler.py +0 -0
  116. {voidaccess-1.9.2 → voidaccess-1.9.3}/scraper/__init__.py +0 -0
  117. {voidaccess-1.9.2 → voidaccess-1.9.3}/scraper/scrape.py +0 -0
  118. {voidaccess-1.9.2 → voidaccess-1.9.3}/scraper/scrape_js.py +0 -0
  119. {voidaccess-1.9.2 → voidaccess-1.9.3}/search/__init__.py +0 -0
  120. {voidaccess-1.9.2 → voidaccess-1.9.3}/search/circuit_breaker.py +0 -0
  121. {voidaccess-1.9.2 → voidaccess-1.9.3}/search/engine_catalog.py +0 -0
  122. {voidaccess-1.9.2 → voidaccess-1.9.3}/search/query_builder.py +0 -0
  123. {voidaccess-1.9.2 → voidaccess-1.9.3}/search/search.py +0 -0
  124. {voidaccess-1.9.2 → voidaccess-1.9.3}/setup.cfg +0 -0
  125. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/__init__.py +0 -0
  126. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/actor_profiles.py +0 -0
  127. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/blockchain.py +0 -0
  128. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/breach_lookup.py +0 -0
  129. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/cache.py +0 -0
  130. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/cisa.py +0 -0
  131. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/data/__init__.py +0 -0
  132. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/data/onion_seeds.json +0 -0
  133. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/dns_enrichment.py +0 -0
  134. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/domain_reputation.py +0 -0
  135. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/engines.py +0 -0
  136. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/enrichment.py +0 -0
  137. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/github_scraper.py +0 -0
  138. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/gitlab_scraper.py +0 -0
  139. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/hash_reputation.py +0 -0
  140. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/historical_intel.py +0 -0
  141. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/infostealer.py +0 -0
  142. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/ip_reputation.py +0 -0
  143. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/nvd.py +0 -0
  144. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/paste_scraper.py +0 -0
  145. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/pastes.py +0 -0
  146. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/proxy_client.py +0 -0
  147. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/rss_scraper.py +0 -0
  148. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/seed_manager.py +0 -0
  149. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/seeds.py +0 -0
  150. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/shodan.py +0 -0
  151. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/telegram.py +0 -0
  152. {voidaccess-1.9.2 → voidaccess-1.9.3}/sources/virustotal.py +0 -0
  153. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_analysis_opsec.py +0 -0
  154. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_analysis_stylometry.py +0 -0
  155. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_analysis_temporal.py +0 -0
  156. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_api_monitors.py +0 -0
  157. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_blockchain.py +0 -0
  158. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_breach_lookup.py +0 -0
  159. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_cli_proxy_config.py +0 -0
  160. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_config.py +0 -0
  161. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_consolidated_contracts.py +0 -0
  162. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_crawler.py +0 -0
  163. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_dns_enrichment.py +0 -0
  164. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_domain_reputation.py +0 -0
  165. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_email_reputation.py +0 -0
  166. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_extractor_llm_pipeline.py +0 -0
  167. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_filter_e2e_manual.py +0 -0
  168. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_filter_parser_manual.py +0 -0
  169. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_fingerprint.py +0 -0
  170. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_github_scraper.py +0 -0
  171. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_gitlab_scraper.py +0 -0
  172. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_graph.py +0 -0
  173. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_graph_integration.py +0 -0
  174. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_hash_reputation.py +0 -0
  175. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_i18n.py +0 -0
  176. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_infostealer.py +0 -0
  177. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_ip_reputation.py +0 -0
  178. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_model_singleton.py +0 -0
  179. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_monitor.py +0 -0
  180. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_monitor_integration.py +0 -0
  181. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_nvd.py +0 -0
  182. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_pagination.py +0 -0
  183. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_paste_scraper.py +0 -0
  184. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_proxy_client.py +0 -0
  185. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_ransomlook.py +0 -0
  186. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_relationship_extract.py +0 -0
  187. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_rss_scraper.py +0 -0
  188. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_scrape_js.py +0 -0
  189. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_settings.py +0 -0
  190. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_sources.py +0 -0
  191. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_sources_enrichment_new.py +0 -0
  192. {voidaccess-1.9.2 → voidaccess-1.9.3}/tests/test_vector.py +0 -0
  193. {voidaccess-1.9.2 → voidaccess-1.9.3}/utils/__init__.py +0 -0
  194. {voidaccess-1.9.2 → voidaccess-1.9.3}/utils/async_utils.py +0 -0
  195. {voidaccess-1.9.2 → voidaccess-1.9.3}/utils/content_dedup.py +0 -0
  196. {voidaccess-1.9.2 → voidaccess-1.9.3}/utils/content_safety.py +0 -0
  197. {voidaccess-1.9.2 → voidaccess-1.9.3}/utils/defang.py +0 -0
  198. {voidaccess-1.9.2 → voidaccess-1.9.3}/utils/encryption.py +0 -0
  199. {voidaccess-1.9.2 → voidaccess-1.9.3}/utils/enrichment_cache.py +0 -0
  200. {voidaccess-1.9.2 → voidaccess-1.9.3}/utils/investigation_metrics.py +0 -0
  201. {voidaccess-1.9.2 → voidaccess-1.9.3}/utils/ioc_freshness.py +0 -0
  202. {voidaccess-1.9.2 → voidaccess-1.9.3}/utils/user_keys.py +0 -0
  203. {voidaccess-1.9.2 → voidaccess-1.9.3}/vector/__init__.py +0 -0
  204. {voidaccess-1.9.2 → voidaccess-1.9.3}/vector/embedder.py +0 -0
  205. {voidaccess-1.9.2 → voidaccess-1.9.3}/vector/model_singleton.py +0 -0
  206. {voidaccess-1.9.2 → voidaccess-1.9.3}/vector/search.py +0 -0
  207. {voidaccess-1.9.2 → voidaccess-1.9.3}/vector/store.py +0 -0
  208. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess/__init__.py +0 -0
  209. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess/config.py +0 -0
  210. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess/llm_utils.py +0 -0
  211. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess.egg-info/SOURCES.txt +0 -0
  212. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess.egg-info/dependency_links.txt +0 -0
  213. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess.egg-info/entry_points.txt +0 -0
  214. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess.egg-info/requires.txt +0 -0
  215. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess.egg-info/top_level.txt +0 -0
  216. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess_cli/adapters/__init__.py +0 -0
  217. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess_cli/adapters/sqlite.py +0 -0
  218. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess_cli/browser.py +0 -0
  219. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess_cli/commands/__init__.py +0 -0
  220. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess_cli/commands/actors.py +0 -0
  221. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess_cli/commands/configure.py +0 -0
  222. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess_cli/commands/enrich.py +0 -0
  223. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess_cli/commands/export.py +0 -0
  224. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess_cli/commands/investigate.py +0 -0
  225. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess_cli/commands/show.py +0 -0
  226. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess_cli/config.py +0 -0
  227. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess_cli/display.py +0 -0
  228. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess_cli/main.py +0 -0
  229. {voidaccess-1.9.2 → voidaccess-1.9.3}/voidaccess_cli/tor_detect.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voidaccess
3
- Version: 1.9.2
3
+ Version: 1.9.3
4
4
  Summary: Dark web OSINT CLI — automated threat intelligence from query to report
5
5
  Author: VoidAccess
6
6
  License-Expression: MIT
@@ -40,7 +40,7 @@ logging.basicConfig(level=logging.DEBUG)
40
40
  # Rate limiter setup
41
41
  # ---------------------------------------------------------------------------
42
42
 
43
- DISABLE_RATE_LIMIT = os.getenv("DISABLE_RATE_LIMIT", "false").lower() == "true"
43
+ DISABLE_RATE_LIMIT = (os.getenv("DISABLE_RATE_LIMIT", "false") or "false").lower() == "true"
44
44
 
45
45
  if DISABLE_RATE_LIMIT:
46
46
  limiter = None
@@ -537,7 +537,7 @@ async def _check_cancelled(inv_uuid: uuid.UUID, investigation_id: str) -> bool:
537
537
  # Rate limiting (shared key_func with api/main.py; enforcement via app.state.limiter)
538
538
  # ---------------------------------------------------------------------------
539
539
 
540
- _DISABLE_RATE_LIMIT = os.getenv("DISABLE_RATE_LIMIT", "false").lower() == "true"
540
+ _DISABLE_RATE_LIMIT = (os.getenv("DISABLE_RATE_LIMIT", "false") or "false").lower() == "true"
541
541
 
542
542
  if not _DISABLE_RATE_LIMIT:
543
543
  try:
@@ -57,7 +57,7 @@ logger = logging.getLogger(__name__)
57
57
  # ---------------------------------------------------------------------------
58
58
 
59
59
  PACKAGE_FORMAT = "voidaccess-ioc-v1"
60
- PACKAGE_VERSION = "1.9.2"
60
+ PACKAGE_VERSION = "1.9.3"
61
61
  SNORT_SID_BASE = 9000001 # 9xxxxxx reserved for VoidAccess-generated rules
62
62
 
63
63
  # Map extractor upper-case entity types to the per-file bucket they belong to.
@@ -18,6 +18,7 @@ from __future__ import annotations
18
18
  import json
19
19
  import logging
20
20
  import os
21
+ import re
21
22
  import warnings
22
23
  from typing import Any, Optional
23
24
  import uuid
@@ -78,6 +79,114 @@ _STIX_PATTERNS: dict[str, str] = {
78
79
  # Entity types that map to STIX Malware objects
79
80
  _MALWARE_TYPES = frozenset({"MALWARE_FAMILY", "RANSOMWARE_GROUP"})
80
81
 
82
+ # STIX 2.1's malware-type vocabulary is intentionally small. Keep the
83
+ # mapping here instead of treating every MALWARE_FAMILY as the generic
84
+ # ``malware`` type: family names and enrichment metadata often carry enough
85
+ # information for SIEM consumers to filter useful categories.
86
+ _STIX_MALWARE_TYPE_ALIASES = {
87
+ "infostealer": "spyware",
88
+ "stealer": "spyware",
89
+ "remote_access_trojan": "remote-access-trojan",
90
+ "remote access trojan": "remote-access-trojan",
91
+ "rat": "remote-access-trojan",
92
+ "coinminer": "resource-exploitation",
93
+ "cryptominer": "resource-exploitation",
94
+ "crypto-miner": "resource-exploitation",
95
+ "miner": "resource-exploitation",
96
+ }
97
+ _STIX_MALWARE_TYPES = frozenset({
98
+ "adware",
99
+ "backdoor",
100
+ "bot",
101
+ "ddos",
102
+ "dropper",
103
+ "exploit-kit",
104
+ "keylogger",
105
+ "ransomware",
106
+ "remote-access-trojan",
107
+ "resource-exploitation",
108
+ "rootkit",
109
+ "screen-capture",
110
+ "spyware",
111
+ "wiper",
112
+ "malware",
113
+ })
114
+
115
+ # These are conservative, high-signal family/name hints. Unknown families
116
+ # retain the valid generic STIX type rather than being assigned a speculative
117
+ # category.
118
+ _MALWARE_NAME_PATTERNS = (
119
+ ("ransomware", re.compile(
120
+ r"\b(?:ransomware|lockbit|blackcat|alphv|clop|conti|ryuk|hive|revil|darkside|wannacry)\b",
121
+ re.IGNORECASE,
122
+ )),
123
+ ("remote-access-trojan", re.compile(
124
+ r"\b(?:remote\s+access\s+trojan|remcos|njrat|nanocore|quasar|darkcomet|xrat)\b",
125
+ re.IGNORECASE,
126
+ )),
127
+ ("spyware", re.compile(
128
+ r"\b(?:spyware|infostealer|stealer|redline|vidar|lumma|raccoon|agent\s+tesla|formbook)\b",
129
+ re.IGNORECASE,
130
+ )),
131
+ ("bot", re.compile(
132
+ r"\b(?:botnet|emotet|qakbot|qbot|trickbot|mirai|zeus|mozi)\b",
133
+ re.IGNORECASE,
134
+ )),
135
+ ("keylogger", re.compile(r"\bkeylogger\b", re.IGNORECASE)),
136
+ ("exploit-kit", re.compile(r"\bexploit[\s-]+kit\b", re.IGNORECASE)),
137
+ ("rootkit", re.compile(r"\brootkit\b", re.IGNORECASE)),
138
+ ("screen-capture", re.compile(r"\bscreen[\s-]+capture\b", re.IGNORECASE)),
139
+ ("dropper", re.compile(r"\bdropper\b", re.IGNORECASE)),
140
+ ("wiper", re.compile(r"\bwiper\b", re.IGNORECASE)),
141
+ ("adware", re.compile(r"\badware\b", re.IGNORECASE)),
142
+ ("resource-exploitation", re.compile(
143
+ r"\b(?:cryptominer|crypto[\s-]+miner|coinminer|cryptojacking)\b",
144
+ re.IGNORECASE,
145
+ )),
146
+ ("backdoor", re.compile(r"\bbackdoor\b", re.IGNORECASE)),
147
+ )
148
+
149
+
150
+ def _entity_attribute(entity: Any, name: str) -> Any:
151
+ """Read optional classification metadata from dicts and ORM-like rows."""
152
+ if isinstance(entity, dict):
153
+ return entity.get(name)
154
+ return getattr(entity, name, None)
155
+
156
+
157
+ def _normalise_malware_type(value: Any) -> Optional[str]:
158
+ if not isinstance(value, str):
159
+ return None
160
+ key = value.strip().lower().replace("_", " ")
161
+ key = _STIX_MALWARE_TYPE_ALIASES.get(key, key)
162
+ key = key.replace(" ", "-")
163
+ return key if key in _STIX_MALWARE_TYPES else None
164
+
165
+
166
+ def _classify_malware(entity: Any) -> list[str]:
167
+ """Return evidence-backed STIX malware types for an entity."""
168
+ entity_type = str(getattr(entity, "entity_type", "") or "").upper()
169
+ if isinstance(entity, dict):
170
+ entity_type = str(entity.get("entity_type") or "").upper()
171
+ if entity_type == "RANSOMWARE_GROUP":
172
+ return ["ransomware"]
173
+
174
+ for field_name in ("malware_types", "malware_type", "malware_category", "category", "family_type"):
175
+ raw = _entity_attribute(entity, field_name)
176
+ values = raw if isinstance(raw, (list, tuple, set)) else [raw]
177
+ classified = [item for item in (_normalise_malware_type(v) for v in values) if item]
178
+ if classified:
179
+ return list(dict.fromkeys(classified))
180
+
181
+ evidence = " ".join(
182
+ str(_entity_attribute(entity, field) or "")
183
+ for field in ("value", "context_snippet")
184
+ )
185
+ for malware_type, pattern in _MALWARE_NAME_PATTERNS:
186
+ if pattern.search(evidence):
187
+ return [malware_type]
188
+ return ["malware"]
189
+
81
190
  # ---------------------------------------------------------------------------
82
191
  # Confidence mapping: VoidAccess float → STIX integer (0-100)
83
192
  # ---------------------------------------------------------------------------
@@ -160,11 +269,7 @@ def entity_to_stix_malware(entity: Any) -> Optional[Any]:
160
269
  malware = stix2.Malware(
161
270
  name=entity.value,
162
271
  is_family=True,
163
- malware_types=(
164
- ["ransomware"]
165
- if entity.entity_type == "RANSOMWARE_GROUP"
166
- else ["malware"]
167
- ),
272
+ malware_types=_classify_malware(entity),
168
273
  confidence=_to_stix_confidence(get_entity_confidence(entity)),
169
274
  allow_custom=True,
170
275
  x_voidaccess_source_quality=getattr(entity, "source_quality", 1.0),
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "voidaccess"
7
- version = "1.9.2"
7
+ version = "1.9.3"
8
8
  description = "Dark web OSINT CLI — automated threat intelligence from query to report"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -555,8 +555,10 @@ def _update_email_entities_in_db(
555
555
  existing.append(tag)
556
556
  db_entity.corroborating_sources = json.dumps(existing)
557
557
  session.commit()
558
- except Exception as exc:
559
- logger.warning("email_reputation: DB update failed: %s", exc)
558
+ except Exception:
559
+ # Do not log the raw SQLAlchemy exception: its parameter dump can
560
+ # contain the full email address being enriched.
561
+ logger.warning("email_reputation: DB update failed")
560
562
 
561
563
 
562
564
  # ---------------------------------------------------------------------------
@@ -38,15 +38,21 @@ def _make_client():
38
38
  """Create a FastAPI TestClient. Import deferred so pytest collection doesn't fail without fastapi."""
39
39
  from fastapi.testclient import TestClient # noqa: PLC0415
40
40
  from api.main import app # noqa: PLC0415
41
- from api.auth import get_current_user
41
+ from api.auth import CurrentUser, get_current_user, require_password_not_reset_pending
42
+ from db.models import User
42
43
  from unittest.mock import MagicMock
43
44
 
44
- mock_user = MagicMock()
45
- mock_user.id = 1
46
- mock_user.email = "test@example.com"
47
- mock_user.is_active = True
45
+ mock_user = User(
46
+ id=1,
47
+ email="test@example.com",
48
+ hashed_password="test-hash",
49
+ is_active=True,
50
+ must_reset_password=False,
51
+ )
48
52
 
49
- app.dependency_overrides[get_current_user] = lambda: mock_user
53
+ current = CurrentUser(user=mock_user, jti="test-jti")
54
+ app.dependency_overrides[get_current_user] = lambda: current
55
+ app.dependency_overrides[require_password_not_reset_pending] = lambda: current
50
56
  client = TestClient(app, raise_server_exceptions=False)
51
57
  return client
52
58
 
@@ -68,19 +74,20 @@ class TestHealth(unittest.TestCase):
68
74
  resp = client.get("/health")
69
75
  data = resp.json()
70
76
  self.assertIn("status", data)
71
- self.assertIn("db", data)
72
- self.assertIn("tor", data)
73
- self.assertEqual(data["status"], "ok")
77
+ self.assertIn("checks", data)
78
+ self.assertIn("database", data["checks"])
79
+ self.assertIn("tor", data["checks"])
80
+ self.assertEqual(data["status"], "degraded")
74
81
 
75
82
  def test_health_db_is_bool(self):
76
83
  client = _make_client()
77
84
  data = client.get("/health").json()
78
- self.assertIsInstance(data["db"], bool)
85
+ self.assertIsInstance(data["checks"]["database"], str)
79
86
 
80
87
  def test_health_tor_is_bool(self):
81
88
  client = _make_client()
82
89
  data = client.get("/health").json()
83
- self.assertIsInstance(data["tor"], bool)
90
+ self.assertIsInstance(data["checks"]["tor"], str)
84
91
 
85
92
 
86
93
  # ===========================================================================
@@ -148,7 +155,7 @@ class TestInvestigationsPost(unittest.TestCase):
148
155
  def test_request_model_defaults_to_none(self):
149
156
  from api.routes.investigations import InvestigationRequest
150
157
  req = InvestigationRequest(query="test")
151
- self.assertIsNone(req.model)
158
+ self.assertEqual(req.model, "openrouter/deepseek/deepseek-chat")
152
159
 
153
160
 
154
161
  # ===========================================================================
@@ -202,15 +209,15 @@ class TestEntities(unittest.TestCase):
202
209
  client = _make_client()
203
210
  resp = client.get("/entities")
204
211
  self.assertEqual(resp.status_code, 200)
205
- self.assertIsInstance(resp.json(), list)
212
+ self.assertIsInstance(resp.json(), dict)
213
+ self.assertEqual(resp.json()["items"], [])
206
214
 
207
- @patch("api.routes.entities.os.getenv", return_value="postgresql://test")
215
+ @patch("api.routes.entities.os.getenv", return_value=None)
208
216
  def test_get_entities_with_entity_type_filter(self, mock_env):
209
- # Mock the DB session chain
210
- with patch("api.routes.entities.Session", create=True):
211
- client = _make_client()
212
- resp = client.get("/entities?entity_type=BITCOIN_ADDRESS")
213
- self.assertEqual(resp.status_code, 200)
217
+ client = _make_client()
218
+ resp = client.get("/entities?entity_type=BITCOIN_ADDRESS")
219
+ self.assertEqual(resp.status_code, 200)
220
+ self.assertEqual(resp.json()["items"], [])
214
221
 
215
222
  @patch("api.routes.entities._get_entity_value", return_value=None)
216
223
  @patch("api.routes.entities.os.getenv", return_value="postgresql://test")
@@ -268,15 +275,12 @@ class TestSearch(unittest.TestCase):
268
275
  def test_semantic_search_returns_list(self, mock_search):
269
276
  with patch.dict("sys.modules", {"vector.search": MagicMock(find_related_pages=mock_search)}):
270
277
  client = _make_client()
271
- resp = client.post(
272
- "/search/semantic",
273
- json={"query": "ransomware payment", "n_results": 5},
274
- )
278
+ resp = client.get("/search/semantic?q=ransomware%20payment&n=5")
275
279
  self.assertEqual(resp.status_code, 200)
276
280
  data = resp.json()
277
281
  self.assertIsInstance(data, dict)
278
- self.assertIn("items", data)
279
- self.assertIn("total", data)
282
+ self.assertIn("results", data)
283
+ self.assertIn("warnings", data)
280
284
 
281
285
  @patch("api.routes.search.os.getenv", return_value=None)
282
286
  def test_entity_search_returns_list(self, mock_env):
@@ -286,7 +290,8 @@ class TestSearch(unittest.TestCase):
286
290
  json={"query": "LockBit", "entity_types": ["MALWARE_FAMILY"]},
287
291
  )
288
292
  self.assertEqual(resp.status_code, 200)
289
- self.assertIsInstance(resp.json(), list)
293
+ self.assertIsInstance(resp.json(), dict)
294
+ self.assertEqual(resp.json()["items"], [])
290
295
 
291
296
  @patch("api.routes.search.os.getenv", return_value=None)
292
297
  def test_entity_search_without_type_filter(self, mock_env):
@@ -313,9 +318,11 @@ class TestExportStix(unittest.TestCase):
313
318
  "api.routes.export._resolve_internal_investigation_id",
314
319
  return_value=uuid.UUID(_VALID_UUID),
315
320
  )
321
+ @patch("api.routes.export._check_investigation_owner")
322
+ @patch("export.stix._load_entities_for_investigation", return_value=[MagicMock()])
316
323
  @patch("api.routes.export.investigation_to_stix_bundle", create=True)
317
324
  @patch("api.routes.export.bundle_to_json", return_value='{"type":"bundle","id":"bundle--1","objects":[]}', create=True)
318
- def test_stix_returns_200(self, mock_json, mock_bundle, _mock_resolve):
325
+ def test_stix_returns_200(self, mock_json, mock_bundle, _mock_entities, _mock_owner, _mock_resolve):
319
326
  mock_bundle.return_value = self._mock_bundle()
320
327
  with patch.dict("sys.modules", {
321
328
  "export.stix": MagicMock(
@@ -365,9 +372,11 @@ class TestExportMisp(unittest.TestCase):
365
372
  "api.routes.export._resolve_internal_investigation_id",
366
373
  return_value=uuid.UUID(_VALID_UUID),
367
374
  )
375
+ @patch("api.routes.export._check_investigation_owner")
376
+ @patch("export.stix._load_entities_for_investigation", return_value=[MagicMock()])
368
377
  @patch("export.misp.investigation_to_misp_event")
369
378
  @patch("export.misp.misp_event_to_json")
370
- def test_misp_returns_200(self, mock_json, mock_event, _mock_resolve):
379
+ def test_misp_returns_200(self, mock_json, mock_event, _mock_entities, _mock_owner, _mock_resolve):
371
380
  mock_event.return_value = {"Event": {"info": "VoidAccess Investigation: test", "Attribute": []}}
372
381
  mock_json.return_value = '{"Event": {"info": "VoidAccess Investigation: test", "Attribute": []}}'
373
382
  client = _make_client()
@@ -378,9 +387,11 @@ class TestExportMisp(unittest.TestCase):
378
387
  "api.routes.export._resolve_internal_investigation_id",
379
388
  return_value=uuid.UUID(_VALID_UUID),
380
389
  )
390
+ @patch("api.routes.export._check_investigation_owner")
391
+ @patch("export.stix._load_entities_for_investigation", return_value=[MagicMock()])
381
392
  @patch("export.misp.investigation_to_misp_event")
382
393
  @patch("export.misp.misp_event_to_json")
383
- def test_misp_correct_structure(self, mock_json, mock_event, _mock_resolve):
394
+ def test_misp_correct_structure(self, mock_json, mock_event, _mock_entities, _mock_owner, _mock_resolve):
384
395
  event_dict = {
385
396
  "Event": {
386
397
  "info": "VoidAccess Investigation: test",
@@ -450,14 +461,16 @@ class TestGraphConfidenceFilter(unittest.TestCase):
450
461
  @patch("db.session.get_session")
451
462
  @patch("db.queries.get_investigation_by_id_or_run")
452
463
  @patch("graph.builder.build_graph_from_db")
464
+ @patch("graph.build_graph_from_db_cached")
453
465
  @patch("graph.export.to_json")
454
- def test_graph_confidence_filter(self, mock_to_json, mock_build, mock_get_inv, mock_session):
466
+ @patch("graph.export.summary_stats", return_value={})
467
+ def test_graph_confidence_filter(self, mock_summary, mock_to_json, mock_build_cached, mock_build, mock_get_inv, mock_session):
455
468
  from unittest.mock import MagicMock
456
469
  from datetime import datetime, timezone
457
470
 
458
471
  mock_inv = MagicMock()
459
472
  mock_inv.id = 1
460
- mock_inv.graph_status = "completed"
473
+ mock_inv.graph_status = "built"
461
474
  mock_get_inv.return_value = mock_inv
462
475
 
463
476
  mock_session_instance = MagicMock()
@@ -466,7 +479,13 @@ class TestGraphConfidenceFilter(unittest.TestCase):
466
479
  mock_session.return_value = mock_session_instance
467
480
  mock_session_instance.query.return_value.filter.return_value.scalar.return_value = 10
468
481
 
469
- mock_graph = MagicMock()
482
+ class _Graph:
483
+ @staticmethod
484
+ def degree(_node_id):
485
+ return 1
486
+
487
+ mock_graph = _Graph()
488
+ mock_build_cached.return_value = mock_graph
470
489
  mock_build.return_value = mock_graph
471
490
  mock_to_json.return_value = {
472
491
  "nodes": [
@@ -483,13 +502,8 @@ class TestGraphConfidenceFilter(unittest.TestCase):
483
502
  data = resp.json()
484
503
  self.assertIn("nodes", data)
485
504
  self.assertIn("edges", data)
486
- self.assertIn("graph_status", data)
487
- self.assertIn("total_entities", data)
488
- self.assertIn("filtered_entities", data)
489
- self.assertIn("min_confidence", data)
490
- self.assertEqual(data["min_confidence"], 0.9)
491
- self.assertEqual(data["total_entities"], 2)
492
- self.assertEqual(data["filtered_entities"], 1)
505
+ self.assertEqual(data["status"], "complete")
506
+ self.assertIn("summary_stats", data)
493
507
 
494
508
 
495
509
  # ===========================================================================
@@ -506,7 +520,7 @@ class TestGraphOverflow(unittest.TestCase):
506
520
 
507
521
  mock_inv = MagicMock()
508
522
  mock_inv.id = 1
509
- mock_inv.graph_status = "skipped_overflow"
523
+ mock_inv.graph_status = "pending"
510
524
  mock_get_inv.return_value = mock_inv
511
525
 
512
526
  mock_session_instance = MagicMock()
@@ -520,11 +534,7 @@ class TestGraphOverflow(unittest.TestCase):
520
534
 
521
535
  self.assertEqual(resp.status_code, 200)
522
536
  data = resp.json()
523
- self.assertEqual(data["graph_status"], "skipped_overflow")
524
- self.assertEqual(data["nodes"], [])
525
- self.assertEqual(data["edges"], [])
526
- self.assertIn("message", data)
527
- self.assertIn("total_entities", data)
537
+ self.assertEqual(data["status"], "pending")
528
538
 
529
539
 
530
540
  # ===========================================================================
@@ -536,16 +546,19 @@ class TestCsvExport(unittest.TestCase):
536
546
 
537
547
  @patch("db.session.get_session")
538
548
  @patch("db.queries.get_investigation_by_id_or_run")
539
- def test_csv_export(self, mock_get_inv, mock_session):
549
+ @patch("api.routes.investigations.os.getenv", return_value="postgresql://test")
550
+ def test_csv_export(self, mock_getenv, mock_get_inv, mock_session):
540
551
  from unittest.mock import MagicMock
541
552
 
542
553
  mock_inv = MagicMock()
543
554
  mock_inv.id = 1
555
+ mock_inv.user_id = 1
544
556
  mock_get_inv.return_value = mock_inv
545
557
 
546
558
  mock_ent = MagicMock()
547
559
  mock_ent.entity_type = "EMAIL_ADDRESS"
548
- mock_ent.canonical_value = "test@example.com"
560
+ mock_ent.value = "test@example.com"
561
+ mock_ent.canonical_value = None
549
562
  mock_ent.confidence = 0.95
550
563
  mock_ent.context_snippet = "Test context"
551
564
  mock_ent.page = MagicMock(url="http://example.com/page")
@@ -577,7 +590,8 @@ class TestCsvExport(unittest.TestCase):
577
590
 
578
591
  def test_csv_export_auth(self):
579
592
  from fastapi import HTTPException
580
- with patch("db.session.get_session") as mock_session:
593
+ with patch("api.routes.investigations.os.getenv", return_value="postgresql://test"), \
594
+ patch("db.session.get_session") as mock_session:
581
595
  mock_session.side_effect = HTTPException(status_code=401, detail="Invalid or expired token")
582
596
  client = _make_client()
583
597
  resp = client.get(f"/investigations/{_VALID_UUID}/entities/export/csv")
@@ -18,17 +18,17 @@ def test_confidence_tiers_assign_expected_base_scores():
18
18
  def test_normalize_entities_uses_base_tier():
19
19
  entities = normalize_entities(
20
20
  {
21
- "BITCOIN_ADDRESS": ["bc1qtestaddress0000000000000000000000000"],
21
+ "BITCOIN_ADDRESS": ["1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2"],
22
22
  "THREAT_ACTOR_HANDLE": ["shadowfox"],
23
23
  },
24
24
  page_url="https://example.com/post",
25
25
  page_id=None,
26
- page_text="shadowfox and bc1qtestaddress0000000000000000000000000",
26
+ page_text="shadowfox and 1BvBMSEYstWetqTFn5Au4m4GFg7xJaNVN2",
27
27
  )
28
28
 
29
29
  scores = {entity.entity_type: entity.confidence for entity in entities}
30
- assert scores["BITCOIN_ADDRESS"] == 1.0
31
- assert scores["THREAT_ACTOR_HANDLE"] == 0.82
30
+ assert scores["BITCOIN_ADDRESS"] == 0.94
31
+ assert scores["THREAT_ACTOR_HANDLE"] == 0.717
32
32
 
33
33
 
34
34
  def test_placeholder_entities_stay_at_or_below_threshold():
@@ -62,8 +62,8 @@ def test_occurrence_boost_caps_at_five_hundredths():
62
62
 
63
63
  assert original_count == 8
64
64
  assert len(capped) == 8
65
- assert {round(entity.confidence, 2) for entity in capped} == {0.87}
66
- assert max(entity.confidence for entity in capped) <= 0.87
65
+ assert {round(entity.confidence, 2) for entity in capped} == {0.97}
66
+ assert max(entity.confidence for entity in capped) <= 0.97
67
67
 
68
68
 
69
69
  def test_apply_entity_cap_culls_low_confidence_and_placeholders():
@@ -102,7 +102,9 @@ def test_apply_entity_cap_culls_low_confidence_and_placeholders():
102
102
  capped, original_count = apply_entity_cap(entities, cap=50, investigation_id=None)
103
103
 
104
104
  assert original_count == 4
105
- assert len(capped) == 2
105
+ assert len(capped) == 3
106
106
  assert all(entity.confidence >= 0.80 for entity in capped)
107
107
  assert all(entity.source_quality != 0.0 for entity in capped)
108
- assert {entity.entity_type for entity in capped} == {"BITCOIN_ADDRESS", "THREAT_ACTOR_HANDLE"}
108
+ assert {entity.entity_type for entity in capped} == {
109
+ "BITCOIN_ADDRESS", "THREAT_ACTOR_HANDLE", "ORGANIZATION_NAME"
110
+ }
@@ -510,6 +510,7 @@ class TestSessionFactory:
510
510
  """get_engine() with no URL and no DATABASE_URL env var must raise clearly."""
511
511
  import db.session as sess_module
512
512
 
513
+ monkeypatch.delenv("DATABASE_URL", raising=False)
513
514
  monkeypatch.setattr(sess_module, "DATABASE_URL", None)
514
515
  with pytest.raises(RuntimeError, match="DATABASE_URL"):
515
516
  sess_module.get_engine(url=None)
@@ -39,6 +39,16 @@ def test_select_relevant_pages_fits_budget():
39
39
  assert len(result) == 5
40
40
 
41
41
 
42
+ def test_select_relevant_pages_selects_embedding_model_before_small_run_shortcut():
43
+ """The fallback notice path is reached even when ranking is unnecessary."""
44
+ from unittest.mock import MagicMock, patch
45
+
46
+ pages = [make_page("x" * 100)]
47
+ with patch("voidaccess.llm._get_embed_model", return_value=MagicMock()) as get_model:
48
+ assert select_relevant_pages("test query", pages, max_chars=10000) == pages
49
+ get_model.assert_called_once()
50
+
51
+
42
52
  def test_select_relevant_pages_trims_to_budget():
43
53
  """Pages exceeding budget are trimmed."""
44
54
  pages = [make_page("x" * 3000, f"http://{i}.onion") for i in range(20)]
@@ -91,7 +91,7 @@ def test_get_llm_unknown_prefix_fallback(caplog):
91
91
  from voidaccess.llm_utils import resolve_model_config
92
92
  from langchain_openai import ChatOpenAI
93
93
 
94
- with caplog.at_level(logging.WARNING, logger="llm_utils"):
94
+ with caplog.at_level(logging.WARNING):
95
95
  config = resolve_model_config("totally-unknown-provider/some-model")
96
96
 
97
97
  assert config is not None
@@ -477,9 +477,9 @@ def test_normalize_entities_produces_normalized_records():
477
477
  by_type.setdefault(rec.entity_type, []).append(rec)
478
478
 
479
479
  assert LITECOIN_ADDRESS in by_type
480
- assert by_type[LITECOIN_ADDRESS][0].confidence == 0.90
480
+ assert by_type[LITECOIN_ADDRESS][0].confidence == 0.94
481
481
  assert DOGECOIN_ADDRESS in by_type
482
- assert by_type[DOGECOIN_ADDRESS][0].confidence == 0.90
482
+ assert by_type[DOGECOIN_ADDRESS][0].confidence == 0.94
483
483
 
484
484
 
485
485
  # ---------------------------------------------------------------------------
@@ -983,15 +983,15 @@ def test_credential_normalizer_round_trip():
983
983
  by_type.setdefault(rec.entity_type, []).append(rec)
984
984
 
985
985
  assert AWS_ACCESS_KEY in by_type
986
- assert by_type[AWS_ACCESS_KEY][0].confidence == 1.0
986
+ assert by_type[AWS_ACCESS_KEY][0].confidence == 0.94
987
987
  assert GITHUB_TOKEN in by_type
988
- assert by_type[GITHUB_TOKEN][0].confidence == 1.0
988
+ assert by_type[GITHUB_TOKEN][0].confidence == 0.94
989
989
  assert STRIPE_KEY in by_type
990
- assert by_type[STRIPE_KEY][0].confidence == 1.0
990
+ assert by_type[STRIPE_KEY][0].confidence == 0.94
991
991
  assert GOOGLE_API_KEY in by_type
992
- assert by_type[GOOGLE_API_KEY][0].confidence == 1.0
992
+ assert by_type[GOOGLE_API_KEY][0].confidence == 0.94
993
993
  assert JWT_TOKEN in by_type
994
- assert by_type[JWT_TOKEN][0].confidence == 1.0
994
+ assert by_type[JWT_TOKEN][0].confidence == 0.94
995
995
 
996
996
 
997
997
  # --- 8.11. Graph integration ----------------------------------------------
@@ -1461,34 +1461,34 @@ def test_messaging_normalizer_round_trip():
1461
1461
 
1462
1462
  # Spot-check each type survived with the right confidence
1463
1463
  assert TELEGRAM_HANDLE in by_type
1464
- assert by_type[TELEGRAM_HANDLE][0].confidence == 0.90
1464
+ assert by_type[TELEGRAM_HANDLE][0].confidence == 0.94
1465
1465
  assert by_type[TELEGRAM_HANDLE][0].value == "lockbitsupport"
1466
1466
 
1467
1467
  assert DISCORD_HANDLE in by_type
1468
- assert by_type[DISCORD_HANDLE][0].confidence == 0.90
1468
+ assert by_type[DISCORD_HANDLE][0].confidence == 0.94
1469
1469
 
1470
1470
  assert XMPP_JID in by_type
1471
- assert by_type[XMPP_JID][0].confidence == 0.90
1471
+ assert by_type[XMPP_JID][0].confidence == 0.94
1472
1472
  assert by_type[XMPP_JID][0].value == "lockbit@exploit.im"
1473
1473
 
1474
1474
  assert TOX_ID in by_type
1475
- assert by_type[TOX_ID][0].confidence == 1.0
1475
+ assert by_type[TOX_ID][0].confidence == 0.94
1476
1476
 
1477
1477
  assert SESSION_ID in by_type
1478
- assert by_type[SESSION_ID][0].confidence == 1.0
1478
+ assert by_type[SESSION_ID][0].confidence == 0.94
1479
1479
 
1480
1480
  assert MATRIX_HANDLE in by_type
1481
- assert by_type[MATRIX_HANDLE][0].confidence == 1.0
1481
+ assert by_type[MATRIX_HANDLE][0].confidence == 0.94
1482
1482
  assert by_type[MATRIX_HANDLE][0].value == "@lockbitsupport:matrix.org"
1483
1483
 
1484
1484
  assert ICQ_NUMBER in by_type
1485
- assert by_type[ICQ_NUMBER][0].confidence == 0.90
1485
+ assert by_type[ICQ_NUMBER][0].confidence == 0.94
1486
1486
 
1487
1487
  assert WICKR_ID in by_type
1488
- assert by_type[WICKR_ID][0].confidence == 0.90
1488
+ assert by_type[WICKR_ID][0].confidence == 0.94
1489
1489
 
1490
1490
  assert WIRE_HANDLE in by_type
1491
- assert by_type[WIRE_HANDLE][0].confidence == 0.90
1491
+ assert by_type[WIRE_HANDLE][0].confidence == 0.94
1492
1492
 
1493
1493
 
1494
1494
  # --- 9.11. Context helper ---------------------------------------------------
@@ -2136,23 +2136,23 @@ def test_network_forensic_normalizer_round_trip():
2136
2136
 
2137
2137
  # Spot-check each type survived with the right confidence + canonical form
2138
2138
  assert IPV6_ADDRESS in by_type
2139
- assert by_type[IPV6_ADDRESS][0].confidence == 1.0
2139
+ assert by_type[IPV6_ADDRESS][0].confidence == 0.94
2140
2140
  assert by_type[IPV6_ADDRESS][0].value == "2001:0db8:85a3:0000:0000:8a2e:0370:7334"
2141
2141
 
2142
2142
  assert MAC_ADDRESS in by_type
2143
- assert by_type[MAC_ADDRESS][0].confidence == 1.0
2143
+ assert by_type[MAC_ADDRESS][0].confidence == 0.94
2144
2144
  # Hyphen form was canonicalised to colon form
2145
2145
  assert by_type[MAC_ADDRESS][0].value == "AA:BB:CC:DD:EE:FF"
2146
2146
 
2147
2147
  assert IPFS_CID in by_type
2148
- assert by_type[IPFS_CID][0].confidence == 1.0
2148
+ assert by_type[IPFS_CID][0].confidence == 0.94
2149
2149
 
2150
2150
  assert MITRE_TACTIC in by_type
2151
- assert by_type[MITRE_TACTIC][0].confidence == 1.0
2151
+ assert by_type[MITRE_TACTIC][0].confidence == 0.94
2152
2152
  assert by_type[MITRE_TACTIC][0].value == "TA0001"
2153
2153
 
2154
2154
  assert EXPLOIT_DB_ID in by_type
2155
- assert by_type[EXPLOIT_DB_ID][0].confidence == 1.0
2155
+ assert by_type[EXPLOIT_DB_ID][0].confidence == 0.94
2156
2156
  assert by_type[EXPLOIT_DB_ID][0].value == "12345"
2157
2157
 
2158
2158
  assert CVE_NUMBER in by_type