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