voidaccess 2.0.1__tar.gz → 2.0.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-2.0.1 → voidaccess-2.0.3}/PKG-INFO +1 -1
- {voidaccess-2.0.1 → voidaccess-2.0.3}/api/main.py +1 -1
- {voidaccess-2.0.1 → voidaccess-2.0.3}/export/__init__.py +2 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/export/ioc_package.py +3 -47
- {voidaccess-2.0.1 → voidaccess-2.0.3}/export/sigma.py +58 -1
- {voidaccess-2.0.1 → voidaccess-2.0.3}/pyproject.toml +2 -2
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_cli_proxy_config.py +33 -0
- voidaccess-2.0.3/tests/test_legacy_backfill.py +124 -0
- voidaccess-2.0.3/voidaccess/backfill.py +251 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess.egg-info/PKG-INFO +1 -1
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess.egg-info/SOURCES.txt +2 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/__init__.py +1 -1
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/commands/export.py +53 -17
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/config.py +15 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/LICENSE +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/README.md +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/alembic.ini +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/analysis/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/analysis/opsec.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/analysis/patterns.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/analysis/temporal.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/api/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/api/auth.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/api/errors.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/actors.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/admin.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/auth.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/entities.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/export.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/investigations.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/monitors.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/search.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/api/routes/settings.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/auth/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/auth/token_blacklist.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/config.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/crawler/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/crawler/dedup.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/crawler/frontier.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/crawler/spider.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/crawler/utils.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/data/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/data/common_words_en.txt +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/data/domain_stopwords_en.txt +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/data/monitors.yaml +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/data/seed_data.json.gz +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/data/threat_gazetteer.json +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/env.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0001_initial_schema.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0002_add_investigation_status_column.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0002_add_missing_tables.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0003_add_canonical_value_and_entity_links.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0004_add_page_posted_at.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0005_add_extraction_method.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0006_add_monitor_alerts.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0007_add_actor_style_profiles.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0008_add_users_table.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0009_add_investigation_id_to_relationships.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0010_add_composite_index_entity_relationships.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0011_add_page_extraction_cache.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0013_add_graph_status.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0015_add_progress_fields.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0016_backfill_graph_status.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0017_add_user_api_keys.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0018_add_user_id_to_investigations.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0019_add_content_safety_log.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0020_add_entity_source_tracking.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0021_add_search_engine_stats.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0022_add_actor_profiles.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0023_add_investigation_metadata.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0024_add_investigation_step_metrics.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0025_add_cancellation_requested.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/migrations/versions/0026_add_entity_investigation_count.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/models.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/queries.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/search_engine_stats.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/db/session.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/export/_entity_loading.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/export/misp.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/export/relationships.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/export/snort_export.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/export/stix.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/export/yara_export.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/confidence.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/dependency_relationship.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/entity_shape.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/gazetteer.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/identity.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/llm_extract.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/ner.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/normalizer.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/pipeline.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/regex_patterns.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/relationship_extract.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/extractor/software_suppression.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/fingerprint/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/fingerprint/calibration.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/fingerprint/profiler.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/fingerprint/stylometry.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/graph/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/graph/builder.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/graph/export.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/graph/model.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/graph/queries.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/graph/visualize.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/i18n/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/i18n/detect.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/i18n/query_expand.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/i18n/translate.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/monitor/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/monitor/_db.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/monitor/alerts.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/monitor/config.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/monitor/diff.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/monitor/jobs.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/monitor/scheduler.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/pacing/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/scraper/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/scraper/scrape.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/scraper/scrape_js.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/scraper/tor_pool.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/search/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/search/circuit_breaker.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/search/config.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/search/engine_catalog.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/search/query_builder.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/search/search.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/search/tor_isolation.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/setup.cfg +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/actor_profiles.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/blockchain.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/breach_lookup.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/cache.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/cisa.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/data/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/data/onion_seeds.json +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/dns_enrichment.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/domain_reputation.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/email_reputation.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/engines.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/enrichment.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/github_scraper.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/gitlab_scraper.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/hash_reputation.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/historical_intel.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/infostealer.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/ip_reputation.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/nvd.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/paste_scraper.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/pastes.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/proxy_client.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/rss_scraper.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/seed_manager.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/seeds.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/shodan.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/telegram.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/sources/virustotal.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_actor_extraction_gap.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_analysis_opsec.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_analysis_stylometry.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_analysis_temporal.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_api.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_api_monitors.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_blockchain.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_breach_lookup.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_cli_manifest.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_community_detection_parity.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_confidence_calibration.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_config.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_consolidated_contracts.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_crawler.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_db.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_dependency_relationship.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_dns_enrichment.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_domain_reputation.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_email_reputation.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_entity_priority.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_extractor_llm_pipeline.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_filter_e2e_manual.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_filter_parser_manual.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_fingerprint.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_gazetteer.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_github_scraper.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_gitlab_scraper.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_graph.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_graph_integration.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_hash_reputation.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_heuristic_expansion.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_i18n.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_infostealer.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_ioc_package_export.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_ip_reputation.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_llm.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_llm_utils.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_model_singleton.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_monitor.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_monitor_integration.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_nvd.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_pacing.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_pacing_enrichment.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_pacing_quotas.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_pagination.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_paste_scraper.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_priority_score_cli_api_parity.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_proxy_client.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_ransomlook.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_regex_patterns.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_relationship_extract.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_requirement_bugs.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_rss_scraper.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_scrape_isolation.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_scrape_js.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_settings.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_sigma_export.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_snort_export.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_software_suppression.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_sources.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_sources_enrichment_new.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_stix_export.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_vector.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/tests/test_yara_export.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/async_utils.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/content_dedup.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/content_safety.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/defang.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/encryption.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/enrichment_cache.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/entity_priority.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/investigation_metrics.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/ioc_freshness.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/utils/user_keys.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/vector/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/vector/embedder.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/vector/model_singleton.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/vector/search.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/vector/store.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess/config.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess/llm.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess/llm_utils.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess.egg-info/dependency_links.txt +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess.egg-info/entry_points.txt +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess.egg-info/requires.txt +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess.egg-info/top_level.txt +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/adapters/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/adapters/sqlite.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/browser.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/commands/__init__.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/commands/actors.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/commands/configure.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/commands/enrich.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/commands/investigate.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/commands/show.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/display.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/main.py +0 -0
- {voidaccess-2.0.1 → voidaccess-2.0.3}/voidaccess_cli/tor_detect.py +0 -0
|
@@ -332,7 +332,7 @@ async def _periodic_stuck_sweep() -> None:
|
|
|
332
332
|
app = FastAPI(
|
|
333
333
|
title="VoidAccess Intelligence API",
|
|
334
334
|
description="VoidAccess: Dark Web Intelligence Platform",
|
|
335
|
-
version="2.0.
|
|
335
|
+
version="2.0.3",
|
|
336
336
|
docs_url="/docs",
|
|
337
337
|
redoc_url="/redoc",
|
|
338
338
|
lifespan=lifespan,
|
|
@@ -17,6 +17,7 @@ from export.misp import (
|
|
|
17
17
|
from export.sigma import (
|
|
18
18
|
entities_to_sigma_rules,
|
|
19
19
|
export_sigma_rules,
|
|
20
|
+
render_sigma_rules,
|
|
20
21
|
sigma_rule_to_yaml,
|
|
21
22
|
)
|
|
22
23
|
from export.yara_export import (
|
|
@@ -44,6 +45,7 @@ __all__ = [
|
|
|
44
45
|
# sigma
|
|
45
46
|
"entities_to_sigma_rules",
|
|
46
47
|
"sigma_rule_to_yaml",
|
|
48
|
+
"render_sigma_rules",
|
|
47
49
|
"export_sigma_rules",
|
|
48
50
|
# yara
|
|
49
51
|
"generate_yara_rules",
|
|
@@ -57,7 +57,7 @@ logger = logging.getLogger(__name__)
|
|
|
57
57
|
# ---------------------------------------------------------------------------
|
|
58
58
|
|
|
59
59
|
PACKAGE_FORMAT = "voidaccess-ioc-v1"
|
|
60
|
-
PACKAGE_VERSION = "2.0.
|
|
60
|
+
PACKAGE_VERSION = "2.0.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.
|
|
@@ -702,57 +702,13 @@ def _safe_misp_json(entities: list[Any], investigation_id: Any) -> str:
|
|
|
702
702
|
|
|
703
703
|
def _safe_sigma_yml(entities: list[Any]) -> str:
|
|
704
704
|
try:
|
|
705
|
-
from export.sigma import
|
|
706
|
-
|
|
707
|
-
# caller passes plain dicts, wrap them in a tiny adapter so Sigma can
|
|
708
|
-
# read .entity_type, .value, .confidence, and .source_url.
|
|
709
|
-
adapted: list[Any] = []
|
|
710
|
-
for e in entities or []:
|
|
711
|
-
if isinstance(e, dict):
|
|
712
|
-
adapted.append(_DictEntityAdapter(e))
|
|
713
|
-
else:
|
|
714
|
-
adapted.append(e)
|
|
715
|
-
rules = entities_to_sigma_rules(adapted)
|
|
716
|
-
return "\n---\n".join(sigma_rule_to_yaml(r) for r in rules if r)
|
|
705
|
+
from export.sigma import render_sigma_rules
|
|
706
|
+
return render_sigma_rules(entities or [])
|
|
717
707
|
except Exception as exc:
|
|
718
708
|
logger.warning("Sigma generation for IOC package failed: %s", exc)
|
|
719
709
|
return ""
|
|
720
710
|
|
|
721
711
|
|
|
722
|
-
class _DictEntityAdapter:
|
|
723
|
-
"""Adapt a plain dict into an object with .entity_type / .value / etc.
|
|
724
|
-
|
|
725
|
-
Used to feed dict-shaped entities into Sigma / STIX / MISP generators
|
|
726
|
-
that expect object access. Keeps the IOC package module standalone
|
|
727
|
-
without changing the existing Sigma / STIX / MISP module signatures.
|
|
728
|
-
"""
|
|
729
|
-
|
|
730
|
-
__slots__ = ("_d",)
|
|
731
|
-
|
|
732
|
-
def __init__(self, d: dict[str, Any]) -> None:
|
|
733
|
-
self._d = d
|
|
734
|
-
|
|
735
|
-
@property
|
|
736
|
-
def entity_type(self) -> str:
|
|
737
|
-
return _entity_type(self._d)
|
|
738
|
-
|
|
739
|
-
@property
|
|
740
|
-
def value(self) -> str:
|
|
741
|
-
return _entity_value(self._d)
|
|
742
|
-
|
|
743
|
-
@property
|
|
744
|
-
def canonical_value(self) -> str:
|
|
745
|
-
return entity_canonical_id(self._d)
|
|
746
|
-
|
|
747
|
-
@property
|
|
748
|
-
def confidence(self) -> float:
|
|
749
|
-
return _entity_confidence(self._d)
|
|
750
|
-
|
|
751
|
-
@property
|
|
752
|
-
def source_url(self) -> str:
|
|
753
|
-
return _entity_source_url(self._d)
|
|
754
|
-
|
|
755
|
-
|
|
756
712
|
# ---------------------------------------------------------------------------
|
|
757
713
|
# README + metadata builders
|
|
758
714
|
# ---------------------------------------------------------------------------
|
|
@@ -207,7 +207,8 @@ def entities_to_sigma_rules(
|
|
|
207
207
|
Falls back to base rule if LLM fails.
|
|
208
208
|
"""
|
|
209
209
|
rules: list[dict] = []
|
|
210
|
-
for
|
|
210
|
+
for raw_entity in entities:
|
|
211
|
+
entity = _adapt_entity(raw_entity)
|
|
211
212
|
if entity.entity_type not in _SIGMA_ENTITY_TYPES:
|
|
212
213
|
continue
|
|
213
214
|
base = _build_base_rule(entity)
|
|
@@ -228,6 +229,23 @@ def sigma_rule_to_yaml(rule: dict) -> str:
|
|
|
228
229
|
return ""
|
|
229
230
|
|
|
230
231
|
|
|
232
|
+
def render_sigma_rules(
|
|
233
|
+
entities: list[Any],
|
|
234
|
+
llm: Optional[Any] = None,
|
|
235
|
+
) -> str:
|
|
236
|
+
"""Render the shared Sigma artifact used by every export surface.
|
|
237
|
+
|
|
238
|
+
The CLI export and the IOC package both receive the same export payload,
|
|
239
|
+
whose entities are normally dictionaries. Keeping adaptation, rule
|
|
240
|
+
generation, and YAML assembly here prevents one surface from growing a
|
|
241
|
+
second DB-loading implementation with different failure semantics.
|
|
242
|
+
"""
|
|
243
|
+
rules = entities_to_sigma_rules(list(entities or []), llm=llm)
|
|
244
|
+
return "\n---\n".join(
|
|
245
|
+
sigma_rule_to_yaml(rule) for rule in rules if rule
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
|
|
231
249
|
def export_sigma_rules(
|
|
232
250
|
investigation_id: Any,
|
|
233
251
|
output_dir: str,
|
|
@@ -320,6 +338,45 @@ def _load_entities_for_investigation(investigation_id: Any) -> list[Any]:
|
|
|
320
338
|
return []
|
|
321
339
|
|
|
322
340
|
|
|
341
|
+
class _DictEntityAdapter:
|
|
342
|
+
"""Expose dictionary-shaped export entities through Sigma's object API."""
|
|
343
|
+
|
|
344
|
+
__slots__ = ("_data",)
|
|
345
|
+
|
|
346
|
+
def __init__(self, data: dict[str, Any]) -> None:
|
|
347
|
+
self._data = data
|
|
348
|
+
|
|
349
|
+
@property
|
|
350
|
+
def entity_type(self) -> str:
|
|
351
|
+
return str(self._data.get("entity_type") or "")
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def value(self) -> str:
|
|
355
|
+
return str(
|
|
356
|
+
self._data.get("canonical_value")
|
|
357
|
+
or self._data.get("value")
|
|
358
|
+
or ""
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def confidence(self) -> float:
|
|
363
|
+
try:
|
|
364
|
+
return float(self._data.get("confidence", 0.0) or 0.0)
|
|
365
|
+
except (TypeError, ValueError):
|
|
366
|
+
return 0.0
|
|
367
|
+
|
|
368
|
+
@property
|
|
369
|
+
def source_url(self) -> str:
|
|
370
|
+
return str(self._data.get("source_url") or "")
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _adapt_entity(entity: Any) -> Any:
|
|
374
|
+
"""Accept both ORM/normalized entities and export dictionaries."""
|
|
375
|
+
if isinstance(entity, dict):
|
|
376
|
+
return _DictEntityAdapter(entity)
|
|
377
|
+
return entity
|
|
378
|
+
|
|
379
|
+
|
|
323
380
|
def _coerce_uuid(value: Any):
|
|
324
381
|
"""Coerce value to uuid.UUID. Returns None on failure."""
|
|
325
382
|
import uuid as _uuid
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "voidaccess"
|
|
7
|
-
version = "2.0.
|
|
7
|
+
version = "2.0.3"
|
|
8
8
|
description = "Dark web OSINT CLI — automated threat intelligence from query to report"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -154,7 +154,7 @@ exclude = [
|
|
|
154
154
|
"sources" = ["data/onion_seeds.json"]
|
|
155
155
|
"sources.data" = ["onion_seeds.json"]
|
|
156
156
|
|
|
157
|
-
# v2.0.
|
|
157
|
+
# v2.0.2 — ship the top-level reference data bundle inside the installed
|
|
158
158
|
# package. The runtime resolves each of these via
|
|
159
159
|
# ``os.path.dirname(os.path.dirname(__file__))/data`` (from extractor/ and
|
|
160
160
|
# monitor/), i.e. ``site-packages/data/<file>`` after a wheel install. The
|
|
@@ -46,6 +46,8 @@ No real network. All file I/O uses tmp_path.
|
|
|
46
46
|
from __future__ import annotations
|
|
47
47
|
|
|
48
48
|
import os
|
|
49
|
+
import stat
|
|
50
|
+
import subprocess
|
|
49
51
|
import sys
|
|
50
52
|
import types
|
|
51
53
|
|
|
@@ -64,11 +66,42 @@ def isolated_config_home(tmp_path, monkeypatch):
|
|
|
64
66
|
|
|
65
67
|
monkeypatch.setattr(cli_config, "CLI_HOME", tmp_path)
|
|
66
68
|
monkeypatch.setattr(cli_config, "CONFIG_PATH", tmp_path / "config.json")
|
|
69
|
+
monkeypatch.setattr(cli_config, "ENV_PATH", tmp_path / ".env")
|
|
67
70
|
monkeypatch.setattr(cli_config, "DB_PATH", tmp_path / "investigations.db")
|
|
68
71
|
monkeypatch.setattr(cli_config, "DEFAULT_OUTPUT_DIR", tmp_path / "results")
|
|
69
72
|
return cli_config
|
|
70
73
|
|
|
71
74
|
|
|
75
|
+
def test_fresh_configure_run_locks_env_file(isolated_config_home):
|
|
76
|
+
"""A configure write applies the same owner-only protection to .env."""
|
|
77
|
+
isolated_config_home.ENV_PATH.write_text(
|
|
78
|
+
"JWT_SECRET=test-only\n",
|
|
79
|
+
encoding="utf-8",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
from typer.testing import CliRunner
|
|
83
|
+
from voidaccess_cli.commands.configure import app as configure_app
|
|
84
|
+
|
|
85
|
+
result = CliRunner().invoke(
|
|
86
|
+
configure_app,
|
|
87
|
+
["tor", "--host", "127.0.0.1", "--port", "9050"],
|
|
88
|
+
)
|
|
89
|
+
assert result.exit_code == 0, result.output
|
|
90
|
+
|
|
91
|
+
if os.name != "nt":
|
|
92
|
+
assert stat.S_IMODE(isolated_config_home.ENV_PATH.stat().st_mode) == 0o600
|
|
93
|
+
else:
|
|
94
|
+
acl = subprocess.run(
|
|
95
|
+
["icacls", str(isolated_config_home.ENV_PATH)],
|
|
96
|
+
check=True,
|
|
97
|
+
capture_output=True,
|
|
98
|
+
text=True,
|
|
99
|
+
).stdout
|
|
100
|
+
assert "(I)" not in acl
|
|
101
|
+
assert "BUILTIN\\Users" not in acl
|
|
102
|
+
assert "Authenticated Users" not in acl
|
|
103
|
+
|
|
104
|
+
|
|
72
105
|
def _scrub_env(monkeypatch):
|
|
73
106
|
"""Remove every proxy-related env var so each test starts from a known state."""
|
|
74
107
|
for k in (
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Tests for the one-time v2.0.3 legacy-data repair."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import datetime
|
|
6
|
+
import uuid
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_backfill_resolves_typed_page_and_persists_communities(db_engine):
|
|
10
|
+
from db.models import Entity, EntityRelationship, Investigation, Page
|
|
11
|
+
from db.session import get_session
|
|
12
|
+
from voidaccess.backfill import run_backfill
|
|
13
|
+
|
|
14
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
15
|
+
investigation_id = uuid.uuid4()
|
|
16
|
+
page_id = uuid.uuid4()
|
|
17
|
+
source_id, target_id = uuid.uuid4(), uuid.uuid4()
|
|
18
|
+
|
|
19
|
+
with get_session() as session:
|
|
20
|
+
session.add(
|
|
21
|
+
Investigation(
|
|
22
|
+
id=investigation_id,
|
|
23
|
+
query="legacy backfill test",
|
|
24
|
+
status="completed",
|
|
25
|
+
graph_status="built",
|
|
26
|
+
created_at=now,
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
session.add(
|
|
30
|
+
Page(
|
|
31
|
+
id=page_id,
|
|
32
|
+
url="https://example.test/legacy",
|
|
33
|
+
cleaned_text="Actor uses malware",
|
|
34
|
+
scrape_timestamp=now,
|
|
35
|
+
created_at=now,
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
session.add_all(
|
|
39
|
+
[
|
|
40
|
+
Entity(
|
|
41
|
+
id=source_id,
|
|
42
|
+
page_id=page_id,
|
|
43
|
+
investigation_id=investigation_id,
|
|
44
|
+
entity_type="THREAT_ACTOR_HANDLE",
|
|
45
|
+
value="actor-one",
|
|
46
|
+
canonical_value="actor-one",
|
|
47
|
+
confidence=0.95,
|
|
48
|
+
first_seen=now,
|
|
49
|
+
last_seen=now,
|
|
50
|
+
created_at=now,
|
|
51
|
+
),
|
|
52
|
+
Entity(
|
|
53
|
+
id=target_id,
|
|
54
|
+
page_id=page_id,
|
|
55
|
+
investigation_id=investigation_id,
|
|
56
|
+
entity_type="MALWARE_FAMILY",
|
|
57
|
+
value="malware-one",
|
|
58
|
+
canonical_value="malware-one",
|
|
59
|
+
confidence=0.95,
|
|
60
|
+
first_seen=now,
|
|
61
|
+
last_seen=now,
|
|
62
|
+
created_at=now,
|
|
63
|
+
),
|
|
64
|
+
]
|
|
65
|
+
)
|
|
66
|
+
session.add(
|
|
67
|
+
EntityRelationship(
|
|
68
|
+
entity_a_id=source_id,
|
|
69
|
+
entity_b_id=target_id,
|
|
70
|
+
relationship_type="USES",
|
|
71
|
+
confidence=0.9,
|
|
72
|
+
investigation_id=investigation_id,
|
|
73
|
+
source_page_id=None,
|
|
74
|
+
first_seen=now,
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
session.commit()
|
|
78
|
+
|
|
79
|
+
result = run_backfill(session, investigation_id)
|
|
80
|
+
session.commit()
|
|
81
|
+
|
|
82
|
+
relationship = session.query(EntityRelationship).one()
|
|
83
|
+
investigation = session.get(Investigation, investigation_id)
|
|
84
|
+
|
|
85
|
+
assert result["typed_relationship_pages"]["resolved"] == 1
|
|
86
|
+
assert relationship.source_page_id == page_id
|
|
87
|
+
assert investigation.metadata_json["communities"]
|
|
88
|
+
assert investigation.metadata_json["community_count"] == 1
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_backfill_does_not_assign_invalid_typed_relationship_page(db_engine):
|
|
92
|
+
from db.models import Entity, EntityRelationship, Investigation, Page
|
|
93
|
+
from db.session import get_session
|
|
94
|
+
from voidaccess.backfill import backfill_typed_relationship_page_ids
|
|
95
|
+
|
|
96
|
+
now = datetime.datetime.now(datetime.timezone.utc)
|
|
97
|
+
investigation_id = uuid.uuid4()
|
|
98
|
+
page_id = uuid.uuid4()
|
|
99
|
+
source_id, target_id = uuid.uuid4(), uuid.uuid4()
|
|
100
|
+
|
|
101
|
+
with get_session() as session:
|
|
102
|
+
session.add(
|
|
103
|
+
Investigation(
|
|
104
|
+
id=investigation_id,
|
|
105
|
+
query="invalid legacy relationship",
|
|
106
|
+
status="completed",
|
|
107
|
+
created_at=now,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
session.add(Page(id=page_id, url="https://example.test/invalid", scrape_timestamp=now, created_at=now))
|
|
111
|
+
session.add_all(
|
|
112
|
+
[
|
|
113
|
+
Entity(id=source_id, page_id=page_id, investigation_id=investigation_id,
|
|
114
|
+
entity_type="ORGANIZATION_NAME", value="org", canonical_value="org"),
|
|
115
|
+
Entity(id=target_id, page_id=page_id, investigation_id=investigation_id,
|
|
116
|
+
entity_type="IP_ADDRESS", value="192.0.2.1", canonical_value="192.0.2.1"),
|
|
117
|
+
EntityRelationship(entity_a_id=source_id, entity_b_id=target_id,
|
|
118
|
+
relationship_type="TARGETS", investigation_id=investigation_id,
|
|
119
|
+
first_seen=now),
|
|
120
|
+
]
|
|
121
|
+
)
|
|
122
|
+
session.commit()
|
|
123
|
+
result = backfill_typed_relationship_page_ids(session, investigation_id)
|
|
124
|
+
assert result == {"scanned": 1, "resolved": 0, "unresolved": 1}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""One-time repairs for legacy investigation data.
|
|
2
|
+
|
|
3
|
+
This module deliberately does not change the schema. It repairs two pieces of
|
|
4
|
+
derived data that were introduced after some investigations had already been
|
|
5
|
+
stored: the page provenance on typed relationship edges and the persisted
|
|
6
|
+
community partition used by the HTTP graph endpoint.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import uuid
|
|
16
|
+
from collections import defaultdict
|
|
17
|
+
from typing import Any, Optional
|
|
18
|
+
|
|
19
|
+
from sqlalchemy.orm import joinedload
|
|
20
|
+
|
|
21
|
+
from extractor.relationship_extract import _LLM_REL_VOCAB, _is_compatible_relationship
|
|
22
|
+
from graph.builder import build_graph_from_db, detect_communities
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _as_uuid(value: Any) -> Optional[uuid.UUID]:
|
|
28
|
+
if isinstance(value, uuid.UUID):
|
|
29
|
+
return value
|
|
30
|
+
try:
|
|
31
|
+
return uuid.UUID(str(value)) if value else None
|
|
32
|
+
except (TypeError, ValueError, AttributeError):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _metadata_dict(value: Any) -> dict[str, Any]:
|
|
37
|
+
if isinstance(value, dict):
|
|
38
|
+
return dict(value)
|
|
39
|
+
if isinstance(value, str) and value:
|
|
40
|
+
try:
|
|
41
|
+
decoded = json.loads(value)
|
|
42
|
+
except (TypeError, ValueError):
|
|
43
|
+
return {}
|
|
44
|
+
return dict(decoded) if isinstance(decoded, dict) else {}
|
|
45
|
+
return {}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _validated_entities(session, investigation_id: uuid.UUID):
|
|
49
|
+
"""Return the current entity set, including cross-investigation links."""
|
|
50
|
+
from db.models import Entity, InvestigationEntityLink
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
session.query(Entity)
|
|
54
|
+
.outerjoin(
|
|
55
|
+
InvestigationEntityLink,
|
|
56
|
+
InvestigationEntityLink.entity_id == Entity.id,
|
|
57
|
+
)
|
|
58
|
+
.filter(
|
|
59
|
+
(Entity.investigation_id == investigation_id)
|
|
60
|
+
| (InvestigationEntityLink.investigation_id == investigation_id)
|
|
61
|
+
)
|
|
62
|
+
.options(joinedload(Entity.page))
|
|
63
|
+
.distinct()
|
|
64
|
+
.all()
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _resolve_source_page_id(
|
|
69
|
+
relationship,
|
|
70
|
+
entity_a,
|
|
71
|
+
entity_b,
|
|
72
|
+
validated_entities: list,
|
|
73
|
+
) -> Optional[uuid.UUID]:
|
|
74
|
+
"""Resolve provenance using the live pipeline's validated entity set.
|
|
75
|
+
|
|
76
|
+
Typed relationship direction is meaningful: the live pipeline persists the
|
|
77
|
+
page on which the claim was extracted. For a legacy row without that
|
|
78
|
+
field, the source endpoint's page is the strongest deterministic evidence;
|
|
79
|
+
the target page and a page containing both endpoints are conservative
|
|
80
|
+
fallbacks. No page outside the validated investigation set is accepted.
|
|
81
|
+
"""
|
|
82
|
+
if not _is_compatible_relationship(
|
|
83
|
+
str(relationship.relationship_type or ""),
|
|
84
|
+
{"type": entity_a.entity_type},
|
|
85
|
+
{"type": entity_b.entity_type},
|
|
86
|
+
):
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
valid_page_ids = {
|
|
90
|
+
ent.page_id for ent in validated_entities if ent.page_id is not None
|
|
91
|
+
}
|
|
92
|
+
if not valid_page_ids:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
page_entity_ids: dict[uuid.UUID, set[uuid.UUID]] = defaultdict(set)
|
|
96
|
+
for ent in validated_entities:
|
|
97
|
+
if ent.page_id is not None:
|
|
98
|
+
page_entity_ids[ent.page_id].add(ent.id)
|
|
99
|
+
|
|
100
|
+
# The source endpoint is the direct equivalent of the page_id attached by
|
|
101
|
+
# relationship_extract.extract_relationships_from_results().
|
|
102
|
+
candidates = [
|
|
103
|
+
entity_a.page_id,
|
|
104
|
+
entity_b.page_id,
|
|
105
|
+
]
|
|
106
|
+
for page_id, entity_ids in page_entity_ids.items():
|
|
107
|
+
if entity_a.id in entity_ids and entity_b.id in entity_ids:
|
|
108
|
+
candidates.append(page_id)
|
|
109
|
+
for page_id, entity_ids in page_entity_ids.items():
|
|
110
|
+
if entity_a.id in entity_ids:
|
|
111
|
+
candidates.append(page_id)
|
|
112
|
+
|
|
113
|
+
seen: set[uuid.UUID] = set()
|
|
114
|
+
for page_id in candidates:
|
|
115
|
+
if page_id is None or page_id in seen:
|
|
116
|
+
continue
|
|
117
|
+
seen.add(page_id)
|
|
118
|
+
if page_id in valid_page_ids:
|
|
119
|
+
return page_id
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def backfill_typed_relationship_page_ids(
|
|
124
|
+
session,
|
|
125
|
+
investigation_id: Optional[uuid.UUID] = None,
|
|
126
|
+
) -> dict[str, int]:
|
|
127
|
+
"""Populate missing ``source_page_id`` values on validated typed edges."""
|
|
128
|
+
from db.models import Entity, EntityRelationship
|
|
129
|
+
|
|
130
|
+
typed_types = set(_LLM_REL_VOCAB.values())
|
|
131
|
+
query = session.query(EntityRelationship).filter(
|
|
132
|
+
EntityRelationship.source_page_id.is_(None),
|
|
133
|
+
EntityRelationship.relationship_type.in_(typed_types),
|
|
134
|
+
)
|
|
135
|
+
if investigation_id is not None:
|
|
136
|
+
query = query.filter(EntityRelationship.investigation_id == investigation_id)
|
|
137
|
+
|
|
138
|
+
rows = query.options(
|
|
139
|
+
joinedload(EntityRelationship.entity_a).joinedload(Entity.page),
|
|
140
|
+
joinedload(EntityRelationship.entity_b).joinedload(Entity.page),
|
|
141
|
+
).all()
|
|
142
|
+
entities_by_investigation: dict[uuid.UUID, list] = {}
|
|
143
|
+
scanned = resolved = unresolved = 0
|
|
144
|
+
|
|
145
|
+
for relationship in rows:
|
|
146
|
+
scanned += 1
|
|
147
|
+
entity_a = relationship.entity_a
|
|
148
|
+
entity_b = relationship.entity_b
|
|
149
|
+
target_investigation = (
|
|
150
|
+
_as_uuid(relationship.investigation_id)
|
|
151
|
+
or _as_uuid(getattr(entity_a, "investigation_id", None))
|
|
152
|
+
or _as_uuid(getattr(entity_b, "investigation_id", None))
|
|
153
|
+
)
|
|
154
|
+
if target_investigation is None:
|
|
155
|
+
unresolved += 1
|
|
156
|
+
continue
|
|
157
|
+
if target_investigation not in entities_by_investigation:
|
|
158
|
+
entities_by_investigation[target_investigation] = _validated_entities(
|
|
159
|
+
session, target_investigation
|
|
160
|
+
)
|
|
161
|
+
page_id = _resolve_source_page_id(
|
|
162
|
+
relationship,
|
|
163
|
+
entity_a,
|
|
164
|
+
entity_b,
|
|
165
|
+
entities_by_investigation[target_investigation],
|
|
166
|
+
)
|
|
167
|
+
if page_id is None:
|
|
168
|
+
unresolved += 1
|
|
169
|
+
continue
|
|
170
|
+
relationship.source_page_id = page_id
|
|
171
|
+
resolved += 1
|
|
172
|
+
|
|
173
|
+
if resolved:
|
|
174
|
+
session.flush()
|
|
175
|
+
return {"scanned": scanned, "resolved": resolved, "unresolved": unresolved}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def backfill_community_partitions(
|
|
179
|
+
session,
|
|
180
|
+
investigation_id: Optional[uuid.UUID] = None,
|
|
181
|
+
) -> dict[str, int]:
|
|
182
|
+
"""Persist detector output for completed investigations lacking a partition."""
|
|
183
|
+
from db.models import Investigation
|
|
184
|
+
|
|
185
|
+
query = session.query(Investigation).filter(Investigation.status == "completed")
|
|
186
|
+
if investigation_id is not None:
|
|
187
|
+
query = query.filter(Investigation.id == investigation_id)
|
|
188
|
+
|
|
189
|
+
scanned = persisted = skipped = 0
|
|
190
|
+
for investigation in query.order_by(Investigation.created_at).all():
|
|
191
|
+
scanned += 1
|
|
192
|
+
metadata = _metadata_dict(investigation.metadata_json)
|
|
193
|
+
existing = metadata.get("communities")
|
|
194
|
+
if isinstance(existing, dict) and existing:
|
|
195
|
+
skipped += 1
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
graph = build_graph_from_db(investigation_id=investigation.id)
|
|
199
|
+
partition = {
|
|
200
|
+
str(node_id): int(community_id)
|
|
201
|
+
for node_id, community_id in detect_communities(graph).items()
|
|
202
|
+
}
|
|
203
|
+
metadata["communities"] = partition
|
|
204
|
+
metadata["community_count"] = len(set(partition.values())) if partition else 0
|
|
205
|
+
investigation.metadata_json = metadata
|
|
206
|
+
session.flush()
|
|
207
|
+
persisted += 1
|
|
208
|
+
|
|
209
|
+
return {"scanned": scanned, "persisted": persisted, "skipped": skipped}
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def run_backfill(
|
|
213
|
+
session,
|
|
214
|
+
investigation_id: Optional[uuid.UUID] = None,
|
|
215
|
+
) -> dict[str, dict[str, int]]:
|
|
216
|
+
"""Run both repairs in one transaction."""
|
|
217
|
+
typed = backfill_typed_relationship_page_ids(session, investigation_id)
|
|
218
|
+
communities = backfill_community_partitions(session, investigation_id)
|
|
219
|
+
return {"typed_relationship_pages": typed, "community_partitions": communities}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
223
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
224
|
+
parser.add_argument("--db-path", help="SQLite database path (defaults to VOIDACCESS_DB_PATH)")
|
|
225
|
+
parser.add_argument("--investigation-id", help="Limit the repair to one investigation")
|
|
226
|
+
parser.add_argument("--verbose", action="store_true")
|
|
227
|
+
args = parser.parse_args(argv)
|
|
228
|
+
|
|
229
|
+
if args.verbose:
|
|
230
|
+
logging.basicConfig(level=logging.INFO)
|
|
231
|
+
|
|
232
|
+
db_path = args.db_path or os.getenv("VOIDACCESS_DB_PATH")
|
|
233
|
+
if db_path:
|
|
234
|
+
db_path = os.path.abspath(os.path.expanduser(db_path))
|
|
235
|
+
os.environ["VOIDACCESS_DB_PATH"] = db_path
|
|
236
|
+
os.environ["DATABASE_URL"] = f"sqlite:///{db_path.replace(os.sep, '/') }"
|
|
237
|
+
|
|
238
|
+
target_id = _as_uuid(args.investigation_id)
|
|
239
|
+
if args.investigation_id and target_id is None:
|
|
240
|
+
parser.error("--investigation-id must be a UUID")
|
|
241
|
+
|
|
242
|
+
from db.session import get_session
|
|
243
|
+
|
|
244
|
+
with get_session() as session:
|
|
245
|
+
result = run_backfill(session, target_id)
|
|
246
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
247
|
+
return 0
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
raise SystemExit(main())
|
|
@@ -186,6 +186,7 @@ tests/test_i18n.py
|
|
|
186
186
|
tests/test_infostealer.py
|
|
187
187
|
tests/test_ioc_package_export.py
|
|
188
188
|
tests/test_ip_reputation.py
|
|
189
|
+
tests/test_legacy_backfill.py
|
|
189
190
|
tests/test_llm.py
|
|
190
191
|
tests/test_llm_utils.py
|
|
191
192
|
tests/test_model_singleton.py
|
|
@@ -232,6 +233,7 @@ vector/model_singleton.py
|
|
|
232
233
|
vector/search.py
|
|
233
234
|
vector/store.py
|
|
234
235
|
voidaccess/__init__.py
|
|
236
|
+
voidaccess/backfill.py
|
|
235
237
|
voidaccess/config.py
|
|
236
238
|
voidaccess/llm.py
|
|
237
239
|
voidaccess/llm_utils.py
|