voidaccess 1.4.2__tar.gz → 1.4.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.
- {voidaccess-1.4.2 → voidaccess-1.4.3}/PKG-INFO +2 -1
- {voidaccess-1.4.2 → voidaccess-1.4.3}/auth/token_blacklist.py +10 -3
- {voidaccess-1.4.2 → voidaccess-1.4.3}/pyproject.toml +2 -1
- {voidaccess-1.4.2 → voidaccess-1.4.3}/search/circuit_breaker.py +10 -3
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/enrichment.py +25 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/hash_reputation.py +18 -4
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess.egg-info/PKG-INFO +2 -1
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess.egg-info/requires.txt +1 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess_cli/commands/configure.py +16 -19
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess_cli/commands/investigate.py +23 -3
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess_cli/config.py +66 -6
- {voidaccess-1.4.2 → voidaccess-1.4.3}/LICENSE +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/README.md +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/analysis/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/analysis/opsec.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/analysis/patterns.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/analysis/temporal.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/api/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/api/auth.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/api/main.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/api/routes/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/api/routes/admin.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/api/routes/auth.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/api/routes/entities.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/api/routes/export.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/api/routes/investigations.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/api/routes/monitors.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/api/routes/search.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/api/routes/settings.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/auth/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/config.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/crawler/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/crawler/dedup.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/crawler/frontier.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/crawler/spider.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/crawler/utils.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/env.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0001_initial_schema.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0002_add_investigation_status_column.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0002_add_missing_tables.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0003_add_canonical_value_and_entity_links.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0004_add_page_posted_at.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0005_add_extraction_method.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0006_add_monitor_alerts.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0007_add_actor_style_profiles.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0008_add_users_table.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0009_add_investigation_id_to_relationships.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0010_add_composite_index_entity_relationships.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0011_add_page_extraction_cache.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0013_add_graph_status.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0015_add_progress_fields.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0016_backfill_graph_status.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0017_add_user_api_keys.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0018_add_user_id_to_investigations.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0019_add_content_safety_log.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0020_add_entity_source_tracking.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/models.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/queries.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/db/session.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/export/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/export/misp.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/export/sigma.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/export/stix.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/extractor/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/extractor/llm_extract.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/extractor/ner.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/extractor/normalizer.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/extractor/pipeline.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/extractor/regex_patterns.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/fingerprint/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/fingerprint/profiler.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/fingerprint/stylometry.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/graph/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/graph/builder.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/graph/export.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/graph/model.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/graph/queries.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/graph/visualize.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/i18n/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/i18n/detect.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/i18n/query_expand.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/i18n/translate.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/monitor/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/monitor/_db.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/monitor/alerts.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/monitor/config.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/monitor/diff.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/monitor/jobs.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/monitor/scheduler.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/scraper/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/scraper/scrape.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/scraper/scrape_js.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/search/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/search/search.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/setup.cfg +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/blockchain.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/cache.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/cisa.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/dns_enrichment.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/domain_reputation.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/email_reputation.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/engines.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/github_scraper.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/gitlab_scraper.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/historical_intel.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/ip_reputation.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/paste_scraper.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/pastes.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/rss_scraper.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/seed_manager.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/seeds.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/shodan.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/telegram.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/sources/virustotal.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_analysis_opsec.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_analysis_stylometry.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_analysis_temporal.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_api.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_api_monitors.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_blockchain.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_config.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_crawler.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_db.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_dns_enrichment.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_domain_reputation.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_email_reputation.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_fingerprint.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_github_scraper.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_gitlab_scraper.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_graph.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_hash_reputation.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_i18n.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_ip_reputation.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_llm.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_llm_utils.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_model_singleton.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_monitor.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_pagination.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_paste_scraper.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_rss_scraper.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_scrape_js.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_settings.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_sources.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_sources_enrichment_new.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/tests/test_vector.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/utils/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/utils/async_utils.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/utils/content_safety.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/utils/defang.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/utils/encryption.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/utils/ioc_freshness.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/utils/user_keys.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/vector/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/vector/embedder.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/vector/model_singleton.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/vector/search.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/vector/store.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess/llm.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess/llm_utils.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess.egg-info/SOURCES.txt +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess.egg-info/dependency_links.txt +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess.egg-info/entry_points.txt +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess.egg-info/top_level.txt +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess_cli/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess_cli/adapters/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess_cli/adapters/sqlite.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess_cli/browser.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess_cli/commands/__init__.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess_cli/commands/enrich.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess_cli/commands/export.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess_cli/commands/show.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess_cli/display.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.3}/voidaccess_cli/main.py +0 -0
- {voidaccess-1.4.2 → voidaccess-1.4.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.4.
|
|
3
|
+
Version: 1.4.3
|
|
4
4
|
Summary: Dark web OSINT CLI — automated threat intelligence from query to report
|
|
5
5
|
Author: VoidAccess
|
|
6
6
|
License-Expression: MIT
|
|
@@ -28,6 +28,7 @@ Requires-Dist: langchain-openai>=0.1
|
|
|
28
28
|
Requires-Dist: langchain-anthropic>=0.1
|
|
29
29
|
Requires-Dist: langchain-google-genai>=1.0
|
|
30
30
|
Requires-Dist: langchain-groq>=0.1
|
|
31
|
+
Requires-Dist: langchain-ollama>=0.1
|
|
31
32
|
Requires-Dist: python-dotenv>=1.0
|
|
32
33
|
Requires-Dist: httpx>=0.27
|
|
33
34
|
Requires-Dist: spacy>=3.7
|
|
@@ -19,16 +19,22 @@ logger = logging.getLogger(__name__)
|
|
|
19
19
|
_pool: Optional[redis.ConnectionPool] = None
|
|
20
20
|
_redis_client: Optional[redis.Redis] = None
|
|
21
21
|
_blacklist_enabled = False
|
|
22
|
+
_redis_unavailable = False
|
|
22
23
|
|
|
23
24
|
BLACKLIST_PREFIX = "blacklist:"
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
async def _get_redis() -> Optional[redis.Redis]:
|
|
27
|
-
global _pool, _redis_client, _blacklist_enabled
|
|
28
|
+
global _pool, _redis_client, _blacklist_enabled, _redis_unavailable
|
|
29
|
+
|
|
30
|
+
if _redis_unavailable:
|
|
31
|
+
return None
|
|
28
32
|
|
|
29
33
|
if REDIS_URL is None:
|
|
30
34
|
_blacklist_enabled = False
|
|
31
|
-
|
|
35
|
+
if not _redis_unavailable:
|
|
36
|
+
logger.info("REDIS_URL not configured — token blacklist disabled")
|
|
37
|
+
_redis_unavailable = True
|
|
32
38
|
return None
|
|
33
39
|
|
|
34
40
|
if _redis_client is None:
|
|
@@ -42,9 +48,10 @@ async def _get_redis() -> Optional[redis.Redis]:
|
|
|
42
48
|
_blacklist_enabled = True
|
|
43
49
|
logger.info("Token blacklist enabled via Redis")
|
|
44
50
|
except Exception as e:
|
|
45
|
-
logger.warning(
|
|
51
|
+
logger.warning("Failed to connect to Redis: %s — token blacklist disabled", e)
|
|
46
52
|
_redis_client = None
|
|
47
53
|
_blacklist_enabled = False
|
|
54
|
+
_redis_unavailable = True
|
|
48
55
|
|
|
49
56
|
return _redis_client
|
|
50
57
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "voidaccess"
|
|
7
|
-
version = "1.4.
|
|
7
|
+
version = "1.4.3"
|
|
8
8
|
description = "Dark web OSINT CLI — automated threat intelligence from query to report"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -34,6 +34,7 @@ dependencies = [
|
|
|
34
34
|
"langchain-anthropic>=0.1",
|
|
35
35
|
"langchain-google-genai>=1.0",
|
|
36
36
|
"langchain-groq>=0.1",
|
|
37
|
+
"langchain-ollama>=0.1",
|
|
37
38
|
"python-dotenv>=1.0",
|
|
38
39
|
"httpx>=0.27",
|
|
39
40
|
"spacy>=3.7",
|
|
@@ -29,6 +29,7 @@ CIRCUIT_PREFIX = "circuit:"
|
|
|
29
29
|
_pool: Optional[redis.ConnectionPool] = None
|
|
30
30
|
_redis_client: Optional[redis.Redis] = None
|
|
31
31
|
_circuit_breaker_enabled = False
|
|
32
|
+
_redis_unavailable = False # latched True once we decide Redis isn't reachable
|
|
32
33
|
|
|
33
34
|
_engine_failures: dict[str, int] = {}
|
|
34
35
|
_engine_last_success: dict[str, float] = {}
|
|
@@ -37,11 +38,16 @@ _engine_open_time: dict[str, float] = {}
|
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
async def _get_redis() -> Optional[redis.Redis]:
|
|
40
|
-
global _pool, _redis_client, _circuit_breaker_enabled
|
|
41
|
+
global _pool, _redis_client, _circuit_breaker_enabled, _redis_unavailable
|
|
42
|
+
|
|
43
|
+
if _redis_unavailable:
|
|
44
|
+
return None
|
|
41
45
|
|
|
42
46
|
if REDIS_URL is None:
|
|
43
47
|
_circuit_breaker_enabled = False
|
|
44
|
-
|
|
48
|
+
if not _redis_unavailable:
|
|
49
|
+
logger.info("REDIS_URL not configured — circuit breaker using in-memory fallback")
|
|
50
|
+
_redis_unavailable = True
|
|
45
51
|
return None
|
|
46
52
|
|
|
47
53
|
if _redis_client is None:
|
|
@@ -55,9 +61,10 @@ async def _get_redis() -> Optional[redis.Redis]:
|
|
|
55
61
|
_circuit_breaker_enabled = True
|
|
56
62
|
logger.info("Circuit breaker enabled via Redis")
|
|
57
63
|
except Exception as e:
|
|
58
|
-
logger.warning(
|
|
64
|
+
logger.warning("Failed to connect to Redis: %s — circuit breaker using in-memory fallback", e)
|
|
59
65
|
_redis_client = None
|
|
60
66
|
_circuit_breaker_enabled = False
|
|
67
|
+
_redis_unavailable = True
|
|
61
68
|
|
|
62
69
|
return _redis_client
|
|
63
70
|
|
|
@@ -40,11 +40,30 @@ THREATFOX_URL = "https://threatfox-api.abuse.ch/api/v1/"
|
|
|
40
40
|
# All HTTP calls use at most 30s client timeout (enforced per request).
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
_ABUSECH_WARNED = False
|
|
44
|
+
|
|
45
|
+
|
|
43
46
|
def _abusech_headers() -> dict[str, str]:
|
|
44
47
|
key = (os.environ.get("ABUSECH_API_KEY") or "").strip()
|
|
45
48
|
return {"Auth-Key": key} if key else {}
|
|
46
49
|
|
|
47
50
|
|
|
51
|
+
def _abusech_enabled() -> bool:
|
|
52
|
+
"""abuse.ch APIs (MalwareBazaar/ThreatFox/URLhaus) require an Auth-Key
|
|
53
|
+
since 2024. Return False (and log once) when the key is missing so we
|
|
54
|
+
skip the request entirely instead of spamming HTTP 401."""
|
|
55
|
+
global _ABUSECH_WARNED
|
|
56
|
+
if (os.environ.get("ABUSECH_API_KEY") or "").strip():
|
|
57
|
+
return True
|
|
58
|
+
if not _ABUSECH_WARNED:
|
|
59
|
+
logger.info(
|
|
60
|
+
"abuse.ch enrichment skipped — set ABUSECH_API_KEY "
|
|
61
|
+
"(free at https://auth.abuse.ch) to enable MalwareBazaar/ThreatFox/URLhaus."
|
|
62
|
+
)
|
|
63
|
+
_ABUSECH_WARNED = True
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
48
67
|
def is_onion_url(url: str) -> bool:
|
|
49
68
|
"""
|
|
50
69
|
Return True if *url* looks like a Tor hidden service URL (.onion).
|
|
@@ -218,6 +237,8 @@ def otx_pulse_to_page(pulse: dict) -> dict:
|
|
|
218
237
|
|
|
219
238
|
async def fetch_malwarebazaar(query: str, limit: int = 20) -> list[dict]:
|
|
220
239
|
"""Query MalwareBazaar by tag then by signature."""
|
|
240
|
+
if not _abusech_enabled():
|
|
241
|
+
return []
|
|
221
242
|
results: list[dict] = []
|
|
222
243
|
q = (query or "").strip()
|
|
223
244
|
if not q:
|
|
@@ -312,6 +333,8 @@ async def fetch_malwarebazaar(query: str, limit: int = 20) -> list[dict]:
|
|
|
312
333
|
|
|
313
334
|
async def fetch_threatfox(query: str, limit: int = 50) -> list[dict]:
|
|
314
335
|
"""Search ThreatFox IOCs by search term."""
|
|
336
|
+
if not _abusech_enabled():
|
|
337
|
+
return []
|
|
315
338
|
results: list[dict] = []
|
|
316
339
|
q = (query or "").strip()
|
|
317
340
|
if not q:
|
|
@@ -397,6 +420,8 @@ async def fetch_threatfox(query: str, limit: int = 50) -> list[dict]:
|
|
|
397
420
|
|
|
398
421
|
async def fetch_urlhaus(query: str, limit: int = 20) -> list[dict]:
|
|
399
422
|
"""Search URLhaus by tag."""
|
|
423
|
+
if not _abusech_enabled():
|
|
424
|
+
return []
|
|
400
425
|
results: list[dict] = []
|
|
401
426
|
q = (query or "").strip()
|
|
402
427
|
if not q:
|
|
@@ -164,11 +164,18 @@ async def query_malwarebazaar(hash_value: str) -> dict[str, Any]:
|
|
|
164
164
|
"""
|
|
165
165
|
POST get_info to MalwareBazaar for a file hash.
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
Requires ABUSECH_API_KEY (abuse.ch made auth-key mandatory in 2024).
|
|
168
|
+
Returns malware family, file type, first seen date.
|
|
168
169
|
"""
|
|
170
|
+
api_key = (os.environ.get("ABUSECH_API_KEY") or "").strip()
|
|
171
|
+
if not api_key:
|
|
172
|
+
return {"found": False, "source": "malwarebazaar_no_key"}
|
|
169
173
|
try:
|
|
170
174
|
timeout = aiohttp.ClientTimeout(total=15)
|
|
171
|
-
headers = {
|
|
175
|
+
headers = {
|
|
176
|
+
"User-Agent": "VoidAccess-OSINT/1.1 (security research)",
|
|
177
|
+
"Auth-Key": api_key,
|
|
178
|
+
}
|
|
172
179
|
async with aiohttp.ClientSession(timeout=timeout, headers=headers) as session:
|
|
173
180
|
async with session.post(
|
|
174
181
|
MALWAREBAZAAR_URL,
|
|
@@ -212,11 +219,18 @@ async def query_threatfox(hash_value: str) -> dict[str, Any]:
|
|
|
212
219
|
"""
|
|
213
220
|
POST search_ioc to ThreatFox for a file hash.
|
|
214
221
|
|
|
215
|
-
|
|
222
|
+
Requires ABUSECH_API_KEY (abuse.ch made auth-key mandatory in 2024).
|
|
223
|
+
Returns malware family and associated IOCs.
|
|
216
224
|
"""
|
|
225
|
+
api_key = (os.environ.get("ABUSECH_API_KEY") or "").strip()
|
|
226
|
+
if not api_key:
|
|
227
|
+
return {"found": False, "source": "threatfox_no_key"}
|
|
217
228
|
try:
|
|
218
229
|
timeout = aiohttp.ClientTimeout(total=15)
|
|
219
|
-
headers = {
|
|
230
|
+
headers = {
|
|
231
|
+
"User-Agent": "VoidAccess-OSINT/1.1 (security research)",
|
|
232
|
+
"Auth-Key": api_key,
|
|
233
|
+
}
|
|
220
234
|
async with aiohttp.ClientSession(timeout=timeout, headers=headers) as session:
|
|
221
235
|
async with session.post(
|
|
222
236
|
THREATFOX_URL,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voidaccess
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.3
|
|
4
4
|
Summary: Dark web OSINT CLI — automated threat intelligence from query to report
|
|
5
5
|
Author: VoidAccess
|
|
6
6
|
License-Expression: MIT
|
|
@@ -28,6 +28,7 @@ Requires-Dist: langchain-openai>=0.1
|
|
|
28
28
|
Requires-Dist: langchain-anthropic>=0.1
|
|
29
29
|
Requires-Dist: langchain-google-genai>=1.0
|
|
30
30
|
Requires-Dist: langchain-groq>=0.1
|
|
31
|
+
Requires-Dist: langchain-ollama>=0.1
|
|
31
32
|
Requires-Dist: python-dotenv>=1.0
|
|
32
33
|
Requires-Dist: httpx>=0.27
|
|
33
34
|
Requires-Dist: spacy>=3.7
|
|
@@ -57,8 +57,23 @@ def _test_llm_key(provider: str, api_key: str, model: str) -> bool:
|
|
|
57
57
|
return True
|
|
58
58
|
try:
|
|
59
59
|
from voidaccess.llm import get_llm
|
|
60
|
+
except ImportError as exc:
|
|
61
|
+
missing = str(exc).split("'")[-2] if "'" in str(exc) else str(exc)
|
|
62
|
+
console.print(
|
|
63
|
+
f"[yellow]Skipped validation:[/yellow] missing dependency [bold]{missing}[/bold]. "
|
|
64
|
+
f"Install with: [bold]pip install {missing.replace('_', '-')}[/bold]"
|
|
65
|
+
)
|
|
66
|
+
return False
|
|
67
|
+
try:
|
|
60
68
|
get_llm(model, api_keys={cli_config.PROVIDER_ENV.get(provider, ""): api_key})
|
|
61
69
|
return True
|
|
70
|
+
except ImportError as exc:
|
|
71
|
+
missing = str(exc).split("'")[-2] if "'" in str(exc) else str(exc)
|
|
72
|
+
console.print(
|
|
73
|
+
f"[yellow]Skipped validation:[/yellow] missing dependency [bold]{missing}[/bold]. "
|
|
74
|
+
f"Install with: [bold]pip install {missing.replace('_', '-')}[/bold]"
|
|
75
|
+
)
|
|
76
|
+
return False
|
|
62
77
|
except Exception as exc:
|
|
63
78
|
console.print(f"[yellow]Could not validate key:[/yellow] {exc}")
|
|
64
79
|
return False
|
|
@@ -117,25 +132,7 @@ def _prompt_output_dir(cfg: dict) -> None:
|
|
|
117
132
|
|
|
118
133
|
|
|
119
134
|
def _ensure_spacy_model() -> None:
|
|
120
|
-
|
|
121
|
-
try:
|
|
122
|
-
import subprocess
|
|
123
|
-
import sys
|
|
124
|
-
|
|
125
|
-
result = subprocess.run(
|
|
126
|
-
[sys.executable, "-m", "spacy", "download", "en_core_web_sm"],
|
|
127
|
-
capture_output=True,
|
|
128
|
-
text=True,
|
|
129
|
-
)
|
|
130
|
-
if result.returncode == 0:
|
|
131
|
-
console.print(" ✓ spaCy model ready")
|
|
132
|
-
else:
|
|
133
|
-
console.print(
|
|
134
|
-
" ⚠ spaCy download failed — run manually: "
|
|
135
|
-
"python -m spacy download en_core_web_sm"
|
|
136
|
-
)
|
|
137
|
-
except Exception as e:
|
|
138
|
-
console.print(f" ⚠ spaCy: {e}")
|
|
135
|
+
cli_config.ensure_spacy_model()
|
|
139
136
|
|
|
140
137
|
|
|
141
138
|
@app.callback()
|
|
@@ -56,10 +56,13 @@ def run(
|
|
|
56
56
|
except Exception:
|
|
57
57
|
import subprocess, sys
|
|
58
58
|
from rich.console import Console
|
|
59
|
-
Console().print(
|
|
59
|
+
Console().print(
|
|
60
|
+
" [dim]→[/dim] Installing spaCy NER model (one-time)..."
|
|
61
|
+
)
|
|
60
62
|
subprocess.run(
|
|
61
|
-
[sys.executable, "-m", "spacy",
|
|
62
|
-
|
|
63
|
+
[sys.executable, "-m", "spacy",
|
|
64
|
+
"download", "en_core_web_sm"],
|
|
65
|
+
capture_output=True
|
|
63
66
|
)
|
|
64
67
|
|
|
65
68
|
if quiet:
|
|
@@ -434,6 +437,23 @@ async def _run_investigation(
|
|
|
434
437
|
}
|
|
435
438
|
)
|
|
436
439
|
|
|
440
|
+
# Close any cached aiohttp sessions so the event loop exits cleanly
|
|
441
|
+
# (otherwise aiohttp prints "Unclosed client session" warnings).
|
|
442
|
+
await _close_cached_sessions()
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
async def _close_cached_sessions() -> None:
|
|
446
|
+
try:
|
|
447
|
+
from scraper.scrape import close_cached_sessions as _close_scrape
|
|
448
|
+
await _close_scrape()
|
|
449
|
+
except Exception:
|
|
450
|
+
pass
|
|
451
|
+
try:
|
|
452
|
+
from search import close_search_session as _close_search
|
|
453
|
+
await _close_search()
|
|
454
|
+
except Exception:
|
|
455
|
+
pass
|
|
456
|
+
|
|
437
457
|
|
|
438
458
|
# ---------------------------------------------------------------------------
|
|
439
459
|
# Side-source helpers (each gracefully degrades if module missing/disabled)
|
|
@@ -139,6 +139,65 @@ def db_url() -> str:
|
|
|
139
139
|
return f"sqlite:///{DB_PATH.as_posix()}"
|
|
140
140
|
|
|
141
141
|
|
|
142
|
+
def ensure_spacy_model(model_name: str = "en_core_web_sm") -> bool:
|
|
143
|
+
"""
|
|
144
|
+
Ensure spaCy NER model is installed. Returns True if model is loadable
|
|
145
|
+
after this call. Handles PEP 668 (externally-managed-environment) on
|
|
146
|
+
Debian/Ubuntu/Kali by setting PIP_BREAK_SYSTEM_PACKAGES=1, and uses
|
|
147
|
+
PIP_USER=1 outside virtualenvs.
|
|
148
|
+
|
|
149
|
+
Prints progress via rich. Safe to call repeatedly.
|
|
150
|
+
"""
|
|
151
|
+
import sys
|
|
152
|
+
import subprocess
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
import spacy
|
|
156
|
+
spacy.load(model_name)
|
|
157
|
+
return True
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
from rich.console import Console
|
|
162
|
+
con = Console()
|
|
163
|
+
con.print(f" [dim]→[/dim] Installing spaCy NER model [bold]{model_name}[/bold] (one-time)...")
|
|
164
|
+
|
|
165
|
+
env = dict(os.environ)
|
|
166
|
+
env["PIP_BREAK_SYSTEM_PACKAGES"] = "1"
|
|
167
|
+
in_venv = sys.prefix != getattr(sys, "base_prefix", sys.prefix)
|
|
168
|
+
if not in_venv:
|
|
169
|
+
env["PIP_USER"] = "1"
|
|
170
|
+
|
|
171
|
+
result = subprocess.run(
|
|
172
|
+
[sys.executable, "-m", "spacy", "download", model_name],
|
|
173
|
+
capture_output=True,
|
|
174
|
+
text=True,
|
|
175
|
+
env=env,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if result.returncode == 0:
|
|
179
|
+
try:
|
|
180
|
+
import importlib
|
|
181
|
+
import spacy as _spacy # noqa: F401
|
|
182
|
+
importlib.invalidate_caches()
|
|
183
|
+
import spacy as _spacy2
|
|
184
|
+
_spacy2.load(model_name)
|
|
185
|
+
con.print(f" [green]✓[/green] spaCy model ready")
|
|
186
|
+
return True
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
err_tail = (result.stderr or result.stdout or "").strip().splitlines()[-3:]
|
|
191
|
+
con.print(f" [yellow]⚠[/yellow] spaCy install failed (exit {result.returncode}) — NER will be skipped.")
|
|
192
|
+
for line in err_tail:
|
|
193
|
+
con.print(f" [dim]{line}[/dim]")
|
|
194
|
+
con.print(
|
|
195
|
+
f" Run manually: [bold]PIP_BREAK_SYSTEM_PACKAGES=1 "
|
|
196
|
+
f"{os.path.basename(sys.executable)} -m spacy download {model_name}[/bold]"
|
|
197
|
+
)
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
|
|
142
201
|
def apply_env(config: Optional[dict[str, Any]] = None) -> None:
|
|
143
202
|
"""
|
|
144
203
|
Push saved config into os.environ so that the existing voidaccess
|
|
@@ -154,11 +213,12 @@ def apply_env(config: Optional[dict[str, Any]] = None) -> None:
|
|
|
154
213
|
os.environ.setdefault("PLAYWRIGHT_ENABLED", "false")
|
|
155
214
|
|
|
156
215
|
def _set_env_if_present(key: str, value: Any, *, clear_if_empty: bool = False) -> None:
|
|
157
|
-
text = str(value)
|
|
158
|
-
if text:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
216
|
+
text = str(value) if value is not None else ""
|
|
217
|
+
if not text or not text.strip():
|
|
218
|
+
if clear_if_empty:
|
|
219
|
+
os.environ.pop(key, None)
|
|
220
|
+
return
|
|
221
|
+
os.environ[key] = text.strip()
|
|
162
222
|
|
|
163
223
|
# Tor proxy
|
|
164
224
|
_set_env_if_present("TOR_PROXY_HOST", cfg.get("tor", {}).get("host", "127.0.0.1"))
|
|
@@ -182,5 +242,5 @@ def apply_env(config: Optional[dict[str, Any]] = None) -> None:
|
|
|
182
242
|
# Keyless APIs (ThreatFox/URLhaus/MalwareBazaar/abuse.ch) must never
|
|
183
243
|
# receive an empty auth header — clear any empty env remnant.
|
|
184
244
|
for key in ("ABUSECH_API_KEY", "VT_API_KEY", "OTX_API_KEY"):
|
|
185
|
-
if not os.environ.get(key):
|
|
245
|
+
if not (os.environ.get(key) or "").strip():
|
|
186
246
|
os.environ.pop(key, None)
|
|
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
|
|
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
|
{voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0002_add_investigation_status_column.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0007_add_actor_style_profiles.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0011_add_page_extraction_cache.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0018_add_user_id_to_investigations.py
RENAMED
|
File without changes
|
|
File without changes
|
{voidaccess-1.4.2 → voidaccess-1.4.3}/db/migrations/versions/0020_add_entity_source_tracking.py
RENAMED
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|