voidaccess 1.7.1__tar.gz → 1.7.2__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.7.1 → voidaccess-1.7.2}/PKG-INFO +7 -7
- {voidaccess-1.7.1 → voidaccess-1.7.2}/README.md +6 -6
- {voidaccess-1.7.1 → voidaccess-1.7.2}/api/routes/actors.py +10 -20
- {voidaccess-1.7.1 → voidaccess-1.7.2}/api/routes/entities.py +8 -12
- {voidaccess-1.7.1 → voidaccess-1.7.2}/api/routes/investigations.py +34 -36
- {voidaccess-1.7.1 → voidaccess-1.7.2}/api/routes/search.py +4 -6
- {voidaccess-1.7.1 → voidaccess-1.7.2}/config.py +13 -7
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/queries.py +4 -5
- {voidaccess-1.7.1 → voidaccess-1.7.2}/export/ioc_package.py +1 -1
- {voidaccess-1.7.1 → voidaccess-1.7.2}/export/misp.py +2 -3
- {voidaccess-1.7.1 → voidaccess-1.7.2}/export/sigma.py +2 -3
- {voidaccess-1.7.1 → voidaccess-1.7.2}/export/stix.py +10 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/extractor/normalizer.py +95 -1
- {voidaccess-1.7.1 → voidaccess-1.7.2}/extractor/pipeline.py +9 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/graph/builder.py +143 -12
- {voidaccess-1.7.1 → voidaccess-1.7.2}/pyproject.toml +1 -1
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/actor_profiles.py +2 -2
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess/llm.py +26 -16
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess.egg-info/PKG-INFO +7 -7
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess_cli/__init__.py +1 -1
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess_cli/adapters/sqlite.py +2 -2
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess_cli/commands/investigate.py +20 -17
- {voidaccess-1.7.1 → voidaccess-1.7.2}/LICENSE +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/analysis/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/analysis/opsec.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/analysis/patterns.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/analysis/temporal.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/api/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/api/auth.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/api/main.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/api/routes/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/api/routes/admin.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/api/routes/auth.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/api/routes/export.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/api/routes/monitors.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/api/routes/settings.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/auth/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/auth/token_blacklist.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/crawler/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/crawler/dedup.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/crawler/frontier.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/crawler/spider.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/crawler/utils.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/__init__.py +0 -0
- {voidaccess-1.7.1/voidaccess → voidaccess-1.7.2/db/migrations}/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/env.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0001_initial_schema.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0002_add_investigation_status_column.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0002_add_missing_tables.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0003_add_canonical_value_and_entity_links.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0004_add_page_posted_at.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0005_add_extraction_method.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0006_add_monitor_alerts.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0007_add_actor_style_profiles.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0008_add_users_table.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0009_add_investigation_id_to_relationships.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0010_add_composite_index_entity_relationships.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0011_add_page_extraction_cache.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0013_add_graph_status.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0015_add_progress_fields.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0016_backfill_graph_status.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0017_add_user_api_keys.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0018_add_user_id_to_investigations.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0019_add_content_safety_log.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0020_add_entity_source_tracking.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0021_add_search_engine_stats.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0022_add_actor_profiles.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/migrations/versions/0023_add_investigation_metadata.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/models.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/search_engine_stats.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/db/session.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/export/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/export/snort_export.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/export/yara_export.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/extractor/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/extractor/llm_extract.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/extractor/ner.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/extractor/regex_patterns.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/fingerprint/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/fingerprint/profiler.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/fingerprint/stylometry.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/graph/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/graph/export.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/graph/model.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/graph/queries.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/graph/visualize.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/i18n/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/i18n/detect.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/i18n/query_expand.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/i18n/translate.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/monitor/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/monitor/_db.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/monitor/alerts.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/monitor/config.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/monitor/diff.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/monitor/jobs.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/monitor/scheduler.py +0 -0
- {voidaccess-1.7.1/utils → voidaccess-1.7.2/scraper}/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/scraper/scrape.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/scraper/scrape_js.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/search/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/search/circuit_breaker.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/search/query_builder.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/search/search.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/setup.cfg +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/blockchain.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/cache.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/cisa.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/data/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/data/onion_seeds.json +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/dns_enrichment.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/domain_reputation.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/email_reputation.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/engines.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/enrichment.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/github_scraper.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/gitlab_scraper.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/hash_reputation.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/historical_intel.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/ip_reputation.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/paste_scraper.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/pastes.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/proxy_client.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/rss_scraper.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/seed_manager.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/seeds.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/shodan.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/telegram.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/sources/virustotal.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_analysis_opsec.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_analysis_stylometry.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_analysis_temporal.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_api.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_api_monitors.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_blockchain.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_cli_proxy_config.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_confidence_calibration.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_config.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_crawler.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_db.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_dns_enrichment.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_domain_reputation.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_email_reputation.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_filter_e2e_manual.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_filter_parser_manual.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_fingerprint.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_github_scraper.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_gitlab_scraper.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_graph.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_graph_integration.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_hash_reputation.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_i18n.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_ip_reputation.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_llm.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_llm_utils.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_model_singleton.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_monitor.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_monitor_integration.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_pagination.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_paste_scraper.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_proxy_client.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_regex_patterns.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_rss_scraper.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_scrape_js.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_settings.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_snort_export.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_sources.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_sources_enrichment_new.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_stix_export.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_vector.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/tests/test_yara_export.py +0 -0
- {voidaccess-1.7.1/scraper → voidaccess-1.7.2/utils}/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/utils/async_utils.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/utils/content_safety.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/utils/defang.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/utils/encryption.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/utils/enrichment_cache.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/utils/ioc_freshness.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/utils/user_keys.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/vector/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/vector/embedder.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/vector/model_singleton.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/vector/search.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/vector/store.py +0 -0
- {voidaccess-1.7.1/db/migrations → voidaccess-1.7.2/voidaccess}/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess/config.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess/llm_utils.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess.egg-info/SOURCES.txt +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess.egg-info/dependency_links.txt +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess.egg-info/entry_points.txt +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess.egg-info/requires.txt +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess.egg-info/top_level.txt +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess_cli/adapters/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess_cli/browser.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess_cli/commands/__init__.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess_cli/commands/actors.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess_cli/commands/configure.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess_cli/commands/enrich.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess_cli/commands/export.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess_cli/commands/show.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess_cli/config.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess_cli/display.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess_cli/main.py +0 -0
- {voidaccess-1.7.1 → voidaccess-1.7.2}/voidaccess_cli/tor_detect.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voidaccess
|
|
3
|
-
Version: 1.7.
|
|
3
|
+
Version: 1.7.2
|
|
4
4
|
Summary: Dark web OSINT CLI — automated threat intelligence from query to report
|
|
5
5
|
Author: VoidAccess
|
|
6
6
|
License-Expression: MIT
|
|
@@ -104,11 +104,12 @@ Commercial threat intelligence platforms often charge prohibitive annual fees fo
|
|
|
104
104
|
|
|
105
105
|
---
|
|
106
106
|
|
|
107
|
-
## What's New in v1.7.
|
|
108
|
-
- STIX export
|
|
109
|
-
-
|
|
110
|
-
-
|
|
111
|
-
-
|
|
107
|
+
## What's New in v1.7.2
|
|
108
|
+
- STIX export relationship generation is bounded and semantic, avoiding page-level cartesian edge explosions at larger entity counts.
|
|
109
|
+
- Repeated NER false positives for threat actor handles and organization names are filtered before persistence.
|
|
110
|
+
- MISP/Sigma/API query paths no longer use the SQLAlchemy subquery-in-`IN()` pattern that produced warnings.
|
|
111
|
+
- Optional configuration warnings are summarized once per process.
|
|
112
|
+
- Refined LLM query output is persisted as a short search string, not explanatory Markdown.
|
|
112
113
|
|
|
113
114
|
---
|
|
114
115
|
|
|
@@ -585,4 +586,3 @@ To report a security vulnerability, see [docs/SECURITY.md](docs/SECURITY.md).
|
|
|
585
586
|
|
|
586
587
|
MIT License. See [LICENSE](LICENSE) for details.
|
|
587
588
|
|
|
588
|
-
|
|
@@ -19,11 +19,12 @@ Commercial threat intelligence platforms often charge prohibitive annual fees fo
|
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
## What's New in v1.7.
|
|
23
|
-
- STIX export
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
-
|
|
22
|
+
## What's New in v1.7.2
|
|
23
|
+
- STIX export relationship generation is bounded and semantic, avoiding page-level cartesian edge explosions at larger entity counts.
|
|
24
|
+
- Repeated NER false positives for threat actor handles and organization names are filtered before persistence.
|
|
25
|
+
- MISP/Sigma/API query paths no longer use the SQLAlchemy subquery-in-`IN()` pattern that produced warnings.
|
|
26
|
+
- Optional configuration warnings are summarized once per process.
|
|
27
|
+
- Refined LLM query output is persisted as a short search string, not explanatory Markdown.
|
|
27
28
|
|
|
28
29
|
---
|
|
29
30
|
|
|
@@ -500,4 +501,3 @@ To report a security vulnerability, see [docs/SECURITY.md](docs/SECURITY.md).
|
|
|
500
501
|
|
|
501
502
|
MIT License. See [LICENSE](LICENSE) for details.
|
|
502
503
|
|
|
503
|
-
|
|
@@ -22,7 +22,7 @@ from typing import Optional
|
|
|
22
22
|
|
|
23
23
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
24
24
|
from pydantic import BaseModel, Field
|
|
25
|
-
from sqlalchemy import or_
|
|
25
|
+
from sqlalchemy import or_, select
|
|
26
26
|
|
|
27
27
|
from api.auth import CurrentUser, get_current_user
|
|
28
28
|
|
|
@@ -88,26 +88,22 @@ def _db_required() -> None:
|
|
|
88
88
|
raise HTTPException(status_code=503, detail="Database not configured")
|
|
89
89
|
|
|
90
90
|
|
|
91
|
-
def
|
|
91
|
+
def _user_owned_investigation_ids_select(user_id: int):
|
|
92
92
|
from db.models import Investigation
|
|
93
93
|
|
|
94
|
-
return (
|
|
95
|
-
session.query(Investigation.id)
|
|
96
|
-
.filter(Investigation.user_id == user_id)
|
|
97
|
-
.subquery()
|
|
98
|
-
)
|
|
94
|
+
return select(Investigation.id).where(Investigation.user_id == user_id)
|
|
99
95
|
|
|
100
96
|
|
|
101
97
|
def _profile_accessible(session, profile_id: str, user_id: int) -> bool:
|
|
102
98
|
"""True when at least one investigation linked to the profile is owned by the user."""
|
|
103
99
|
from db.models import ActorAlias, ActorInfrastructure
|
|
104
100
|
|
|
105
|
-
|
|
101
|
+
inv_ids_select = _user_owned_investigation_ids_select(user_id)
|
|
106
102
|
alias_match = (
|
|
107
103
|
session.query(ActorAlias.id)
|
|
108
104
|
.filter(
|
|
109
105
|
ActorAlias.actor_id == profile_id,
|
|
110
|
-
ActorAlias.source_investigation_id.in_(
|
|
106
|
+
ActorAlias.source_investigation_id.in_(inv_ids_select),
|
|
111
107
|
)
|
|
112
108
|
.first()
|
|
113
109
|
)
|
|
@@ -117,7 +113,7 @@ def _profile_accessible(session, profile_id: str, user_id: int) -> bool:
|
|
|
117
113
|
session.query(ActorInfrastructure.id)
|
|
118
114
|
.filter(
|
|
119
115
|
ActorInfrastructure.actor_id == profile_id,
|
|
120
|
-
ActorInfrastructure.source_investigation_id.in_(
|
|
116
|
+
ActorInfrastructure.source_investigation_id.in_(inv_ids_select),
|
|
121
117
|
)
|
|
122
118
|
.first()
|
|
123
119
|
)
|
|
@@ -146,8 +142,6 @@ async def list_actors(
|
|
|
146
142
|
users since the seed is public data.
|
|
147
143
|
"""
|
|
148
144
|
_db_required()
|
|
149
|
-
from sqlalchemy import select
|
|
150
|
-
|
|
151
145
|
from db.models import (
|
|
152
146
|
ActorProfile,
|
|
153
147
|
ActorAlias,
|
|
@@ -156,9 +150,7 @@ async def list_actors(
|
|
|
156
150
|
|
|
157
151
|
try:
|
|
158
152
|
with _session() as session:
|
|
159
|
-
user_inv_ids =
|
|
160
|
-
session, current_user.user.id
|
|
161
|
-
)
|
|
153
|
+
user_inv_ids = _user_owned_investigation_ids_select(current_user.user.id)
|
|
162
154
|
|
|
163
155
|
# Subquery of profile IDs visible to this user
|
|
164
156
|
visible_alias = select(ActorAlias.actor_id).where(
|
|
@@ -176,13 +168,13 @@ async def list_actors(
|
|
|
176
168
|
)
|
|
177
169
|
if search:
|
|
178
170
|
pattern = f"%{search.lower()}%"
|
|
179
|
-
|
|
171
|
+
alias_select = select(ActorAlias.actor_id).where(
|
|
180
172
|
ActorAlias.alias_value.ilike(pattern)
|
|
181
173
|
)
|
|
182
174
|
q = q.filter(
|
|
183
175
|
or_(
|
|
184
176
|
ActorProfile.canonical_handle.ilike(pattern),
|
|
185
|
-
ActorProfile.id.in_(
|
|
177
|
+
ActorProfile.id.in_(alias_select),
|
|
186
178
|
)
|
|
187
179
|
)
|
|
188
180
|
|
|
@@ -313,9 +305,7 @@ async def get_actor_investigations(
|
|
|
313
305
|
|
|
314
306
|
try:
|
|
315
307
|
with _session() as session:
|
|
316
|
-
user_inv_ids =
|
|
317
|
-
session, current_user.user.id
|
|
318
|
-
)
|
|
308
|
+
user_inv_ids = _user_owned_investigation_ids_select(current_user.user.id)
|
|
319
309
|
all_ids = [
|
|
320
310
|
uuid_value(s)
|
|
321
311
|
for s in profile.get("investigation_ids", [])
|
|
@@ -57,14 +57,12 @@ async def list_entities(
|
|
|
57
57
|
|
|
58
58
|
with get_session() as session:
|
|
59
59
|
user_inv_ids = (
|
|
60
|
-
|
|
61
|
-
.
|
|
62
|
-
.subquery()
|
|
60
|
+
sa.select(Investigation.id)
|
|
61
|
+
.where(Investigation.user_id == current_user.user.id)
|
|
63
62
|
)
|
|
64
63
|
linked_entity_ids = (
|
|
65
|
-
|
|
66
|
-
.
|
|
67
|
-
.subquery()
|
|
64
|
+
sa.select(InvestigationEntityLink.entity_id)
|
|
65
|
+
.where(InvestigationEntityLink.investigation_id.in_(user_inv_ids))
|
|
68
66
|
)
|
|
69
67
|
q = session.query(Entity).filter(
|
|
70
68
|
sa.or_(
|
|
@@ -749,14 +747,12 @@ def _assert_entity_accessible(session, entity_id: uuid.UUID, user_id: int) -> No
|
|
|
749
747
|
from db.models import Entity, Investigation, InvestigationEntityLink # noqa: PLC0415
|
|
750
748
|
|
|
751
749
|
user_inv_ids = (
|
|
752
|
-
|
|
753
|
-
.
|
|
754
|
-
.subquery()
|
|
750
|
+
sa.select(Investigation.id)
|
|
751
|
+
.where(Investigation.user_id == user_id)
|
|
755
752
|
)
|
|
756
753
|
linked_entity_ids = (
|
|
757
|
-
|
|
758
|
-
.
|
|
759
|
-
.subquery()
|
|
754
|
+
sa.select(InvestigationEntityLink.entity_id)
|
|
755
|
+
.where(InvestigationEntityLink.investigation_id.in_(user_inv_ids))
|
|
760
756
|
)
|
|
761
757
|
accessible = (
|
|
762
758
|
session.query(Entity.id)
|
|
@@ -672,30 +672,30 @@ def _get_db_investigation(investigation_id: str) -> Any:
|
|
|
672
672
|
)
|
|
673
673
|
|
|
674
674
|
# Entity IDs for this investigation = own entities + junction-table links
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
.
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
)
|
|
686
|
-
|
|
687
|
-
)
|
|
688
|
-
entity_count = int(
|
|
689
|
-
session.query(func.count()).select_from(entity_subq).scalar() or 0
|
|
690
|
-
)
|
|
691
|
-
relationship_count = int(
|
|
692
|
-
session.query(func.count(EntityRelationship.id))
|
|
693
|
-
.filter(
|
|
694
|
-
(EntityRelationship.entity_a_id.in_(
|
|
695
|
-
| (EntityRelationship.entity_b_id.in_(
|
|
696
|
-
)
|
|
697
|
-
.scalar()
|
|
698
|
-
or 0
|
|
675
|
+
linked_ids_select = (
|
|
676
|
+
sa_select(InvestigationEntityLink.entity_id)
|
|
677
|
+
.where(InvestigationEntityLink.investigation_id == inv.id)
|
|
678
|
+
)
|
|
679
|
+
entity_subq = (
|
|
680
|
+
session.query(Entity.id)
|
|
681
|
+
.filter(
|
|
682
|
+
(Entity.investigation_id == inv.id)
|
|
683
|
+
| Entity.id.in_(linked_ids_select)
|
|
684
|
+
)
|
|
685
|
+
.subquery()
|
|
686
|
+
)
|
|
687
|
+
entity_ids_select = sa_select(entity_subq.c.id)
|
|
688
|
+
entity_count = int(
|
|
689
|
+
session.query(func.count()).select_from(entity_subq).scalar() or 0
|
|
690
|
+
)
|
|
691
|
+
relationship_count = int(
|
|
692
|
+
session.query(func.count(EntityRelationship.id))
|
|
693
|
+
.filter(
|
|
694
|
+
(EntityRelationship.entity_a_id.in_(entity_ids_select))
|
|
695
|
+
| (EntityRelationship.entity_b_id.in_(entity_ids_select))
|
|
696
|
+
)
|
|
697
|
+
.scalar()
|
|
698
|
+
or 0
|
|
699
699
|
)
|
|
700
700
|
|
|
701
701
|
# Phase 6.1 — sources_used / infrastructure_clusters from DB metadata.
|
|
@@ -2673,14 +2673,13 @@ async def get_investigation_entities(
|
|
|
2673
2673
|
if str(inv.user_id) != str(current_user.user.id):
|
|
2674
2674
|
raise HTTPException(status_code=403, detail="Forbidden")
|
|
2675
2675
|
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
.
|
|
2679
|
-
|
|
2680
|
-
)
|
|
2676
|
+
linked_ids_select = (
|
|
2677
|
+
sa_select(InvestigationEntityLink.entity_id)
|
|
2678
|
+
.where(InvestigationEntityLink.investigation_id == inv.id)
|
|
2679
|
+
)
|
|
2681
2680
|
query = session.query(Entity).filter(
|
|
2682
2681
|
(Entity.investigation_id == inv.id)
|
|
2683
|
-
| Entity.id.in_(
|
|
2682
|
+
| Entity.id.in_(linked_ids_select)
|
|
2684
2683
|
)
|
|
2685
2684
|
if entity_type:
|
|
2686
2685
|
query = query.filter(Entity.entity_type == entity_type)
|
|
@@ -2800,16 +2799,15 @@ async def export_investigation_entities_csv(
|
|
|
2800
2799
|
if str(inv.user_id) != str(current_user.user.id):
|
|
2801
2800
|
raise HTTPException(status_code=403, detail="Forbidden")
|
|
2802
2801
|
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
.
|
|
2806
|
-
|
|
2807
|
-
)
|
|
2802
|
+
linked_ids_select = (
|
|
2803
|
+
sa_select(InvestigationEntityLink.entity_id)
|
|
2804
|
+
.where(InvestigationEntityLink.investigation_id == inv.id)
|
|
2805
|
+
)
|
|
2808
2806
|
entities = (
|
|
2809
2807
|
session.query(Entity)
|
|
2810
2808
|
.filter(
|
|
2811
2809
|
(Entity.investigation_id == inv.id)
|
|
2812
|
-
| Entity.id.in_(
|
|
2810
|
+
| Entity.id.in_(linked_ids_select)
|
|
2813
2811
|
)
|
|
2814
2812
|
.all()
|
|
2815
2813
|
)
|
|
@@ -138,14 +138,12 @@ async def search_entities(
|
|
|
138
138
|
|
|
139
139
|
with get_session() as session:
|
|
140
140
|
user_inv_ids = (
|
|
141
|
-
|
|
142
|
-
.
|
|
143
|
-
.subquery()
|
|
141
|
+
sa.select(Investigation.id)
|
|
142
|
+
.where(Investigation.user_id == current_user.user.id)
|
|
144
143
|
)
|
|
145
144
|
linked_entity_ids = (
|
|
146
|
-
|
|
147
|
-
.
|
|
148
|
-
.subquery()
|
|
145
|
+
sa.select(InvestigationEntityLink.entity_id)
|
|
146
|
+
.where(InvestigationEntityLink.investigation_id.in_(user_inv_ids))
|
|
149
147
|
)
|
|
150
148
|
q = session.query(Entity).filter(
|
|
151
149
|
sa.or_(
|
|
@@ -6,6 +6,7 @@ from dotenv import load_dotenv
|
|
|
6
6
|
load_dotenv()
|
|
7
7
|
|
|
8
8
|
logger = logging.getLogger(__name__)
|
|
9
|
+
_OPTIONAL_CONFIG_WARNING_SHOWN = False
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def _clean_env(name, default=None):
|
|
@@ -183,19 +184,24 @@ OPTIONAL_KEYS = [
|
|
|
183
184
|
|
|
184
185
|
|
|
185
186
|
def validate_config():
|
|
187
|
+
global _OPTIONAL_CONFIG_WARNING_SHOWN
|
|
186
188
|
missing_required = []
|
|
187
189
|
for key in REQUIRED_KEYS:
|
|
188
190
|
if _clean_env(key) is None:
|
|
189
191
|
missing_required.append(key)
|
|
190
192
|
if missing_required:
|
|
191
193
|
raise RuntimeError(f"Missing required configuration keys: {', '.join(missing_required)}")
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
)
|
|
194
|
+
missing_optional = [
|
|
195
|
+
key for key in OPTIONAL_KEYS
|
|
196
|
+
if (_clean_env(key) is None or _clean_env(key) == "")
|
|
197
|
+
]
|
|
198
|
+
if missing_optional and not _OPTIONAL_CONFIG_WARNING_SHOWN:
|
|
199
|
+
logger.warning(
|
|
200
|
+
"Optional configuration keys not set (%d): %s - related features will be disabled",
|
|
201
|
+
len(missing_optional),
|
|
202
|
+
", ".join(missing_optional),
|
|
203
|
+
)
|
|
204
|
+
_OPTIONAL_CONFIG_WARNING_SHOWN = True
|
|
199
205
|
|
|
200
206
|
|
|
201
207
|
validate_config()
|
|
@@ -69,16 +69,15 @@ def count_distinct_pages_for_investigation(
|
|
|
69
69
|
junction table (deduped entities from previous investigations re-linked here).
|
|
70
70
|
"""
|
|
71
71
|
from db.models import InvestigationEntityLink # noqa: PLC0415
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.
|
|
75
|
-
.subquery()
|
|
72
|
+
linked_ids_select = (
|
|
73
|
+
sa.select(InvestigationEntityLink.entity_id)
|
|
74
|
+
.where(InvestigationEntityLink.investigation_id == investigation_id)
|
|
76
75
|
)
|
|
77
76
|
n = (
|
|
78
77
|
session.query(sa.func.count(sa.distinct(Entity.page_id)))
|
|
79
78
|
.filter(
|
|
80
79
|
(Entity.investigation_id == investigation_id)
|
|
81
|
-
| Entity.id.in_(
|
|
80
|
+
| Entity.id.in_(linked_ids_select)
|
|
82
81
|
)
|
|
83
82
|
.scalar()
|
|
84
83
|
)
|
|
@@ -54,7 +54,7 @@ logger = logging.getLogger(__name__)
|
|
|
54
54
|
# ---------------------------------------------------------------------------
|
|
55
55
|
|
|
56
56
|
PACKAGE_FORMAT = "voidaccess-ioc-v1"
|
|
57
|
-
PACKAGE_VERSION = "1.7.
|
|
57
|
+
PACKAGE_VERSION = "1.7.2"
|
|
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.
|
|
@@ -192,16 +192,15 @@ def _load_investigation_and_entities(
|
|
|
192
192
|
if investigation is None:
|
|
193
193
|
return None, []
|
|
194
194
|
|
|
195
|
-
|
|
195
|
+
linked_ids_select = (
|
|
196
196
|
session.query(InvestigationEntityLink.entity_id)
|
|
197
197
|
.filter(InvestigationEntityLink.investigation_id == investigation.id)
|
|
198
|
-
.subquery()
|
|
199
198
|
)
|
|
200
199
|
db_entities = (
|
|
201
200
|
session.query(Entity)
|
|
202
201
|
.filter(
|
|
203
202
|
(Entity.investigation_id == investigation.id)
|
|
204
|
-
| Entity.id.in_(
|
|
203
|
+
| Entity.id.in_(linked_ids_select)
|
|
205
204
|
)
|
|
206
205
|
.all()
|
|
207
206
|
)
|
|
@@ -294,16 +294,15 @@ def _load_entities_for_investigation(investigation_id: Any) -> list[Any]:
|
|
|
294
294
|
if inv is None:
|
|
295
295
|
return []
|
|
296
296
|
|
|
297
|
-
|
|
297
|
+
linked_ids_select = (
|
|
298
298
|
session.query(InvestigationEntityLink.entity_id)
|
|
299
299
|
.filter(InvestigationEntityLink.investigation_id == inv.id)
|
|
300
|
-
.subquery()
|
|
301
300
|
)
|
|
302
301
|
db_entities = (
|
|
303
302
|
session.query(Entity)
|
|
304
303
|
.filter(
|
|
305
304
|
(Entity.investigation_id == inv.id)
|
|
306
|
-
| Entity.id.in_(
|
|
305
|
+
| Entity.id.in_(linked_ids_select)
|
|
307
306
|
)
|
|
308
307
|
.all()
|
|
309
308
|
)
|
|
@@ -63,6 +63,9 @@ _STIX_PATTERNS: dict[str, str] = {
|
|
|
63
63
|
"ETHEREUM_ADDRESS": "[cryptocurrency-wallet:address = '{value}']",
|
|
64
64
|
"MONERO_ADDRESS": "[cryptocurrency-wallet:address = '{value}']",
|
|
65
65
|
"EMAIL_ADDRESS": "[email-message:from_ref.value = '{value}']",
|
|
66
|
+
"FILE_HASH_MD5": "[file:hashes.MD5 = '{value}']",
|
|
67
|
+
"FILE_HASH_SHA1": "[file:hashes.'SHA-1' = '{value}']",
|
|
68
|
+
"FILE_HASH_SHA256": "[file:hashes.'SHA-256' = '{value}']",
|
|
66
69
|
"ONION_URL": "[url:value = '{value}']",
|
|
67
70
|
"IP_ADDRESS": "[ipv4-addr:value = '{value}']",
|
|
68
71
|
"CVE_NUMBER": "[vulnerability:name = '{value}']",
|
|
@@ -111,6 +114,8 @@ def entity_to_stix_indicator(entity: Any) -> Optional[Any]:
|
|
|
111
114
|
indicator_types = ["malicious-activity"]
|
|
112
115
|
elif etype in ("BITCOIN_ADDRESS", "ETHEREUM_ADDRESS", "MONERO_ADDRESS"):
|
|
113
116
|
indicator_types = ["malicious-activity"]
|
|
117
|
+
elif etype in ("FILE_HASH_MD5", "FILE_HASH_SHA1", "FILE_HASH_SHA256"):
|
|
118
|
+
indicator_types = ["malicious-activity"]
|
|
114
119
|
elif etype in ("IP_ADDRESS", "ONION_URL"):
|
|
115
120
|
indicator_types = ["malicious-activity"]
|
|
116
121
|
elif etype == "CVE_NUMBER":
|
|
@@ -424,6 +429,7 @@ def _build_stix_relationships(
|
|
|
424
429
|
graph = build_graph_from_db(investigation_id=inv_uuid)
|
|
425
430
|
|
|
426
431
|
relationships: list[Any] = []
|
|
432
|
+
seen_relationships: set[tuple[str, str, str]] = set()
|
|
427
433
|
for source_node, target_node, data in graph.edges(data=True):
|
|
428
434
|
src_stix_id = stix_id_map.get(source_node)
|
|
429
435
|
tgt_stix_id = stix_id_map.get(target_node)
|
|
@@ -432,6 +438,10 @@ def _build_stix_relationships(
|
|
|
432
438
|
edge_type = data.get("edge_type", "related-to")
|
|
433
439
|
# Map VoidAccess edge types to STIX relationship types
|
|
434
440
|
rel_type = _edge_type_to_stix(edge_type)
|
|
441
|
+
relationship_key = (rel_type, src_stix_id, tgt_stix_id)
|
|
442
|
+
if relationship_key in seen_relationships:
|
|
443
|
+
continue
|
|
444
|
+
seen_relationships.add(relationship_key)
|
|
435
445
|
try:
|
|
436
446
|
rel = stix2.Relationship(
|
|
437
447
|
relationship_type=rel_type,
|
|
@@ -623,6 +623,11 @@ ENTITY_BLOCKLIST: frozenset[str] = frozenset({
|
|
|
623
623
|
"moderator", "administrator", "operator", "staff", "support",
|
|
624
624
|
"customer", "vendor", "buyer", "seller", "trader",
|
|
625
625
|
"dropper", "loader", "stager", "payload", "beacon",
|
|
626
|
+
# Audit-confirmed NER false positives (1.6.2, 1.7.0, 1.7.1).
|
|
627
|
+
"asks", "keyword", "sends", "input", "content", "delivery", "into",
|
|
628
|
+
"submits", "advertising", "attempts", "pass", "consent", "devices",
|
|
629
|
+
"stdin", "stdout", "detect", "uri_regex:1", "intel", "passive",
|
|
630
|
+
"commands",
|
|
626
631
|
})
|
|
627
632
|
|
|
628
633
|
KNOWN_TOOLS: frozenset[str] = frozenset({
|
|
@@ -633,7 +638,15 @@ KNOWN_TOOLS: frozenset[str] = frozenset({
|
|
|
633
638
|
"kali", "parrot", "blackarch", "backtrack",
|
|
634
639
|
})
|
|
635
640
|
|
|
641
|
+
COMMON_THREAT_TERMS: frozenset[str] = frozenset({
|
|
642
|
+
"apt41", "apt28", "apt29", "fin7", "fin8", "lockbitsupp",
|
|
643
|
+
"scatteredspider", "lazarus", "sandworm", "blackcat", "alphv",
|
|
644
|
+
})
|
|
645
|
+
|
|
636
646
|
LEET_GENERIC = re.compile(r"^h[4a][ck]+[3e]?r?$")
|
|
647
|
+
HANDLE_SHAPE_RE = re.compile(
|
|
648
|
+
r"^(?:[A-Z]{2,}\d{1,3}|[A-Za-z]+[A-Z][A-Za-z0-9]*|[A-Za-z0-9]+[_\-.][A-Za-z0-9_.-]+|[A-Za-z]+\d+[A-Za-z0-9]*)$"
|
|
649
|
+
)
|
|
637
650
|
|
|
638
651
|
|
|
639
652
|
ENTITY_MIN_LENGTH: dict[str, int] = {
|
|
@@ -723,6 +736,13 @@ def is_blocked_entity(entity_type: str, entity_value: str) -> bool:
|
|
|
723
736
|
return True
|
|
724
737
|
if LEET_GENERIC.match(value_lower):
|
|
725
738
|
return True
|
|
739
|
+
if value_lower in COMMON_THREAT_TERMS:
|
|
740
|
+
return False
|
|
741
|
+
if re.fullmatch(r"[a-z]+", value_lower) and not HANDLE_SHAPE_RE.match(entity_value.strip()):
|
|
742
|
+
return True
|
|
743
|
+
|
|
744
|
+
if entity_type == "ORGANIZATION_NAME" and value_lower in ENTITY_BLOCKLIST:
|
|
745
|
+
return True
|
|
726
746
|
|
|
727
747
|
min_len = ENTITY_MIN_LENGTH.get(entity_type, 3)
|
|
728
748
|
if len(value_lower) < min_len:
|
|
@@ -868,13 +888,87 @@ _ORG_NOISE_RE = re.compile(
|
|
|
868
888
|
r'|[<>{}\[\]|\\]',
|
|
869
889
|
re.IGNORECASE,
|
|
870
890
|
)
|
|
891
|
+
_HTML_ENTITY_RE = re.compile(r"&(?:#[0-9]+|#x[0-9a-f]+|[a-z][a-z0-9]+);?", re.IGNORECASE)
|
|
892
|
+
_DECODED_HTML_UI_RE = re.compile(r"[\u2190-\u21ff\u00ae\u00a9\u2122]")
|
|
893
|
+
_ORG_SENTENCE_WORDS: frozenset[str] = frozenset({
|
|
894
|
+
"the", "a", "an", "how", "cancel", "reply", "post", "posts", "latest",
|
|
895
|
+
"popular", "tutorial", "tutorials", "guide", "home", "news", "security",
|
|
896
|
+
"advertisement", "advertising", "speaking", "subscribe", "mailing",
|
|
897
|
+
"list", "leave", "recent", "author", "actively", "exploited", "analyzing",
|
|
898
|
+
"targeted", "intrusions", "through", "stay", "upcoming", "seizes",
|
|
899
|
+
"related", "articles", "dummies", "vulnerability", "date", "added",
|
|
900
|
+
"expands", "leadership", "approach", "radar", "supply", "chain",
|
|
901
|
+
"compromise", "commercial", "live", "proxies", "software", "office",
|
|
902
|
+
"oauth", "os", "application", "server", "protect", "sharepoint",
|
|
903
|
+
"tv", "stealer", "stack", "protection", "organization", "redacted",
|
|
904
|
+
"organizations", "are", "building", "achieves", "patches", "moves",
|
|
905
|
+
"contact", "rule", "addition", "activated", "scale", "automation",
|
|
906
|
+
"algorithmic", "payload", "decomposition", "third-party", "third",
|
|
907
|
+
"party", "additional", "resources", "learn", "join", "dll",
|
|
908
|
+
"sideloading", "factory", "malware", "threat", "research", "tags",
|
|
909
|
+
"copilot", "next-gen", "xmrig", "authenticode", "updatefactor",
|
|
910
|
+
"coldfusion", "falcon", "openid", "overwatch", "analysis", "table",
|
|
911
|
+
"antimalware", "scan", "interface", "bypass", "static", "file-size",
|
|
912
|
+
"inflation", "loaders", "data", "exe", "getassertion",
|
|
913
|
+
"getcredentials", "imphash", "cluster", "role", "remote", "desktop",
|
|
914
|
+
"protocol", "sha-256", "webauthn", "yubikey", "systemagentservice",
|
|
915
|
+
"chromium", "clientdatahash", "gift", "card", "fraud", "macos",
|
|
916
|
+
"linux", "tools", "typescript", "webassembly", "webhid",
|
|
917
|
+
"ai-hallucinated", "hallucinated", "domains", "comparison", "llm",
|
|
918
|
+
"dataset", "jaccard", "multi-signal", "risk", "classification",
|
|
919
|
+
"share", "executive", "summary", "temperature", "configuration",
|
|
920
|
+
"account", "numbers", "balanced", "llm2", "briefs",
|
|
921
|
+
})
|
|
922
|
+
_ORG_ALLOWED_CONNECTORS: frozenset[str] = frozenset({
|
|
923
|
+
"and", "of", "for", "to", "in", "on", "with",
|
|
924
|
+
})
|
|
925
|
+
_ORG_SUFFIX_HINTS: frozenset[str] = frozenset({
|
|
926
|
+
"agency", "bank", "bureau", "center", "centre", "committee", "company",
|
|
927
|
+
"corp", "corporation", "council", "department", "directorate", "division",
|
|
928
|
+
"foundation", "gmbh", "group", "inc", "institute", "labs", "limited",
|
|
929
|
+
"llc", "ltd", "networks", "team", "technologies", "university", "union",
|
|
930
|
+
})
|
|
871
931
|
|
|
872
932
|
|
|
873
933
|
def _is_valid_org_name(value: str) -> bool:
|
|
874
934
|
v = value.strip()
|
|
935
|
+
v_lower = v.lower()
|
|
936
|
+
if v_lower in ENTITY_BLOCKLIST:
|
|
937
|
+
return False
|
|
875
938
|
if len(v) < 3 or len(v) > 60:
|
|
876
939
|
return False
|
|
877
|
-
if
|
|
940
|
+
if re.search(r"[:,/()]", v):
|
|
941
|
+
return False
|
|
942
|
+
if re.search(r"\.[a-z0-9]{2,5}\b", v_lower):
|
|
943
|
+
return False
|
|
944
|
+
if re.search(r"\d{1,2}/\d{1,2}/\d{2,4}", v):
|
|
945
|
+
return False
|
|
946
|
+
if _HTML_ENTITY_RE.search(v) or _DECODED_HTML_UI_RE.search(v):
|
|
947
|
+
return False
|
|
948
|
+
if re.search(r"\b[a-z]+['\u2019]\w*\b", v_lower):
|
|
949
|
+
return False
|
|
950
|
+
tokens = re.findall(r"[a-z0-9]+(?:[-.][a-z0-9]+)*", v_lower)
|
|
951
|
+
if not tokens:
|
|
952
|
+
return False
|
|
953
|
+
if any(token.isdigit() for token in tokens):
|
|
954
|
+
return False
|
|
955
|
+
if any(token == "cve" or token.startswith("cve-") for token in tokens):
|
|
956
|
+
return False
|
|
957
|
+
if len(tokens) > 4:
|
|
958
|
+
return False
|
|
959
|
+
if "-" in v and not any(token in _ORG_SUFFIX_HINTS for token in tokens):
|
|
960
|
+
return False
|
|
961
|
+
if any(token in _ORG_SENTENCE_WORDS for token in tokens):
|
|
962
|
+
return False
|
|
963
|
+
word_tokens = [token for token in tokens if re.search(r"[a-z]", token)]
|
|
964
|
+
if len(word_tokens) > 1:
|
|
965
|
+
generic_words = [
|
|
966
|
+
token for token in word_tokens
|
|
967
|
+
if token in ENTITY_BLOCKLIST or token in _ORG_ALLOWED_CONNECTORS
|
|
968
|
+
]
|
|
969
|
+
if len(generic_words) >= len(word_tokens):
|
|
970
|
+
return False
|
|
971
|
+
if re.fullmatch(r"[A-Z_]+:?\d*", v) and v_lower not in COMMON_THREAT_TERMS:
|
|
878
972
|
return False
|
|
879
973
|
if _ORG_NOISE_RE.search(v):
|
|
880
974
|
return False
|
|
@@ -19,6 +19,7 @@ ExtractionResult is a dataclass exported through extractor/__init__.py.
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
21
|
import asyncio
|
|
22
|
+
import html
|
|
22
23
|
import logging
|
|
23
24
|
from dataclasses import dataclass, field
|
|
24
25
|
from typing import Any, Optional
|
|
@@ -175,6 +176,13 @@ def _type_priority(entity_type: str) -> int:
|
|
|
175
176
|
return priority
|
|
176
177
|
return 99
|
|
177
178
|
|
|
179
|
+
|
|
180
|
+
def _clean_text_for_extraction(text: str) -> str:
|
|
181
|
+
"""Decode HTML entities before regex/NER/LLM extraction sees page text."""
|
|
182
|
+
if not text:
|
|
183
|
+
return ""
|
|
184
|
+
return html.unescape(text)
|
|
185
|
+
|
|
178
186
|
# ---------------------------------------------------------------------------
|
|
179
187
|
# Result dataclass
|
|
180
188
|
# ---------------------------------------------------------------------------
|
|
@@ -231,6 +239,7 @@ async def extract_entities_from_page(
|
|
|
231
239
|
by regex, or below the top-N priority cut).
|
|
232
240
|
"""
|
|
233
241
|
errors: list[str] = []
|
|
242
|
+
page_text = _clean_text_for_extraction(page_text or "")
|
|
234
243
|
|
|
235
244
|
# -----------------------------------------------------------------------
|
|
236
245
|
# Stage 1 — Regex
|