voidaccess 2.0.2__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.2 → voidaccess-2.0.3}/PKG-INFO +1 -1
- {voidaccess-2.0.2 → voidaccess-2.0.3}/api/main.py +1 -1
- {voidaccess-2.0.2 → voidaccess-2.0.3}/export/ioc_package.py +1 -1
- {voidaccess-2.0.2 → voidaccess-2.0.3}/pyproject.toml +1 -1
- {voidaccess-2.0.2 → 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.2 → voidaccess-2.0.3}/voidaccess.egg-info/PKG-INFO +1 -1
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess.egg-info/SOURCES.txt +2 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/__init__.py +1 -1
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/config.py +15 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/LICENSE +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/README.md +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/alembic.ini +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/analysis/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/analysis/opsec.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/analysis/patterns.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/analysis/temporal.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/api/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/api/auth.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/api/errors.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/actors.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/admin.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/auth.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/entities.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/export.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/investigations.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/monitors.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/search.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/api/routes/settings.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/auth/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/auth/token_blacklist.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/config.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/crawler/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/crawler/dedup.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/crawler/frontier.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/crawler/spider.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/crawler/utils.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/data/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/data/common_words_en.txt +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/data/domain_stopwords_en.txt +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/data/monitors.yaml +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/data/seed_data.json.gz +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/data/threat_gazetteer.json +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/env.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0001_initial_schema.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0002_add_investigation_status_column.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0002_add_missing_tables.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0003_add_canonical_value_and_entity_links.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0004_add_page_posted_at.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0005_add_extraction_method.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0006_add_monitor_alerts.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0007_add_actor_style_profiles.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0008_add_users_table.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0009_add_investigation_id_to_relationships.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0010_add_composite_index_entity_relationships.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0011_add_page_extraction_cache.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0013_add_graph_status.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0015_add_progress_fields.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0016_backfill_graph_status.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0017_add_user_api_keys.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0018_add_user_id_to_investigations.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0019_add_content_safety_log.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0020_add_entity_source_tracking.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0021_add_search_engine_stats.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0022_add_actor_profiles.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0023_add_investigation_metadata.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0024_add_investigation_step_metrics.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0025_add_cancellation_requested.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0026_add_entity_investigation_count.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/models.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/queries.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/search_engine_stats.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/db/session.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/export/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/export/_entity_loading.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/export/misp.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/export/relationships.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/export/sigma.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/export/snort_export.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/export/stix.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/export/yara_export.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/confidence.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/dependency_relationship.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/entity_shape.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/gazetteer.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/identity.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/llm_extract.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/ner.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/normalizer.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/pipeline.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/regex_patterns.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/relationship_extract.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/extractor/software_suppression.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/fingerprint/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/fingerprint/calibration.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/fingerprint/profiler.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/fingerprint/stylometry.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/graph/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/graph/builder.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/graph/export.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/graph/model.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/graph/queries.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/graph/visualize.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/i18n/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/i18n/detect.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/i18n/query_expand.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/i18n/translate.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/monitor/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/monitor/_db.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/monitor/alerts.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/monitor/config.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/monitor/diff.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/monitor/jobs.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/monitor/scheduler.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/pacing/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/scraper/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/scraper/scrape.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/scraper/scrape_js.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/scraper/tor_pool.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/search/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/search/circuit_breaker.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/search/config.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/search/engine_catalog.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/search/query_builder.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/search/search.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/search/tor_isolation.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/setup.cfg +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/actor_profiles.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/blockchain.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/breach_lookup.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/cache.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/cisa.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/data/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/data/onion_seeds.json +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/dns_enrichment.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/domain_reputation.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/email_reputation.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/engines.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/enrichment.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/github_scraper.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/gitlab_scraper.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/hash_reputation.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/historical_intel.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/infostealer.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/ip_reputation.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/nvd.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/paste_scraper.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/pastes.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/proxy_client.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/rss_scraper.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/seed_manager.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/seeds.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/shodan.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/telegram.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/sources/virustotal.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_actor_extraction_gap.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_analysis_opsec.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_analysis_stylometry.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_analysis_temporal.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_api.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_api_monitors.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_blockchain.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_breach_lookup.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_cli_manifest.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_community_detection_parity.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_confidence_calibration.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_config.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_consolidated_contracts.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_crawler.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_db.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_dependency_relationship.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_dns_enrichment.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_domain_reputation.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_email_reputation.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_entity_priority.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_extractor_llm_pipeline.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_filter_e2e_manual.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_filter_parser_manual.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_fingerprint.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_gazetteer.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_github_scraper.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_gitlab_scraper.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_graph.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_graph_integration.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_hash_reputation.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_heuristic_expansion.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_i18n.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_infostealer.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_ioc_package_export.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_ip_reputation.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_llm.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_llm_utils.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_model_singleton.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_monitor.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_monitor_integration.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_nvd.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_pacing.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_pacing_enrichment.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_pacing_quotas.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_pagination.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_paste_scraper.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_priority_score_cli_api_parity.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_proxy_client.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_ransomlook.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_regex_patterns.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_relationship_extract.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_requirement_bugs.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_rss_scraper.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_scrape_isolation.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_scrape_js.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_settings.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_sigma_export.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_snort_export.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_software_suppression.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_sources.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_sources_enrichment_new.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_stix_export.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_vector.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/tests/test_yara_export.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/async_utils.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/content_dedup.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/content_safety.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/defang.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/encryption.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/enrichment_cache.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/entity_priority.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/investigation_metrics.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/ioc_freshness.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/utils/user_keys.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/vector/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/vector/embedder.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/vector/model_singleton.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/vector/search.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/vector/store.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess/config.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess/llm.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess/llm_utils.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess.egg-info/dependency_links.txt +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess.egg-info/entry_points.txt +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess.egg-info/requires.txt +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess.egg-info/top_level.txt +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/adapters/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/adapters/sqlite.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/browser.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/commands/__init__.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/commands/actors.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/commands/configure.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/commands/enrich.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/commands/export.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/commands/investigate.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/commands/show.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/display.py +0 -0
- {voidaccess-2.0.2 → voidaccess-2.0.3}/voidaccess_cli/main.py +0 -0
- {voidaccess-2.0.2 → 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,
|
|
@@ -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.
|
|
@@ -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
|
|
@@ -20,6 +20,7 @@ _WARNED_CONFIG_KEYS: set[str] = set()
|
|
|
20
20
|
|
|
21
21
|
CLI_HOME = Path(os.path.expanduser("~/.voidaccess"))
|
|
22
22
|
CONFIG_PATH = CLI_HOME / "config.json"
|
|
23
|
+
ENV_PATH = Path.cwd() / ".env"
|
|
23
24
|
DB_PATH = CLI_HOME / "investigations.db"
|
|
24
25
|
DEFAULT_OUTPUT_DIR = CLI_HOME / "results"
|
|
25
26
|
|
|
@@ -248,6 +249,19 @@ def _lock_file(path: Path) -> None:
|
|
|
248
249
|
pass
|
|
249
250
|
|
|
250
251
|
|
|
252
|
+
def lock_env_file() -> None:
|
|
253
|
+
"""Apply the same owner-only protection to the project ``.env`` file.
|
|
254
|
+
|
|
255
|
+
``voidaccess configure`` stores its CLI settings separately in
|
|
256
|
+
``~/.voidaccess/config.json``, but a self-hosted checkout may also have a
|
|
257
|
+
project-level ``.env`` containing the API/database credentials consumed by
|
|
258
|
+
the runtime. Lock it whenever configuration is saved, while keeping the
|
|
259
|
+
operation a no-op for CLI-only installs that have no ``.env``.
|
|
260
|
+
"""
|
|
261
|
+
if ENV_PATH.is_file():
|
|
262
|
+
_lock_file(ENV_PATH)
|
|
263
|
+
|
|
264
|
+
|
|
251
265
|
def save_config(config: dict[str, Any]) -> None:
|
|
252
266
|
features = config.setdefault("features", {})
|
|
253
267
|
# Keep the legacy aliases readable for older clients. When a caller
|
|
@@ -271,6 +285,7 @@ def save_config(config: dict[str, Any]) -> None:
|
|
|
271
285
|
encoding="utf-8",
|
|
272
286
|
)
|
|
273
287
|
_lock_file(CONFIG_PATH)
|
|
288
|
+
lock_env_file()
|
|
274
289
|
|
|
275
290
|
|
|
276
291
|
def is_configured() -> bool:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0002_add_investigation_status_column.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0007_add_actor_style_profiles.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{voidaccess-2.0.2 → voidaccess-2.0.3}/db/migrations/versions/0011_add_page_extraction_cache.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|