voidaccess 1.9.1__tar.gz → 1.9.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {voidaccess-1.9.1 → voidaccess-1.9.3}/PKG-INFO +12 -1
- {voidaccess-1.9.1 → voidaccess-1.9.3}/README.md +11 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/api/main.py +1 -1
- {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/investigations.py +1 -1
- {voidaccess-1.9.1 → voidaccess-1.9.3}/export/ioc_package.py +1 -1
- {voidaccess-1.9.1 → voidaccess-1.9.3}/export/misp.py +72 -19
- voidaccess-1.9.3/export/relationships.py +81 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/export/stix.py +150 -22
- {voidaccess-1.9.1 → voidaccess-1.9.3}/graph/builder.py +10 -2
- {voidaccess-1.9.1 → voidaccess-1.9.3}/pyproject.toml +1 -1
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/email_reputation.py +4 -2
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_api.py +62 -48
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_confidence_calibration.py +10 -8
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_db.py +1 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_llm.py +10 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_llm_utils.py +1 -1
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_regex_patterns.py +21 -21
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_snort_export.py +28 -28
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_stix_export.py +20 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_yara_export.py +17 -17
- {voidaccess-1.9.1 → voidaccess-1.9.3}/vector/model_singleton.py +5 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess/llm.py +15 -10
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess.egg-info/PKG-INFO +12 -1
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess.egg-info/SOURCES.txt +1 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/__init__.py +1 -1
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/commands/configure.py +2 -3
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/commands/export.py +17 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/config.py +67 -20
- {voidaccess-1.9.1 → voidaccess-1.9.3}/LICENSE +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/analysis/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/analysis/opsec.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/analysis/patterns.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/analysis/temporal.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/api/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/api/auth.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/api/errors.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/actors.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/admin.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/auth.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/entities.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/export.py +2 -2
- {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/monitors.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/search.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/api/routes/settings.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/auth/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/auth/token_blacklist.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/config.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/crawler/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/crawler/dedup.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/crawler/frontier.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/crawler/spider.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/crawler/utils.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/env.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0001_initial_schema.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0002_add_investigation_status_column.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0002_add_missing_tables.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0003_add_canonical_value_and_entity_links.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0004_add_page_posted_at.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0005_add_extraction_method.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0006_add_monitor_alerts.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0007_add_actor_style_profiles.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0008_add_users_table.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0009_add_investigation_id_to_relationships.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0010_add_composite_index_entity_relationships.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0011_add_page_extraction_cache.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0013_add_graph_status.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0015_add_progress_fields.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0016_backfill_graph_status.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0017_add_user_api_keys.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0018_add_user_id_to_investigations.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0019_add_content_safety_log.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0020_add_entity_source_tracking.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0021_add_search_engine_stats.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0022_add_actor_profiles.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0023_add_investigation_metadata.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0024_add_investigation_step_metrics.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/migrations/versions/0025_add_cancellation_requested.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/models.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/queries.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/search_engine_stats.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/db/session.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/export/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/export/_entity_loading.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/export/sigma.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/export/snort_export.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/export/yara_export.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/confidence.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/entity_shape.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/gazetteer.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/identity.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/llm_extract.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/ner.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/normalizer.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/pipeline.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/regex_patterns.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/extractor/relationship_extract.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/fingerprint/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/fingerprint/calibration.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/fingerprint/profiler.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/fingerprint/stylometry.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/graph/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/graph/export.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/graph/model.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/graph/queries.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/graph/visualize.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/i18n/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/i18n/detect.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/i18n/query_expand.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/i18n/translate.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/monitor/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/monitor/_db.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/monitor/alerts.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/monitor/config.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/monitor/diff.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/monitor/jobs.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/monitor/scheduler.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/scraper/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/scraper/scrape.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/scraper/scrape_js.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/search/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/search/circuit_breaker.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/search/engine_catalog.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/search/query_builder.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/search/search.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/setup.cfg +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/actor_profiles.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/blockchain.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/breach_lookup.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/cache.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/cisa.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/data/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/data/onion_seeds.json +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/dns_enrichment.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/domain_reputation.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/engines.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/enrichment.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/github_scraper.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/gitlab_scraper.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/hash_reputation.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/historical_intel.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/infostealer.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/ip_reputation.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/nvd.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/paste_scraper.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/pastes.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/proxy_client.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/rss_scraper.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/seed_manager.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/seeds.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/shodan.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/telegram.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/sources/virustotal.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_analysis_opsec.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_analysis_stylometry.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_analysis_temporal.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_api_monitors.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_blockchain.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_breach_lookup.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_cli_proxy_config.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_config.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_consolidated_contracts.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_crawler.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_dns_enrichment.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_domain_reputation.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_email_reputation.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_extractor_llm_pipeline.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_filter_e2e_manual.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_filter_parser_manual.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_fingerprint.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_github_scraper.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_gitlab_scraper.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_graph.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_graph_integration.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_hash_reputation.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_i18n.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_infostealer.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_ip_reputation.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_model_singleton.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_monitor.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_monitor_integration.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_nvd.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_pagination.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_paste_scraper.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_proxy_client.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_ransomlook.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_relationship_extract.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_rss_scraper.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_scrape_js.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_settings.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_sources.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_sources_enrichment_new.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/tests/test_vector.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/async_utils.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/content_dedup.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/content_safety.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/defang.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/encryption.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/enrichment_cache.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/investigation_metrics.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/ioc_freshness.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/utils/user_keys.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/vector/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/vector/embedder.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/vector/search.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/vector/store.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess/config.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess/llm_utils.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess.egg-info/dependency_links.txt +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess.egg-info/entry_points.txt +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess.egg-info/requires.txt +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess.egg-info/top_level.txt +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/adapters/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/adapters/sqlite.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/browser.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/commands/__init__.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/commands/actors.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/commands/enrich.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/commands/investigate.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/commands/show.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/display.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/main.py +0 -0
- {voidaccess-1.9.1 → voidaccess-1.9.3}/voidaccess_cli/tor_detect.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voidaccess
|
|
3
|
-
Version: 1.9.
|
|
3
|
+
Version: 1.9.3
|
|
4
4
|
Summary: Dark web OSINT CLI — automated threat intelligence from query to report
|
|
5
5
|
Author: VoidAccess
|
|
6
6
|
License-Expression: MIT
|
|
@@ -172,6 +172,17 @@ Full pipeline behavior, timeouts, recovery, and data flow are documented in [Arc
|
|
|
172
172
|
|
|
173
173
|
Sources that need API keys skip cleanly when their keys are absent. The complete key and configuration reference is in [Architecture](docs/architecture.md#13-configuration-reference).
|
|
174
174
|
|
|
175
|
+
### Optional vector embeddings
|
|
176
|
+
|
|
177
|
+
The default installation can run without PyTorch. When the embedding stack is
|
|
178
|
+
unavailable, VoidAccess logs that it is using a deterministic SHA-256 fallback
|
|
179
|
+
encoder. Install the optional NLP dependencies to enable full
|
|
180
|
+
sentence-transformer vector embeddings:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
pip install "voidaccess[nlp]"
|
|
184
|
+
```
|
|
185
|
+
|
|
175
186
|
## LLM Providers
|
|
176
187
|
|
|
177
188
|
| Provider | Typical models | Notes |
|
|
@@ -87,6 +87,17 @@ Full pipeline behavior, timeouts, recovery, and data flow are documented in [Arc
|
|
|
87
87
|
|
|
88
88
|
Sources that need API keys skip cleanly when their keys are absent. The complete key and configuration reference is in [Architecture](docs/architecture.md#13-configuration-reference).
|
|
89
89
|
|
|
90
|
+
### Optional vector embeddings
|
|
91
|
+
|
|
92
|
+
The default installation can run without PyTorch. When the embedding stack is
|
|
93
|
+
unavailable, VoidAccess logs that it is using a deterministic SHA-256 fallback
|
|
94
|
+
encoder. Install the optional NLP dependencies to enable full
|
|
95
|
+
sentence-transformer vector embeddings:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pip install "voidaccess[nlp]"
|
|
99
|
+
```
|
|
100
|
+
|
|
90
101
|
## LLM Providers
|
|
91
102
|
|
|
92
103
|
| Provider | Typical models | Notes |
|
|
@@ -40,7 +40,7 @@ logging.basicConfig(level=logging.DEBUG)
|
|
|
40
40
|
# Rate limiter setup
|
|
41
41
|
# ---------------------------------------------------------------------------
|
|
42
42
|
|
|
43
|
-
DISABLE_RATE_LIMIT = os.getenv("DISABLE_RATE_LIMIT", "false").lower() == "true"
|
|
43
|
+
DISABLE_RATE_LIMIT = (os.getenv("DISABLE_RATE_LIMIT", "false") or "false").lower() == "true"
|
|
44
44
|
|
|
45
45
|
if DISABLE_RATE_LIMIT:
|
|
46
46
|
limiter = None
|
|
@@ -537,7 +537,7 @@ async def _check_cancelled(inv_uuid: uuid.UUID, investigation_id: str) -> bool:
|
|
|
537
537
|
# Rate limiting (shared key_func with api/main.py; enforcement via app.state.limiter)
|
|
538
538
|
# ---------------------------------------------------------------------------
|
|
539
539
|
|
|
540
|
-
_DISABLE_RATE_LIMIT = os.getenv("DISABLE_RATE_LIMIT", "false").lower() == "true"
|
|
540
|
+
_DISABLE_RATE_LIMIT = (os.getenv("DISABLE_RATE_LIMIT", "false") or "false").lower() == "true"
|
|
541
541
|
|
|
542
542
|
if not _DISABLE_RATE_LIMIT:
|
|
543
543
|
try:
|
|
@@ -57,7 +57,7 @@ logger = logging.getLogger(__name__)
|
|
|
57
57
|
# ---------------------------------------------------------------------------
|
|
58
58
|
|
|
59
59
|
PACKAGE_FORMAT = "voidaccess-ioc-v1"
|
|
60
|
-
PACKAGE_VERSION = "1.9.
|
|
60
|
+
PACKAGE_VERSION = "1.9.3"
|
|
61
61
|
SNORT_SID_BASE = 9000001 # 9xxxxxx reserved for VoidAccess-generated rules
|
|
62
62
|
|
|
63
63
|
# Map extractor upper-case entity types to the per-file bucket they belong to.
|
|
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|
|
16
16
|
import json
|
|
17
17
|
import logging
|
|
18
18
|
import os
|
|
19
|
+
import uuid
|
|
19
20
|
from datetime import datetime, timezone
|
|
20
21
|
from typing import Any, Optional
|
|
21
22
|
|
|
@@ -76,6 +77,11 @@ _MISP_ATTR_MAP: dict[str, dict] = {
|
|
|
76
77
|
"category": "Attribution",
|
|
77
78
|
"to_ids": False,
|
|
78
79
|
},
|
|
80
|
+
"ORGANIZATION_NAME": {
|
|
81
|
+
"type": "target-org",
|
|
82
|
+
"category": "Targeting data",
|
|
83
|
+
"to_ids": False,
|
|
84
|
+
},
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
|
|
@@ -94,7 +100,7 @@ def investigation_to_misp_event(
|
|
|
94
100
|
Returns a valid (but empty-attribute) event if the investigation is not found.
|
|
95
101
|
Never raises.
|
|
96
102
|
"""
|
|
97
|
-
investigation,
|
|
103
|
+
investigation, entity_rows = _load_investigation_and_entity_rows(
|
|
98
104
|
investigation_id, entity_ids=entity_ids
|
|
99
105
|
)
|
|
100
106
|
|
|
@@ -110,7 +116,14 @@ def investigation_to_misp_event(
|
|
|
110
116
|
query = getattr(investigation, "query", "") or ""
|
|
111
117
|
|
|
112
118
|
attributes: list[dict] = []
|
|
113
|
-
|
|
119
|
+
graph_endpoint_map: dict[str, str] = {}
|
|
120
|
+
objects: list[dict] = []
|
|
121
|
+
|
|
122
|
+
from extractor.identity import entity_graph_id # noqa: PLC0415
|
|
123
|
+
from export._entity_loading import normalized_entity_from_db_row # noqa: PLC0415
|
|
124
|
+
|
|
125
|
+
for row in entity_rows:
|
|
126
|
+
entity = normalized_entity_from_db_row(row)
|
|
114
127
|
mapping = _MISP_ATTR_MAP.get(entity.entity_type)
|
|
115
128
|
if mapping is None:
|
|
116
129
|
continue
|
|
@@ -123,6 +136,40 @@ def investigation_to_misp_event(
|
|
|
123
136
|
}
|
|
124
137
|
attributes.append(attr)
|
|
125
138
|
|
|
139
|
+
object_uuid = str(row.id)
|
|
140
|
+
graph_endpoint_map[entity_graph_id(entity)] = object_uuid
|
|
141
|
+
objects.append(
|
|
142
|
+
{
|
|
143
|
+
"name": "voidaccess-entity",
|
|
144
|
+
"meta-category": "threat-intelligence",
|
|
145
|
+
"uuid": object_uuid,
|
|
146
|
+
"Attribute": [dict(attr, uuid=str(uuid.uuid4()))],
|
|
147
|
+
"ObjectReference": [],
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Use the same graph relationship enumeration as STIX. This includes
|
|
152
|
+
# typed persisted edges and applies the same endpoint/deduplication rules.
|
|
153
|
+
from export.relationships import load_export_relationships # noqa: PLC0415
|
|
154
|
+
graph_relationships, relationship_warning = load_export_relationships(
|
|
155
|
+
investigation_id, graph_endpoint_map
|
|
156
|
+
)
|
|
157
|
+
if relationship_warning:
|
|
158
|
+
logger.warning("MISP relationships were not included: %s", relationship_warning)
|
|
159
|
+
|
|
160
|
+
object_by_uuid = {obj["uuid"]: obj for obj in objects}
|
|
161
|
+
for relationship in graph_relationships:
|
|
162
|
+
source_object = object_by_uuid.get(relationship["source_ref"])
|
|
163
|
+
if source_object is None or relationship["target_ref"] not in object_by_uuid:
|
|
164
|
+
continue
|
|
165
|
+
source_object["ObjectReference"].append(
|
|
166
|
+
{
|
|
167
|
+
"object_uuid": relationship["source_ref"],
|
|
168
|
+
"referenced_uuid": relationship["target_ref"],
|
|
169
|
+
"relationship_type": relationship["edge_type"],
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
|
|
126
173
|
return {
|
|
127
174
|
"Event": {
|
|
128
175
|
"info": f"VoidAccess Investigation: {query}",
|
|
@@ -131,6 +178,7 @@ def investigation_to_misp_event(
|
|
|
131
178
|
"analysis": "2", # Completed
|
|
132
179
|
"distribution": "0", # Your organisation only
|
|
133
180
|
"Attribute": attributes,
|
|
181
|
+
"Object": objects,
|
|
134
182
|
}
|
|
135
183
|
}
|
|
136
184
|
|
|
@@ -163,8 +211,21 @@ def _load_investigation_and_entities(
|
|
|
163
211
|
|
|
164
212
|
Returns (investigation, entities) or (None, []) on error / not found.
|
|
165
213
|
"""
|
|
166
|
-
|
|
214
|
+
investigation, rows = _load_investigation_and_entity_rows(
|
|
215
|
+
investigation_id, entity_ids=entity_ids
|
|
216
|
+
)
|
|
217
|
+
if investigation is None:
|
|
218
|
+
return None, []
|
|
167
219
|
|
|
220
|
+
from export._entity_loading import normalized_entity_from_db_row # noqa: PLC0415
|
|
221
|
+
return investigation, [normalized_entity_from_db_row(row) for row in rows]
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _load_investigation_and_entity_rows(
|
|
225
|
+
investigation_id: Any,
|
|
226
|
+
entity_ids: Optional[list[str]] = None,
|
|
227
|
+
):
|
|
228
|
+
"""Load an investigation and its ORM entity rows for export rendering."""
|
|
168
229
|
if not os.getenv("DATABASE_URL"):
|
|
169
230
|
return None, []
|
|
170
231
|
|
|
@@ -172,7 +233,7 @@ def _load_investigation_and_entities(
|
|
|
172
233
|
from db.session import get_session # noqa: PLC0415
|
|
173
234
|
from db.queries import get_investigation_by_id_or_run # noqa: PLC0415
|
|
174
235
|
from db.models import Entity, InvestigationEntityLink # noqa: PLC0415
|
|
175
|
-
from
|
|
236
|
+
from sqlalchemy.orm import joinedload # noqa: PLC0415
|
|
176
237
|
|
|
177
238
|
inv_uuid = _coerce_uuid(investigation_id)
|
|
178
239
|
if inv_uuid is None:
|
|
@@ -192,15 +253,16 @@ def _load_investigation_and_entities(
|
|
|
192
253
|
if investigation is None:
|
|
193
254
|
return None, []
|
|
194
255
|
|
|
195
|
-
linked_ids_select = (
|
|
196
|
-
session.query(InvestigationEntityLink.entity_id)
|
|
197
|
-
.filter(InvestigationEntityLink.investigation_id == investigation.id)
|
|
198
|
-
)
|
|
199
256
|
db_entities = (
|
|
200
257
|
session.query(Entity)
|
|
258
|
+
.outerjoin(
|
|
259
|
+
InvestigationEntityLink,
|
|
260
|
+
InvestigationEntityLink.entity_id == Entity.id,
|
|
261
|
+
)
|
|
262
|
+
.options(joinedload(Entity.page))
|
|
201
263
|
.filter(
|
|
202
264
|
(Entity.investigation_id == investigation.id)
|
|
203
|
-
|
|
|
265
|
+
| (InvestigationEntityLink.investigation_id == investigation.id)
|
|
204
266
|
)
|
|
205
267
|
.all()
|
|
206
268
|
)
|
|
@@ -209,17 +271,8 @@ def _load_investigation_and_entities(
|
|
|
209
271
|
want = frozenset(filter_uuids)
|
|
210
272
|
db_entities = [e for e in db_entities if e.id in want]
|
|
211
273
|
|
|
212
|
-
from export._entity_loading import normalized_entity_from_db_row # noqa: PLC0415
|
|
213
|
-
|
|
214
|
-
# Shared row→NormalizedEntity mapping (same one STIX/Sigma use), so
|
|
215
|
-
# auxiliary fields (source_quality, extraction_method) no longer
|
|
216
|
-
# differ between export paths.
|
|
217
|
-
normalized: list[NormalizedEntity] = [
|
|
218
|
-
normalized_entity_from_db_row(e) for e in db_entities
|
|
219
|
-
]
|
|
220
|
-
|
|
221
274
|
session.expunge_all()
|
|
222
|
-
return investigation,
|
|
275
|
+
return investigation, db_entities
|
|
223
276
|
|
|
224
277
|
except Exception as exc:
|
|
225
278
|
logger.warning("_load_investigation_and_entities failed: %s", exc)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Shared relationship loading for structured exporters.
|
|
2
|
+
|
|
3
|
+
The graph builder is the canonical source for both persisted typed edges and
|
|
4
|
+
the bounded co-occurrence edges used by exports. Keeping the enumeration in
|
|
5
|
+
one place prevents one format from silently losing relationships that another
|
|
6
|
+
format preserves.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from typing import Any, Optional
|
|
13
|
+
import uuid
|
|
14
|
+
|
|
15
|
+
from graph.model import RELATIONSHIP_TYPE_STIX
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_export_relationships(
|
|
21
|
+
investigation_id: Any,
|
|
22
|
+
endpoint_map: dict[str, str],
|
|
23
|
+
) -> tuple[list[dict[str, Any]], Optional[str]]:
|
|
24
|
+
"""Return graph relationships whose endpoints exist in ``endpoint_map``.
|
|
25
|
+
|
|
26
|
+
Each result contains the original VoidAccess ``edge_type`` plus its STIX
|
|
27
|
+
spelling. Consumers can therefore share the exact same deduplication and
|
|
28
|
+
endpoint selection while rendering format-specific relationship records.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
from graph.builder import build_graph_from_db # noqa: PLC0415
|
|
32
|
+
|
|
33
|
+
inv_uuid = _coerce_uuid(investigation_id)
|
|
34
|
+
graph = build_graph_from_db(investigation_id=inv_uuid)
|
|
35
|
+
relationships: list[dict[str, Any]] = []
|
|
36
|
+
seen: set[tuple[str, str, str]] = set()
|
|
37
|
+
|
|
38
|
+
for source_node, target_node, data in graph.edges(data=True):
|
|
39
|
+
source_ref = endpoint_map.get(source_node)
|
|
40
|
+
target_ref = endpoint_map.get(target_node)
|
|
41
|
+
if not source_ref or not target_ref:
|
|
42
|
+
continue
|
|
43
|
+
|
|
44
|
+
edge_type = _normalise_edge_type(data.get("edge_type", "related-to"))
|
|
45
|
+
stix_type = RELATIONSHIP_TYPE_STIX.get(edge_type, "related-to")
|
|
46
|
+
key = (stix_type, source_ref, target_ref)
|
|
47
|
+
if key in seen:
|
|
48
|
+
continue
|
|
49
|
+
seen.add(key)
|
|
50
|
+
relationships.append(
|
|
51
|
+
{
|
|
52
|
+
"source_ref": source_ref,
|
|
53
|
+
"target_ref": target_ref,
|
|
54
|
+
"edge_type": edge_type,
|
|
55
|
+
"stix_relationship_type": stix_type,
|
|
56
|
+
"confidence": data.get("confidence"),
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
return relationships, None
|
|
60
|
+
except Exception as exc:
|
|
61
|
+
logger.warning("load_export_relationships failed: %s", exc)
|
|
62
|
+
return [], f"load_export_relationships failed: {exc}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _normalise_edge_type(edge_type: Any) -> str:
|
|
66
|
+
"""Normalise enum/string edge types without changing their meaning."""
|
|
67
|
+
value = getattr(edge_type, "value", edge_type)
|
|
68
|
+
text = str(value or "related-to").strip()
|
|
69
|
+
upper = text.upper()
|
|
70
|
+
if upper in RELATIONSHIP_TYPE_STIX:
|
|
71
|
+
return upper
|
|
72
|
+
return text
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _coerce_uuid(value: Any) -> Optional[uuid.UUID]:
|
|
76
|
+
if isinstance(value, uuid.UUID):
|
|
77
|
+
return value
|
|
78
|
+
try:
|
|
79
|
+
return uuid.UUID(str(value))
|
|
80
|
+
except (ValueError, AttributeError):
|
|
81
|
+
return None
|
|
@@ -18,11 +18,11 @@ from __future__ import annotations
|
|
|
18
18
|
import json
|
|
19
19
|
import logging
|
|
20
20
|
import os
|
|
21
|
+
import re
|
|
21
22
|
import warnings
|
|
22
23
|
from typing import Any, Optional
|
|
23
24
|
import uuid
|
|
24
25
|
|
|
25
|
-
from graph.model import RELATIONSHIP_TYPE_STIX
|
|
26
26
|
from extractor.confidence import get_entity_confidence
|
|
27
27
|
|
|
28
28
|
logger = logging.getLogger(__name__)
|
|
@@ -79,6 +79,114 @@ _STIX_PATTERNS: dict[str, str] = {
|
|
|
79
79
|
# Entity types that map to STIX Malware objects
|
|
80
80
|
_MALWARE_TYPES = frozenset({"MALWARE_FAMILY", "RANSOMWARE_GROUP"})
|
|
81
81
|
|
|
82
|
+
# STIX 2.1's malware-type vocabulary is intentionally small. Keep the
|
|
83
|
+
# mapping here instead of treating every MALWARE_FAMILY as the generic
|
|
84
|
+
# ``malware`` type: family names and enrichment metadata often carry enough
|
|
85
|
+
# information for SIEM consumers to filter useful categories.
|
|
86
|
+
_STIX_MALWARE_TYPE_ALIASES = {
|
|
87
|
+
"infostealer": "spyware",
|
|
88
|
+
"stealer": "spyware",
|
|
89
|
+
"remote_access_trojan": "remote-access-trojan",
|
|
90
|
+
"remote access trojan": "remote-access-trojan",
|
|
91
|
+
"rat": "remote-access-trojan",
|
|
92
|
+
"coinminer": "resource-exploitation",
|
|
93
|
+
"cryptominer": "resource-exploitation",
|
|
94
|
+
"crypto-miner": "resource-exploitation",
|
|
95
|
+
"miner": "resource-exploitation",
|
|
96
|
+
}
|
|
97
|
+
_STIX_MALWARE_TYPES = frozenset({
|
|
98
|
+
"adware",
|
|
99
|
+
"backdoor",
|
|
100
|
+
"bot",
|
|
101
|
+
"ddos",
|
|
102
|
+
"dropper",
|
|
103
|
+
"exploit-kit",
|
|
104
|
+
"keylogger",
|
|
105
|
+
"ransomware",
|
|
106
|
+
"remote-access-trojan",
|
|
107
|
+
"resource-exploitation",
|
|
108
|
+
"rootkit",
|
|
109
|
+
"screen-capture",
|
|
110
|
+
"spyware",
|
|
111
|
+
"wiper",
|
|
112
|
+
"malware",
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
# These are conservative, high-signal family/name hints. Unknown families
|
|
116
|
+
# retain the valid generic STIX type rather than being assigned a speculative
|
|
117
|
+
# category.
|
|
118
|
+
_MALWARE_NAME_PATTERNS = (
|
|
119
|
+
("ransomware", re.compile(
|
|
120
|
+
r"\b(?:ransomware|lockbit|blackcat|alphv|clop|conti|ryuk|hive|revil|darkside|wannacry)\b",
|
|
121
|
+
re.IGNORECASE,
|
|
122
|
+
)),
|
|
123
|
+
("remote-access-trojan", re.compile(
|
|
124
|
+
r"\b(?:remote\s+access\s+trojan|remcos|njrat|nanocore|quasar|darkcomet|xrat)\b",
|
|
125
|
+
re.IGNORECASE,
|
|
126
|
+
)),
|
|
127
|
+
("spyware", re.compile(
|
|
128
|
+
r"\b(?:spyware|infostealer|stealer|redline|vidar|lumma|raccoon|agent\s+tesla|formbook)\b",
|
|
129
|
+
re.IGNORECASE,
|
|
130
|
+
)),
|
|
131
|
+
("bot", re.compile(
|
|
132
|
+
r"\b(?:botnet|emotet|qakbot|qbot|trickbot|mirai|zeus|mozi)\b",
|
|
133
|
+
re.IGNORECASE,
|
|
134
|
+
)),
|
|
135
|
+
("keylogger", re.compile(r"\bkeylogger\b", re.IGNORECASE)),
|
|
136
|
+
("exploit-kit", re.compile(r"\bexploit[\s-]+kit\b", re.IGNORECASE)),
|
|
137
|
+
("rootkit", re.compile(r"\brootkit\b", re.IGNORECASE)),
|
|
138
|
+
("screen-capture", re.compile(r"\bscreen[\s-]+capture\b", re.IGNORECASE)),
|
|
139
|
+
("dropper", re.compile(r"\bdropper\b", re.IGNORECASE)),
|
|
140
|
+
("wiper", re.compile(r"\bwiper\b", re.IGNORECASE)),
|
|
141
|
+
("adware", re.compile(r"\badware\b", re.IGNORECASE)),
|
|
142
|
+
("resource-exploitation", re.compile(
|
|
143
|
+
r"\b(?:cryptominer|crypto[\s-]+miner|coinminer|cryptojacking)\b",
|
|
144
|
+
re.IGNORECASE,
|
|
145
|
+
)),
|
|
146
|
+
("backdoor", re.compile(r"\bbackdoor\b", re.IGNORECASE)),
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _entity_attribute(entity: Any, name: str) -> Any:
|
|
151
|
+
"""Read optional classification metadata from dicts and ORM-like rows."""
|
|
152
|
+
if isinstance(entity, dict):
|
|
153
|
+
return entity.get(name)
|
|
154
|
+
return getattr(entity, name, None)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _normalise_malware_type(value: Any) -> Optional[str]:
|
|
158
|
+
if not isinstance(value, str):
|
|
159
|
+
return None
|
|
160
|
+
key = value.strip().lower().replace("_", " ")
|
|
161
|
+
key = _STIX_MALWARE_TYPE_ALIASES.get(key, key)
|
|
162
|
+
key = key.replace(" ", "-")
|
|
163
|
+
return key if key in _STIX_MALWARE_TYPES else None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _classify_malware(entity: Any) -> list[str]:
|
|
167
|
+
"""Return evidence-backed STIX malware types for an entity."""
|
|
168
|
+
entity_type = str(getattr(entity, "entity_type", "") or "").upper()
|
|
169
|
+
if isinstance(entity, dict):
|
|
170
|
+
entity_type = str(entity.get("entity_type") or "").upper()
|
|
171
|
+
if entity_type == "RANSOMWARE_GROUP":
|
|
172
|
+
return ["ransomware"]
|
|
173
|
+
|
|
174
|
+
for field_name in ("malware_types", "malware_type", "malware_category", "category", "family_type"):
|
|
175
|
+
raw = _entity_attribute(entity, field_name)
|
|
176
|
+
values = raw if isinstance(raw, (list, tuple, set)) else [raw]
|
|
177
|
+
classified = [item for item in (_normalise_malware_type(v) for v in values) if item]
|
|
178
|
+
if classified:
|
|
179
|
+
return list(dict.fromkeys(classified))
|
|
180
|
+
|
|
181
|
+
evidence = " ".join(
|
|
182
|
+
str(_entity_attribute(entity, field) or "")
|
|
183
|
+
for field in ("value", "context_snippet")
|
|
184
|
+
)
|
|
185
|
+
for malware_type, pattern in _MALWARE_NAME_PATTERNS:
|
|
186
|
+
if pattern.search(evidence):
|
|
187
|
+
return [malware_type]
|
|
188
|
+
return ["malware"]
|
|
189
|
+
|
|
82
190
|
# ---------------------------------------------------------------------------
|
|
83
191
|
# Confidence mapping: VoidAccess float → STIX integer (0-100)
|
|
84
192
|
# ---------------------------------------------------------------------------
|
|
@@ -161,6 +269,7 @@ def entity_to_stix_malware(entity: Any) -> Optional[Any]:
|
|
|
161
269
|
malware = stix2.Malware(
|
|
162
270
|
name=entity.value,
|
|
163
271
|
is_family=True,
|
|
272
|
+
malware_types=_classify_malware(entity),
|
|
164
273
|
confidence=_to_stix_confidence(get_entity_confidence(entity)),
|
|
165
274
|
allow_custom=True,
|
|
166
275
|
x_voidaccess_source_quality=getattr(entity, "source_quality", 1.0),
|
|
@@ -176,6 +285,28 @@ def entity_to_stix_malware(entity: Any) -> Optional[Any]:
|
|
|
176
285
|
return None
|
|
177
286
|
|
|
178
287
|
|
|
288
|
+
def entity_to_stix_identity(entity: Any) -> Optional[Any]:
|
|
289
|
+
"""Convert an organization entity to a STIX Identity object."""
|
|
290
|
+
if not _STIX2_AVAILABLE or entity.entity_type != "ORGANIZATION_NAME":
|
|
291
|
+
return None
|
|
292
|
+
try:
|
|
293
|
+
return stix2.Identity(
|
|
294
|
+
name=entity.value,
|
|
295
|
+
identity_class="organization",
|
|
296
|
+
confidence=_to_stix_confidence(get_entity_confidence(entity)),
|
|
297
|
+
allow_custom=True,
|
|
298
|
+
x_voidaccess_source_quality=getattr(entity, "source_quality", 1.0),
|
|
299
|
+
external_references=(
|
|
300
|
+
[{"source_name": "voidaccess", "url": entity.source_url}]
|
|
301
|
+
if entity.source_url
|
|
302
|
+
else []
|
|
303
|
+
),
|
|
304
|
+
)
|
|
305
|
+
except Exception as exc:
|
|
306
|
+
logger.warning("entity_to_stix_identity failed for %r: %s", entity.value, exc)
|
|
307
|
+
return None
|
|
308
|
+
|
|
309
|
+
|
|
179
310
|
def entity_to_stix_threat_actor(entity: Any) -> Optional[Any]:
|
|
180
311
|
"""
|
|
181
312
|
Convert a THREAT_ACTOR_HANDLE entity to a STIX 2.1 ThreatActor object.
|
|
@@ -287,6 +418,11 @@ def investigation_to_stix_bundle(
|
|
|
287
418
|
stix_objects.append(actor)
|
|
288
419
|
_register(entity, actor.id)
|
|
289
420
|
|
|
421
|
+
identity = entity_to_stix_identity(entity)
|
|
422
|
+
if identity:
|
|
423
|
+
stix_objects.append(identity)
|
|
424
|
+
_register(entity, identity.id)
|
|
425
|
+
|
|
290
426
|
if include_relationships and stix_objects:
|
|
291
427
|
relationships, relationship_warning = _build_stix_relationships(
|
|
292
428
|
investigation_id, stix_id_map
|
|
@@ -436,32 +572,22 @@ def _build_stix_relationships(
|
|
|
436
572
|
if not _STIX2_AVAILABLE:
|
|
437
573
|
return [], "stix2 is not installed"
|
|
438
574
|
try:
|
|
439
|
-
from
|
|
440
|
-
|
|
441
|
-
inv_uuid = _coerce_uuid(investigation_id)
|
|
442
|
-
graph = build_graph_from_db(investigation_id=inv_uuid)
|
|
575
|
+
from export.relationships import load_export_relationships # noqa: PLC0415
|
|
443
576
|
|
|
577
|
+
graph_relationships, warning = load_export_relationships(
|
|
578
|
+
investigation_id, stix_id_map
|
|
579
|
+
)
|
|
580
|
+
if warning:
|
|
581
|
+
return [], warning
|
|
444
582
|
relationships: list[Any] = []
|
|
445
|
-
|
|
446
|
-
for source_node, target_node, data in graph.edges(data=True):
|
|
447
|
-
src_stix_id = stix_id_map.get(source_node)
|
|
448
|
-
tgt_stix_id = stix_id_map.get(target_node)
|
|
449
|
-
if not src_stix_id or not tgt_stix_id:
|
|
450
|
-
continue
|
|
451
|
-
edge_type = data.get("edge_type", "related-to")
|
|
452
|
-
# Map VoidAccess edge types to STIX relationship types
|
|
453
|
-
rel_type = _edge_type_to_stix(edge_type)
|
|
454
|
-
relationship_key = (rel_type, src_stix_id, tgt_stix_id)
|
|
455
|
-
if relationship_key in seen_relationships:
|
|
456
|
-
continue
|
|
457
|
-
seen_relationships.add(relationship_key)
|
|
583
|
+
for data in graph_relationships:
|
|
458
584
|
# Carry the edge's own confidence onto the SRO. Typed relationships
|
|
459
585
|
# have a claim-specific confidence distinct from co-occurrence's
|
|
460
586
|
# flat value; STIX 2.1 confidence is an int 0-100.
|
|
461
587
|
rel_kwargs: dict[str, Any] = {
|
|
462
|
-
"relationship_type":
|
|
463
|
-
"source_ref":
|
|
464
|
-
"target_ref":
|
|
588
|
+
"relationship_type": data["stix_relationship_type"],
|
|
589
|
+
"source_ref": data["source_ref"],
|
|
590
|
+
"target_ref": data["target_ref"],
|
|
465
591
|
"allow_custom": True,
|
|
466
592
|
}
|
|
467
593
|
try:
|
|
@@ -488,7 +614,9 @@ def _build_stix_relationships(
|
|
|
488
614
|
|
|
489
615
|
def _edge_type_to_stix(edge_type: str) -> str:
|
|
490
616
|
"""Map VoidAccess graph edge types to STIX relationship type strings."""
|
|
491
|
-
|
|
617
|
+
from export.relationships import _normalise_edge_type # noqa: PLC0415
|
|
618
|
+
from graph.model import RELATIONSHIP_TYPE_STIX # noqa: PLC0415
|
|
619
|
+
return RELATIONSHIP_TYPE_STIX.get(_normalise_edge_type(edge_type), "related-to")
|
|
492
620
|
|
|
493
621
|
|
|
494
622
|
def _coerce_uuid(value: Any) -> Optional[uuid.UUID]:
|
|
@@ -789,8 +789,12 @@ def build_graph_from_db(
|
|
|
789
789
|
.join(
|
|
790
790
|
InvestigationEntityLink,
|
|
791
791
|
InvestigationEntityLink.entity_id == Entity.id,
|
|
792
|
+
isouter=True,
|
|
793
|
+
)
|
|
794
|
+
.filter(
|
|
795
|
+
(Entity.investigation_id == investigation_id)
|
|
796
|
+
| (InvestigationEntityLink.investigation_id == investigation_id)
|
|
792
797
|
)
|
|
793
|
-
.filter(InvestigationEntityLink.investigation_id == investigation_id)
|
|
794
798
|
.options(joinedload(Entity.page))
|
|
795
799
|
)
|
|
796
800
|
else:
|
|
@@ -1230,8 +1234,12 @@ def build_graph_from_db_cached(investigation_id: uuid.UUID) -> nx.MultiDiGraph:
|
|
|
1230
1234
|
.join(
|
|
1231
1235
|
InvestigationEntityLink,
|
|
1232
1236
|
InvestigationEntityLink.entity_id == Entity.id,
|
|
1237
|
+
isouter=True,
|
|
1238
|
+
)
|
|
1239
|
+
.filter(
|
|
1240
|
+
(Entity.investigation_id == investigation_id)
|
|
1241
|
+
| (InvestigationEntityLink.investigation_id == investigation_id)
|
|
1233
1242
|
)
|
|
1234
|
-
.filter(InvestigationEntityLink.investigation_id == investigation_id)
|
|
1235
1243
|
.options(joinedload(Entity.page))
|
|
1236
1244
|
.yield_per(500)
|
|
1237
1245
|
)
|
|
@@ -555,8 +555,10 @@ def _update_email_entities_in_db(
|
|
|
555
555
|
existing.append(tag)
|
|
556
556
|
db_entity.corroborating_sources = json.dumps(existing)
|
|
557
557
|
session.commit()
|
|
558
|
-
except Exception
|
|
559
|
-
|
|
558
|
+
except Exception:
|
|
559
|
+
# Do not log the raw SQLAlchemy exception: its parameter dump can
|
|
560
|
+
# contain the full email address being enriched.
|
|
561
|
+
logger.warning("email_reputation: DB update failed")
|
|
560
562
|
|
|
561
563
|
|
|
562
564
|
# ---------------------------------------------------------------------------
|