voidaccess 1.9.0__tar.gz → 1.9.1__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 (228) hide show
  1. {voidaccess-1.9.0/voidaccess.egg-info → voidaccess-1.9.1}/PKG-INFO +1 -1
  2. {voidaccess-1.9.0 → voidaccess-1.9.1}/config.py +11 -18
  3. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/session.py +17 -4
  4. {voidaccess-1.9.0 → voidaccess-1.9.1}/export/ioc_package.py +1 -1
  5. {voidaccess-1.9.0 → voidaccess-1.9.1}/pyproject.toml +1 -1
  6. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_config.py +24 -1
  7. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_llm.py +13 -0
  8. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess/config.py +4 -0
  9. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess/llm.py +26 -5
  10. {voidaccess-1.9.0 → voidaccess-1.9.1/voidaccess.egg-info}/PKG-INFO +1 -1
  11. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess_cli/__init__.py +1 -1
  12. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess_cli/commands/investigate.py +58 -10
  13. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess_cli/commands/show.py +2 -0
  14. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess_cli/config.py +4 -4
  15. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess_cli/main.py +5 -3
  16. {voidaccess-1.9.0 → voidaccess-1.9.1}/LICENSE +0 -0
  17. {voidaccess-1.9.0 → voidaccess-1.9.1}/README.md +0 -0
  18. {voidaccess-1.9.0 → voidaccess-1.9.1}/analysis/__init__.py +0 -0
  19. {voidaccess-1.9.0 → voidaccess-1.9.1}/analysis/opsec.py +0 -0
  20. {voidaccess-1.9.0 → voidaccess-1.9.1}/analysis/patterns.py +0 -0
  21. {voidaccess-1.9.0 → voidaccess-1.9.1}/analysis/temporal.py +0 -0
  22. {voidaccess-1.9.0 → voidaccess-1.9.1}/api/__init__.py +0 -0
  23. {voidaccess-1.9.0 → voidaccess-1.9.1}/api/auth.py +0 -0
  24. {voidaccess-1.9.0 → voidaccess-1.9.1}/api/errors.py +0 -0
  25. {voidaccess-1.9.0 → voidaccess-1.9.1}/api/main.py +0 -0
  26. {voidaccess-1.9.0 → voidaccess-1.9.1}/api/routes/__init__.py +0 -0
  27. {voidaccess-1.9.0 → voidaccess-1.9.1}/api/routes/actors.py +0 -0
  28. {voidaccess-1.9.0 → voidaccess-1.9.1}/api/routes/admin.py +0 -0
  29. {voidaccess-1.9.0 → voidaccess-1.9.1}/api/routes/auth.py +0 -0
  30. {voidaccess-1.9.0 → voidaccess-1.9.1}/api/routes/entities.py +0 -0
  31. {voidaccess-1.9.0 → voidaccess-1.9.1}/api/routes/export.py +0 -0
  32. {voidaccess-1.9.0 → voidaccess-1.9.1}/api/routes/investigations.py +0 -0
  33. {voidaccess-1.9.0 → voidaccess-1.9.1}/api/routes/monitors.py +0 -0
  34. {voidaccess-1.9.0 → voidaccess-1.9.1}/api/routes/search.py +0 -0
  35. {voidaccess-1.9.0 → voidaccess-1.9.1}/api/routes/settings.py +0 -0
  36. {voidaccess-1.9.0 → voidaccess-1.9.1}/auth/__init__.py +0 -0
  37. {voidaccess-1.9.0 → voidaccess-1.9.1}/auth/token_blacklist.py +0 -0
  38. {voidaccess-1.9.0 → voidaccess-1.9.1}/crawler/__init__.py +0 -0
  39. {voidaccess-1.9.0 → voidaccess-1.9.1}/crawler/dedup.py +0 -0
  40. {voidaccess-1.9.0 → voidaccess-1.9.1}/crawler/frontier.py +0 -0
  41. {voidaccess-1.9.0 → voidaccess-1.9.1}/crawler/spider.py +0 -0
  42. {voidaccess-1.9.0 → voidaccess-1.9.1}/crawler/utils.py +0 -0
  43. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/__init__.py +0 -0
  44. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/__init__.py +0 -0
  45. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/env.py +0 -0
  46. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0001_initial_schema.py +0 -0
  47. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0002_add_investigation_status_column.py +0 -0
  48. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0002_add_missing_tables.py +0 -0
  49. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0003_add_canonical_value_and_entity_links.py +0 -0
  50. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0004_add_page_posted_at.py +0 -0
  51. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0005_add_extraction_method.py +0 -0
  52. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0006_add_monitor_alerts.py +0 -0
  53. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0007_add_actor_style_profiles.py +0 -0
  54. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0008_add_users_table.py +0 -0
  55. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0009_add_investigation_id_to_relationships.py +0 -0
  56. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0010_add_composite_index_entity_relationships.py +0 -0
  57. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0011_add_page_extraction_cache.py +0 -0
  58. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0013_add_graph_status.py +0 -0
  59. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0015_add_progress_fields.py +0 -0
  60. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0016_backfill_graph_status.py +0 -0
  61. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0017_add_user_api_keys.py +0 -0
  62. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0018_add_user_id_to_investigations.py +0 -0
  63. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0019_add_content_safety_log.py +0 -0
  64. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0020_add_entity_source_tracking.py +0 -0
  65. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0021_add_search_engine_stats.py +0 -0
  66. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0022_add_actor_profiles.py +0 -0
  67. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0023_add_investigation_metadata.py +0 -0
  68. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0024_add_investigation_step_metrics.py +0 -0
  69. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/migrations/versions/0025_add_cancellation_requested.py +0 -0
  70. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/models.py +0 -0
  71. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/queries.py +0 -0
  72. {voidaccess-1.9.0 → voidaccess-1.9.1}/db/search_engine_stats.py +0 -0
  73. {voidaccess-1.9.0 → voidaccess-1.9.1}/export/__init__.py +0 -0
  74. {voidaccess-1.9.0 → voidaccess-1.9.1}/export/_entity_loading.py +0 -0
  75. {voidaccess-1.9.0 → voidaccess-1.9.1}/export/misp.py +0 -0
  76. {voidaccess-1.9.0 → voidaccess-1.9.1}/export/sigma.py +0 -0
  77. {voidaccess-1.9.0 → voidaccess-1.9.1}/export/snort_export.py +0 -0
  78. {voidaccess-1.9.0 → voidaccess-1.9.1}/export/stix.py +0 -0
  79. {voidaccess-1.9.0 → voidaccess-1.9.1}/export/yara_export.py +0 -0
  80. {voidaccess-1.9.0 → voidaccess-1.9.1}/extractor/__init__.py +0 -0
  81. {voidaccess-1.9.0 → voidaccess-1.9.1}/extractor/confidence.py +0 -0
  82. {voidaccess-1.9.0 → voidaccess-1.9.1}/extractor/entity_shape.py +0 -0
  83. {voidaccess-1.9.0 → voidaccess-1.9.1}/extractor/gazetteer.py +0 -0
  84. {voidaccess-1.9.0 → voidaccess-1.9.1}/extractor/identity.py +0 -0
  85. {voidaccess-1.9.0 → voidaccess-1.9.1}/extractor/llm_extract.py +0 -0
  86. {voidaccess-1.9.0 → voidaccess-1.9.1}/extractor/ner.py +0 -0
  87. {voidaccess-1.9.0 → voidaccess-1.9.1}/extractor/normalizer.py +0 -0
  88. {voidaccess-1.9.0 → voidaccess-1.9.1}/extractor/pipeline.py +0 -0
  89. {voidaccess-1.9.0 → voidaccess-1.9.1}/extractor/regex_patterns.py +0 -0
  90. {voidaccess-1.9.0 → voidaccess-1.9.1}/extractor/relationship_extract.py +0 -0
  91. {voidaccess-1.9.0 → voidaccess-1.9.1}/fingerprint/__init__.py +0 -0
  92. {voidaccess-1.9.0 → voidaccess-1.9.1}/fingerprint/calibration.py +0 -0
  93. {voidaccess-1.9.0 → voidaccess-1.9.1}/fingerprint/profiler.py +0 -0
  94. {voidaccess-1.9.0 → voidaccess-1.9.1}/fingerprint/stylometry.py +0 -0
  95. {voidaccess-1.9.0 → voidaccess-1.9.1}/graph/__init__.py +0 -0
  96. {voidaccess-1.9.0 → voidaccess-1.9.1}/graph/builder.py +0 -0
  97. {voidaccess-1.9.0 → voidaccess-1.9.1}/graph/export.py +0 -0
  98. {voidaccess-1.9.0 → voidaccess-1.9.1}/graph/model.py +0 -0
  99. {voidaccess-1.9.0 → voidaccess-1.9.1}/graph/queries.py +0 -0
  100. {voidaccess-1.9.0 → voidaccess-1.9.1}/graph/visualize.py +0 -0
  101. {voidaccess-1.9.0 → voidaccess-1.9.1}/i18n/__init__.py +0 -0
  102. {voidaccess-1.9.0 → voidaccess-1.9.1}/i18n/detect.py +0 -0
  103. {voidaccess-1.9.0 → voidaccess-1.9.1}/i18n/query_expand.py +0 -0
  104. {voidaccess-1.9.0 → voidaccess-1.9.1}/i18n/translate.py +0 -0
  105. {voidaccess-1.9.0 → voidaccess-1.9.1}/monitor/__init__.py +0 -0
  106. {voidaccess-1.9.0 → voidaccess-1.9.1}/monitor/_db.py +0 -0
  107. {voidaccess-1.9.0 → voidaccess-1.9.1}/monitor/alerts.py +0 -0
  108. {voidaccess-1.9.0 → voidaccess-1.9.1}/monitor/config.py +0 -0
  109. {voidaccess-1.9.0 → voidaccess-1.9.1}/monitor/diff.py +0 -0
  110. {voidaccess-1.9.0 → voidaccess-1.9.1}/monitor/jobs.py +0 -0
  111. {voidaccess-1.9.0 → voidaccess-1.9.1}/monitor/scheduler.py +0 -0
  112. {voidaccess-1.9.0 → voidaccess-1.9.1}/scraper/__init__.py +0 -0
  113. {voidaccess-1.9.0 → voidaccess-1.9.1}/scraper/scrape.py +0 -0
  114. {voidaccess-1.9.0 → voidaccess-1.9.1}/scraper/scrape_js.py +0 -0
  115. {voidaccess-1.9.0 → voidaccess-1.9.1}/search/__init__.py +0 -0
  116. {voidaccess-1.9.0 → voidaccess-1.9.1}/search/circuit_breaker.py +0 -0
  117. {voidaccess-1.9.0 → voidaccess-1.9.1}/search/engine_catalog.py +0 -0
  118. {voidaccess-1.9.0 → voidaccess-1.9.1}/search/query_builder.py +0 -0
  119. {voidaccess-1.9.0 → voidaccess-1.9.1}/search/search.py +0 -0
  120. {voidaccess-1.9.0 → voidaccess-1.9.1}/setup.cfg +0 -0
  121. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/__init__.py +0 -0
  122. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/actor_profiles.py +0 -0
  123. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/blockchain.py +0 -0
  124. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/breach_lookup.py +0 -0
  125. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/cache.py +0 -0
  126. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/cisa.py +0 -0
  127. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/data/__init__.py +0 -0
  128. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/data/onion_seeds.json +0 -0
  129. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/dns_enrichment.py +0 -0
  130. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/domain_reputation.py +0 -0
  131. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/email_reputation.py +0 -0
  132. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/engines.py +0 -0
  133. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/enrichment.py +0 -0
  134. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/github_scraper.py +0 -0
  135. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/gitlab_scraper.py +0 -0
  136. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/hash_reputation.py +0 -0
  137. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/historical_intel.py +0 -0
  138. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/infostealer.py +0 -0
  139. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/ip_reputation.py +0 -0
  140. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/nvd.py +0 -0
  141. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/paste_scraper.py +0 -0
  142. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/pastes.py +0 -0
  143. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/proxy_client.py +0 -0
  144. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/rss_scraper.py +0 -0
  145. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/seed_manager.py +0 -0
  146. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/seeds.py +0 -0
  147. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/shodan.py +0 -0
  148. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/telegram.py +0 -0
  149. {voidaccess-1.9.0 → voidaccess-1.9.1}/sources/virustotal.py +0 -0
  150. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_analysis_opsec.py +0 -0
  151. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_analysis_stylometry.py +0 -0
  152. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_analysis_temporal.py +0 -0
  153. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_api.py +0 -0
  154. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_api_monitors.py +0 -0
  155. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_blockchain.py +0 -0
  156. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_breach_lookup.py +0 -0
  157. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_cli_proxy_config.py +0 -0
  158. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_confidence_calibration.py +0 -0
  159. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_consolidated_contracts.py +0 -0
  160. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_crawler.py +0 -0
  161. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_db.py +0 -0
  162. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_dns_enrichment.py +0 -0
  163. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_domain_reputation.py +0 -0
  164. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_email_reputation.py +0 -0
  165. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_extractor_llm_pipeline.py +0 -0
  166. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_filter_e2e_manual.py +0 -0
  167. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_filter_parser_manual.py +0 -0
  168. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_fingerprint.py +0 -0
  169. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_github_scraper.py +0 -0
  170. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_gitlab_scraper.py +0 -0
  171. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_graph.py +0 -0
  172. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_graph_integration.py +0 -0
  173. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_hash_reputation.py +0 -0
  174. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_i18n.py +0 -0
  175. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_infostealer.py +0 -0
  176. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_ip_reputation.py +0 -0
  177. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_llm_utils.py +0 -0
  178. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_model_singleton.py +0 -0
  179. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_monitor.py +0 -0
  180. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_monitor_integration.py +0 -0
  181. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_nvd.py +0 -0
  182. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_pagination.py +0 -0
  183. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_paste_scraper.py +0 -0
  184. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_proxy_client.py +0 -0
  185. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_ransomlook.py +0 -0
  186. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_regex_patterns.py +0 -0
  187. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_relationship_extract.py +0 -0
  188. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_rss_scraper.py +0 -0
  189. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_scrape_js.py +0 -0
  190. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_settings.py +0 -0
  191. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_snort_export.py +0 -0
  192. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_sources.py +0 -0
  193. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_sources_enrichment_new.py +0 -0
  194. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_stix_export.py +0 -0
  195. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_vector.py +0 -0
  196. {voidaccess-1.9.0 → voidaccess-1.9.1}/tests/test_yara_export.py +0 -0
  197. {voidaccess-1.9.0 → voidaccess-1.9.1}/utils/__init__.py +0 -0
  198. {voidaccess-1.9.0 → voidaccess-1.9.1}/utils/async_utils.py +0 -0
  199. {voidaccess-1.9.0 → voidaccess-1.9.1}/utils/content_dedup.py +0 -0
  200. {voidaccess-1.9.0 → voidaccess-1.9.1}/utils/content_safety.py +0 -0
  201. {voidaccess-1.9.0 → voidaccess-1.9.1}/utils/defang.py +0 -0
  202. {voidaccess-1.9.0 → voidaccess-1.9.1}/utils/encryption.py +0 -0
  203. {voidaccess-1.9.0 → voidaccess-1.9.1}/utils/enrichment_cache.py +0 -0
  204. {voidaccess-1.9.0 → voidaccess-1.9.1}/utils/investigation_metrics.py +0 -0
  205. {voidaccess-1.9.0 → voidaccess-1.9.1}/utils/ioc_freshness.py +0 -0
  206. {voidaccess-1.9.0 → voidaccess-1.9.1}/utils/user_keys.py +0 -0
  207. {voidaccess-1.9.0 → voidaccess-1.9.1}/vector/__init__.py +0 -0
  208. {voidaccess-1.9.0 → voidaccess-1.9.1}/vector/embedder.py +0 -0
  209. {voidaccess-1.9.0 → voidaccess-1.9.1}/vector/model_singleton.py +0 -0
  210. {voidaccess-1.9.0 → voidaccess-1.9.1}/vector/search.py +0 -0
  211. {voidaccess-1.9.0 → voidaccess-1.9.1}/vector/store.py +0 -0
  212. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess/__init__.py +0 -0
  213. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess/llm_utils.py +0 -0
  214. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess.egg-info/SOURCES.txt +0 -0
  215. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess.egg-info/dependency_links.txt +0 -0
  216. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess.egg-info/entry_points.txt +0 -0
  217. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess.egg-info/requires.txt +0 -0
  218. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess.egg-info/top_level.txt +0 -0
  219. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess_cli/adapters/__init__.py +0 -0
  220. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess_cli/adapters/sqlite.py +0 -0
  221. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess_cli/browser.py +0 -0
  222. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess_cli/commands/__init__.py +0 -0
  223. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess_cli/commands/actors.py +0 -0
  224. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess_cli/commands/configure.py +0 -0
  225. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess_cli/commands/enrich.py +0 -0
  226. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess_cli/commands/export.py +0 -0
  227. {voidaccess-1.9.0 → voidaccess-1.9.1}/voidaccess_cli/display.py +0 -0
  228. {voidaccess-1.9.0 → voidaccess-1.9.1}/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.0
3
+ Version: 1.9.1
4
4
  Summary: Dark web OSINT CLI — automated threat intelligence from query to report
5
5
  Author: VoidAccess
6
6
  License-Expression: MIT
@@ -5,7 +5,7 @@ from dotenv import load_dotenv
5
5
  load_dotenv()
6
6
 
7
7
  logger = logging.getLogger(__name__)
8
- _OPTIONAL_CONFIG_WARNING_SHOWN = False
8
+ _OPTIONAL_CONFIG_WARNING_STATE = "_voidaccess_optional_config_warning_shown"
9
9
 
10
10
 
11
11
  def _clean_env(name, default=None):
@@ -151,16 +151,12 @@ except ValueError:
151
151
  BLOCKCYPHER_TOKEN = _clean_env("BLOCKCYPHER_TOKEN", "")
152
152
  ETHERSCAN_API_KEY = _clean_env("ETHERSCAN_API_KEY", "")
153
153
 
154
- # Auth — REQUIRED in production. Generate with: python -c "import secrets; print(secrets.token_hex(32))"
155
- _jwt_secret = _clean_env("JWT_SECRET")
156
- if _jwt_secret is None:
157
- raise RuntimeError(
158
- "JWT_SECRET environment variable is not set. "
159
- "Generate a secure secret with: python -c \"import secrets; print(secrets.token_hex(32))\" "
160
- "and set it as JWT_SECRET in your .env file. "
161
- "Do NOT use a random secret — it will change on restart and invalidate all issued tokens."
162
- )
163
- JWT_SECRET = _jwt_secret
154
+ # Auth — REQUIRED when the API/auth stack is actually used. Do not validate
155
+ # this at import time: the CLI injects its runtime environment in its entry
156
+ # point before importing dependent modules, and `--help`/`--version` must work
157
+ # on a fresh install. `validate_config()` and the API startup path enforce it
158
+ # at the point where authentication is needed.
159
+ JWT_SECRET = _clean_env("JWT_SECRET")
164
160
 
165
161
  # Token blacklist Redis (optional — omit to disable blacklist checks)
166
162
  REDIS_URL = _clean_env("REDIS_URL")
@@ -210,7 +206,6 @@ OPTIONAL_KEYS = [
210
206
 
211
207
 
212
208
  def validate_config():
213
- global _OPTIONAL_CONFIG_WARNING_SHOWN
214
209
  missing_required = []
215
210
  for key in REQUIRED_KEYS:
216
211
  if _clean_env(key) is None:
@@ -221,16 +216,14 @@ def validate_config():
221
216
  key for key in OPTIONAL_KEYS
222
217
  if (_clean_env(key) is None or _clean_env(key) == "")
223
218
  ]
224
- if missing_optional and not _OPTIONAL_CONFIG_WARNING_SHOWN:
219
+ if missing_optional and not getattr(logging, _OPTIONAL_CONFIG_WARNING_STATE, False):
225
220
  logger.warning(
226
221
  "Optional configuration keys not set (%d): %s - related features will be disabled",
227
222
  len(missing_optional),
228
223
  ", ".join(missing_optional),
229
224
  )
230
- _OPTIONAL_CONFIG_WARNING_SHOWN = True
231
-
232
-
233
- validate_config()
234
-
225
+ # Store the guard on the process-wide logging module so compatibility
226
+ # wrappers/reloads cannot reset it and reintroduce warning spam.
227
+ setattr(logging, _OPTIONAL_CONFIG_WARNING_STATE, True)
235
228
 
236
229
 
@@ -38,6 +38,7 @@ Usage (testing — pass an explicit URL to avoid needing DATABASE_URL in env)
38
38
  from __future__ import annotations
39
39
 
40
40
  from functools import lru_cache
41
+ import os
41
42
 
42
43
  from contextlib import asynccontextmanager, contextmanager
43
44
  from typing import AsyncGenerator, Generator, Optional
@@ -48,7 +49,19 @@ from sqlalchemy.orm import sessionmaker, Session
48
49
 
49
50
  import config
50
51
 
52
+ # Kept as a compatibility/testing hook. Production lookups below use the
53
+ # current environment so apply_env() can run before the first DB operation.
51
54
  DATABASE_URL = config.DATABASE_URL
55
+ _INITIAL_DATABASE_URL = DATABASE_URL
56
+
57
+
58
+ def _database_url() -> Optional[str]:
59
+ # Preserve the long-standing test/embedding hook of monkeypatching this
60
+ # module's DATABASE_URL. Otherwise, read the live environment so a caller
61
+ # can inject DATABASE_URL after db.session has been imported.
62
+ if DATABASE_URL != _INITIAL_DATABASE_URL:
63
+ return DATABASE_URL
64
+ return os.getenv("DATABASE_URL") or DATABASE_URL
52
65
  _async_engine_cache: dict[str, "AsyncEngine"] = {}
53
66
 
54
67
 
@@ -104,7 +117,7 @@ def get_engine(url: Optional[str] = None) -> Engine:
104
117
  PostgreSQL gets a connection pool tuned for the scraping workload.
105
118
  SQLite skips pool parameters that only apply to QueuePool.
106
119
  """
107
- target_url = url or DATABASE_URL
120
+ target_url = url or _database_url()
108
121
  if not target_url:
109
122
  raise RuntimeError(
110
123
  "DATABASE_URL is not configured.\n"
@@ -122,7 +135,7 @@ def release_engine(url: Optional[str] = None) -> None:
122
135
  Calls engine.dispose() to release connection pool resources and file handles,
123
136
  then clears the cache. Use this in test teardown to prevent leaks.
124
137
  """
125
- target_url = url or DATABASE_URL
138
+ target_url = url or _database_url()
126
139
  if target_url:
127
140
  try:
128
141
  engine = get_engine(target_url)
@@ -139,7 +152,7 @@ def get_async_engine(url: Optional[str] = None) -> "AsyncEngine":
139
152
  Converts postgresql:// to postgresql+asyncpg:// and sqlite:// to sqlite+aiosqlite://.
140
153
  """
141
154
 
142
- target_url = url or DATABASE_URL
155
+ target_url = url or _database_url()
143
156
  if not target_url:
144
157
  raise RuntimeError(
145
158
  "DATABASE_URL is not configured.\n"
@@ -185,7 +198,7 @@ def release_async_engine(url: Optional[str] = None) -> None:
185
198
 
186
199
  Calls engine.dispose() to release connection pool resources and file handles.
187
200
  """
188
- target_url = url or DATABASE_URL
201
+ target_url = url or _database_url()
189
202
  if target_url in _async_engine_cache:
190
203
  _async_engine_cache[target_url].dispose()
191
204
  del _async_engine_cache[target_url]
@@ -57,7 +57,7 @@ logger = logging.getLogger(__name__)
57
57
  # ---------------------------------------------------------------------------
58
58
 
59
59
  PACKAGE_FORMAT = "voidaccess-ioc-v1"
60
- PACKAGE_VERSION = "1.9.0"
60
+ PACKAGE_VERSION = "1.9.1"
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 = "1.9.0"
7
+ version = "1.9.1"
8
8
  description = "Dark web OSINT CLI — automated threat intelligence from query to report"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -3,6 +3,7 @@
3
3
  import os
4
4
  import sys
5
5
  import unittest
6
+ import logging
6
7
  from unittest.mock import patch, MagicMock
7
8
 
8
9
 
@@ -52,6 +53,28 @@ class TestConfigValidation(unittest.TestCase):
52
53
  warning_calls = str(mock_logger.warning.call_args_list)
53
54
  self.assertIn("OPENAI_API_KEY", warning_calls)
54
55
 
56
+ def test_optional_config_warning_is_once_per_process(self):
57
+ """Repeated validation must not spam the same process with warnings."""
58
+ import config
59
+
60
+ with patch.dict(os.environ, {"JWT_SECRET": "test-secret-key-123"}, clear=True):
61
+ # The guard intentionally lives on logging so compatibility-module
62
+ # reloads cannot reset it; reset it explicitly for this isolated
63
+ # regression test.
64
+ setattr(logging, "_voidaccess_optional_config_warning_shown", False)
65
+ try:
66
+ with patch.object(config, "logger") as mock_logger:
67
+ config.validate_config()
68
+ config.validate_config()
69
+
70
+ warning_calls = [
71
+ call for call in mock_logger.warning.call_args_list
72
+ if call.args and "Optional configuration keys not set" in str(call.args[0])
73
+ ]
74
+ self.assertEqual(len(warning_calls), 1)
75
+ finally:
76
+ setattr(logging, "_voidaccess_optional_config_warning_shown", False)
77
+
55
78
 
56
79
  if __name__ == "__main__":
57
- unittest.main()
80
+ unittest.main()
@@ -19,6 +19,19 @@ def test_select_relevant_pages_empty():
19
19
  assert result == []
20
20
 
21
21
 
22
+ def test_get_llm_uses_runtime_provider_key_when_api_keys_omitted(monkeypatch):
23
+ """A key injected after module import must reach the LangChain client."""
24
+ from unittest.mock import patch
25
+ from langchain_openai import ChatOpenAI
26
+ from voidaccess.llm import get_llm
27
+
28
+ monkeypatch.setenv("OPENROUTER_API_KEY", "runtime-openrouter-key")
29
+ with patch.object(ChatOpenAI, "__init__", return_value=None) as init:
30
+ get_llm("openrouter/test-model")
31
+
32
+ assert init.call_args.kwargs["api_key"] == "runtime-openrouter-key"
33
+
34
+
22
35
  def test_select_relevant_pages_fits_budget():
23
36
  """Pages that fit in budget are all returned."""
24
37
  pages = [make_page("a" * 100, f"http://{i}.onion") for i in range(5)]
@@ -2,6 +2,10 @@
2
2
 
3
3
  from importlib import import_module, reload
4
4
 
5
+ # Reload the compatibility target so explicit `importlib.reload` calls keep
6
+ # reflecting a test/application environment change. The warning guard itself
7
+ # lives on the process-wide logging module, so this reload cannot reintroduce
8
+ # warning spam.
5
9
  _config = reload(import_module("config"))
6
10
 
7
11
  for _name in dir(_config):
@@ -256,11 +256,32 @@ def get_llm(model_choice, api_keys: dict | None = None):
256
256
  "GOOGLE_API_KEY": "google_api_key",
257
257
  "GROQ_API_KEY": "groq_api_key",
258
258
  }
259
- if api_keys:
260
- for key_name, key_value in api_keys.items():
261
- if key_value and key_name in _ENV_TO_LANGCHAIN:
262
- param_name = _ENV_TO_LANGCHAIN[key_name]
263
- model_specific_params[param_name] = key_value
259
+ # Prefer explicitly supplied per-user keys, but also resolve the current
260
+ # process environment when callers omit api_keys. The CLI injects its
261
+ # saved config at runtime, so relying only on import-time llm_utils values
262
+ # silently drops a valid OPENROUTER_API_KEY.
263
+ runtime_keys = dict(api_keys or {})
264
+ base_url = str(model_specific_params.get("base_url") or "").lower()
265
+ if "openrouter" in base_url:
266
+ runtime_keys.setdefault("OPENROUTER_API_KEY", os.getenv("OPENROUTER_API_KEY", ""))
267
+ elif "groq.com" in base_url:
268
+ runtime_keys.setdefault("GROQ_API_KEY", os.getenv("GROQ_API_KEY", ""))
269
+ elif "ChatAnthropic" in getattr(llm_class, "__name__", ""):
270
+ runtime_keys.setdefault("ANTHROPIC_API_KEY", os.getenv("ANTHROPIC_API_KEY", ""))
271
+ elif "ChatGoogle" in getattr(llm_class, "__name__", ""):
272
+ runtime_keys.setdefault("GOOGLE_API_KEY", os.getenv("GOOGLE_API_KEY", ""))
273
+ elif "ChatOpenAI" in getattr(llm_class, "__name__", ""):
274
+ runtime_keys.setdefault("OPENAI_API_KEY", os.getenv("OPENAI_API_KEY", ""))
275
+
276
+ for key_name, key_value in runtime_keys.items():
277
+ if key_value and key_name in _ENV_TO_LANGCHAIN:
278
+ param_name = _ENV_TO_LANGCHAIN[key_name]
279
+ model_specific_params[param_name] = key_value
280
+ # ChatOpenAI uses `api_key` as the canonical constructor field;
281
+ # keep it synchronized with the mapped alias when a model config
282
+ # was created from stale import-time defaults.
283
+ if param_name == "openai_api_key":
284
+ model_specific_params["api_key"] = key_value
264
285
 
265
286
  # Combine common parameters with model-specific parameters
266
287
  # Model-specific parameters will override common ones if there are any conflicts
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: voidaccess
3
- Version: 1.9.0
3
+ Version: 1.9.1
4
4
  Summary: Dark web OSINT CLI — automated threat intelligence from query to report
5
5
  Author: VoidAccess
6
6
  License-Expression: MIT
@@ -1,3 +1,3 @@
1
1
  """voidaccess CLI — dark-web OSINT command-line interface."""
2
2
 
3
- __version__ = "1.9.0"
3
+ __version__ = "1.9.1"
@@ -26,13 +26,6 @@ from typing import Any, Callable, Optional
26
26
  import typer
27
27
  from rich.console import Console
28
28
 
29
- from extractor.identity import entity_display_id
30
-
31
- # Import reputation enrichment sources (used in Step 6.2–6.4)
32
- from sources.domain_reputation import enrich_domain_entities
33
- from sources.email_reputation import enrich_email_entities
34
- from sources.hash_reputation import enrich_hash_entities
35
-
36
29
  console = Console()
37
30
  logger = logging.getLogger(__name__)
38
31
 
@@ -368,7 +361,11 @@ async def _run_investigation(
368
361
  if not no_llm:
369
362
  try:
370
363
  from voidaccess.llm import get_llm
371
- llm = get_llm(chosen_model)
364
+ provider = cli_config.get_llm_provider(cfg)
365
+ provider_env = cli_config.PROVIDER_ENV.get(provider)
366
+ api_key = cli_config.get_llm_key(cfg)
367
+ api_keys = {provider_env: api_key} if provider_env and api_key else {}
368
+ llm = get_llm(chosen_model, api_keys=api_keys)
372
369
  except Exception as exc:
373
370
  display.update_step("Refining query", "fail", f"LLM init failed: {exc}")
374
371
  llm = None
@@ -716,6 +713,7 @@ async def _run_investigation(
716
713
  # whole cluster further down.
717
714
  display.update_step("Enriching domains", "active")
718
715
  try:
716
+ from sources.domain_reputation import enrich_domain_entities
719
717
  # enrich_*_entities return (extraction_results, stats) — unpack so the
720
718
  # threaded list isn't clobbered into a tuple for the next step.
721
719
  extraction_results, _ = await asyncio.wait_for(
@@ -738,6 +736,7 @@ async def _run_investigation(
738
736
 
739
737
  display.update_step("Enriching hashes", "active")
740
738
  try:
739
+ from sources.hash_reputation import enrich_hash_entities
741
740
  extraction_results, _ = await asyncio.wait_for(
742
741
  enrich_hash_entities(extraction_results, inv_uuid),
743
742
  timeout=45,
@@ -752,6 +751,7 @@ async def _run_investigation(
752
751
 
753
752
  display.update_step("Enriching emails", "active")
754
753
  try:
754
+ from sources.email_reputation import enrich_email_entities
755
755
  extraction_results, _ = await asyncio.wait_for(
756
756
  enrich_email_entities(extraction_results, inv_uuid),
757
757
  timeout=30,
@@ -869,9 +869,22 @@ async def _run_investigation(
869
869
  # onto the event-loop thread so Rich's live renderer remains safe.
870
870
  loop.call_soon_threadsafe(display.update_step, "Building graph", "active", label)
871
871
 
872
+ def _graph_metrics(metrics: dict[str, int]) -> None:
873
+ detail = (
874
+ f"entities: {metrics.get('entity_rows_loaded', 0)} | "
875
+ f"intra-page edges: {metrics.get('intra_page_edges', 0)} | "
876
+ f"cross-page edges: {metrics.get('cross_page_edges', 0)}"
877
+ )
878
+ loop.call_soon_threadsafe(display.update_step, "Building graph", "active", detail)
879
+
872
880
  try:
873
881
  edges_written = await asyncio.wait_for(
874
- asyncio.to_thread(_build_cooccurrence_edges, investigation_id, _graph_progress),
882
+ asyncio.to_thread(
883
+ _build_cooccurrence_edges,
884
+ investigation_id,
885
+ _graph_progress,
886
+ _graph_metrics,
887
+ ),
875
888
  timeout=CLI_PHASE_TIMEOUTS["graph_build"],
876
889
  )
877
890
  if no_llm:
@@ -1272,12 +1285,19 @@ def _lookup_page_ids(urls: list[str]) -> dict[str, uuid.UUID]:
1272
1285
  def _build_cooccurrence_edges(
1273
1286
  investigation_id: str,
1274
1287
  progress_callback: Optional[Callable[[str], None]] = None,
1288
+ metrics_callback: Optional[Callable[[dict[str, int]], None]] = None,
1275
1289
  ) -> int:
1276
1290
  """Generate CO_APPEARED_ON edges for entities sharing a page."""
1277
1291
  from db.models import Entity
1278
1292
  from db.session import get_session
1279
1293
  from sqlalchemy.orm import joinedload
1280
- from graph.builder import _iter_semantic_cooccurrence_pairs
1294
+ from graph.builder import (
1295
+ _ENTITY_TYPE_TO_NODE_TYPE,
1296
+ _iter_semantic_cooccurrence_pairs,
1297
+ _link_cross_page_entities,
1298
+ _make_node_id,
1299
+ )
1300
+ import networkx as nx
1281
1301
  from voidaccess_cli.adapters.sqlite import save_relationships
1282
1302
 
1283
1303
  edges: list[dict] = []
@@ -1292,6 +1312,7 @@ def _build_cooccurrence_edges(
1292
1312
  .filter(Entity.investigation_id == inv_uuid)
1293
1313
  .all()
1294
1314
  )
1315
+ entity_rows_loaded = len(rows)
1295
1316
  by_page: dict[uuid.UUID, list] = {}
1296
1317
  for ent in rows:
1297
1318
  page_id = getattr(ent, "page_id", None)
@@ -1316,6 +1337,31 @@ def _build_cooccurrence_edges(
1316
1337
  "confidence": 0.8,
1317
1338
  }
1318
1339
  )
1340
+ intra_page_edges = len(edges)
1341
+
1342
+ # Calculate the same cross-page bridge metric used by the graph
1343
+ # builder, while the ORM rows are still attached to their pages. The
1344
+ # CLI continues to persist its bounded co-occurrence set below, but
1345
+ # the live UI now exposes all three already-computed graph metrics.
1346
+ graph = nx.MultiDiGraph()
1347
+ for ent in rows:
1348
+ page_url = ent.page.url if ent.page else ""
1349
+ node_id = _make_node_id(ent.entity_type, ent.value, page_url)
1350
+ graph.add_node(
1351
+ node_id,
1352
+ node_type=_ENTITY_TYPE_TO_NODE_TYPE.get(ent.entity_type, ""),
1353
+ confidence=float(ent.confidence or 0.0),
1354
+ metadata={"confidence": float(ent.confidence or 0.0)},
1355
+ )
1356
+ cross_page_edges = _link_cross_page_entities(graph, rows, inv_uuid)
1357
+ if metrics_callback:
1358
+ metrics_callback(
1359
+ {
1360
+ "entity_rows_loaded": entity_rows_loaded,
1361
+ "intra_page_edges": intra_page_edges,
1362
+ "cross_page_edges": cross_page_edges,
1363
+ }
1364
+ )
1319
1365
  if progress_callback:
1320
1366
  progress_callback("loading persisted relationships")
1321
1367
  written = save_relationships(investigation_id, edges)
@@ -1408,6 +1454,8 @@ def _slugify(s: str) -> str:
1408
1454
 
1409
1455
 
1410
1456
  def _render_markdown(payload: dict[str, Any]) -> str:
1457
+ from extractor.identity import entity_display_id
1458
+
1411
1459
  lines: list[str] = []
1412
1460
  lines.append(f"# Investigation: {payload['query']}")
1413
1461
  lines.append(
@@ -184,12 +184,14 @@ def _print_path(data: dict, from_value: str, to_value: str) -> None:
184
184
  def _print_summary(data: dict) -> None:
185
185
  inv = data.get("investigation") or data
186
186
  entities = data.get("entities", [])
187
+ relationships = data.get("relationships", []) or []
187
188
  table = Table(title="Investigation summary")
188
189
  table.add_column("Field", style="bold")
189
190
  table.add_column("Value")
190
191
  table.add_row("Query", str(inv.get("query") or ""))
191
192
  table.add_row("Status", str(inv.get("status") or ""))
192
193
  table.add_row("Entities", str(len(entities)))
194
+ table.add_row("Relationships", str(len(relationships)))
193
195
  table.add_row("Created", str(inv.get("created_at") or "")[:19])
194
196
  table.add_row("Summary", (str(inv.get("summary") or "—"))[:120])
195
197
  console.print(table)
@@ -317,10 +317,11 @@ def ensure_spacy_model(model_name: str = "en_core_web_sm") -> bool:
317
317
 
318
318
  def apply_env(config: Optional[dict[str, Any]] = None) -> None:
319
319
  """
320
- Push saved config into os.environ so that the existing voidaccess
321
- modules (config.py, llm.py, sources/*) pick up the values at import.
320
+ Push saved config into os.environ before importing runtime modules.
322
321
 
323
- Must be called BEFORE any voidaccess module is imported.
322
+ This remains the single environment-injection boundary for the CLI;
323
+ runtime modules must resolve environment-dependent values when they are
324
+ used rather than freezing them during import.
324
325
  """
325
326
  cfg = config or load_config()
326
327
 
@@ -390,4 +391,3 @@ def apply_env(config: Optional[dict[str, Any]] = None) -> None:
390
391
  os.environ.pop(key, None)
391
392
 
392
393
 
393
-
@@ -521,12 +521,14 @@ def main(
521
521
  console.print(f"voidaccess {__version__}")
522
522
  raise typer.Exit()
523
523
  cli_config.apply_env()
524
+ # Validate after the CLI has injected its runtime environment. This keeps
525
+ # --help/--version cold-start safe while preserving the required-key and
526
+ # once-per-process optional-key checks for real command invocations.
527
+ import config as runtime_config
528
+ runtime_config.validate_config()
524
529
  if not no_banner and ctx.invoked_subcommand:
525
530
  show_banner(console)
526
531
 
527
532
 
528
533
  if __name__ == "__main__":
529
534
  app()
530
-
531
-
532
-
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
File without changes
File without changes
File without changes