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