forward-netbox 2.2.2__tar.gz → 2.2.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.
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/PKG-INFO +3 -2
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/README.md +2 -1
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/__init__.py +1 -1
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/jobs.py +30 -5
- forward_netbox-2.2.3/forward_netbox/migrations/0030_forwarddeviceanalysis_cve_ids.py +18 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/models.py +3 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_device_analysis.nqe +6 -5
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_vrfs.nqe +14 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardsync_health.html +3 -3
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/inc/device_analysis_panel.html +8 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/inc/execution_insights.html +2 -1
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_apply_identity_audit.py +32 -1
- forward_netbox-2.2.3/forward_netbox/tests/test_execution_insights_delete_label.py +42 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_jobs.py +34 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_models.py +4 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_scope_module_ui.py +6 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_vsys_parent.py +19 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/apply_identity_audit.py +80 -7
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/device_analysis.py +18 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/model_validation.py +6 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/query_binding_resolution.py +12 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/vsys_parent.py +32 -6
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/pyproject.toml +1 -1
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/LICENSE +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/api/__init__.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/api/serializers.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/api/urls.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/api/views.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/choices.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/exceptions.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/filtersets.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/forms.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/__init__.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/__init__.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_apic_cimc_readiness_audit.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_apply_identity_audit.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_blocker_audit.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_collection_gap_alert.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_device_scope_reconciliation_audit.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_link_vsys_parents.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_module_readiness.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_primary_ip_audit.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_pushdown_profile.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_query_diff_coverage_audit.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_scope_ipam_audit.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_seed_ui_harness.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_tag_delete_eligible.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/management/commands/forward_validation_org_query_audit.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0001_initial.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0002_add_coalesce_fields.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0003_forwardingestionissue_context_fields.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0004_forwardingestion_baseline_fields.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0005_validation_and_reporting.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0006_alter_forwardnqemap_netbox_model.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0007_forwardingestion_persisted_change_counters.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0008_alter_forwardnqemap_netbox_model.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0009_alter_forwardnqemap_netbox_model.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0010_forwardvalidationrun_override_applied_and_more.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0011_forwardingestion_change_request_id.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0012_forwardnqemap_query_reference.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0013_execution_run_step.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0014_execution_step_fetch_scope.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0015_execution_step_apply_engine.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0016_execution_step_query_metrics.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0017_execution_step_row_counters.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0018_execution_run_reconciliation_events.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0019_forwardexecutionstep_query_parameters.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0020_execution_step_operation.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0021_add_fhrpgroup_nqe_map_choice.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0022_alter_forwardnqemap_netbox_model.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0023_split_ip_address_maps.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0024_forwarddeviceanalysis.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0025_device_analysis_netboxmodel.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0026_forwarddeviceanalysis_collection_result.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0027_alter_forwardexecutionrun_backend.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0028_remove_forwardexecutionstep_run_and_more.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/0029_forward_parent_device_cf.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/migrations/__init__.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/navigation.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/__init__.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_apic_cimc_inventory.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_apic_nodes.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_app_profiles.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_bridge_domains.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_command_inventory.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_contracts.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_endpoint_groups.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_fabrics.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_filters.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_l3outs.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_nodes.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_pods.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_static_port_bindings.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_tenants.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_aci_vrfs.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_bgp_address_families.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_bgp_peer_address_families.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_bgp_peers.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_device_feature_tags.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_device_feature_tags_with_rules.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_device_models.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_device_models_with_netbox_aliases.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_device_types.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_device_vendors.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_devices.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_devices_with_netbox_aliases.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_hsrp_groups.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_inferred_interface_cables.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_interfaces.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_inventory_items.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_ip_addresses_ipv4.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_ip_addresses_ipv6.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_ip_addresses_unassignable_diagnostics.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_locations.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_mac_addresses.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_modules.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_ospf_areas.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_ospf_instances.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_ospf_interfaces.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_peering_sessions.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_platforms.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_prefixes_ipv4.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_prefixes_ipv6.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_routing_import_diagnostics.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_virtual_chassis.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_vlans.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/netbox_utilities.nqe +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/signals.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/static/forward_netbox/img/fn-logo.svg +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tables.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/template_content.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwarddriftpolicy.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardingestion.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardnqemap.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardsource.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardsync.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardsync_dependency_preview.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardsync_drift_report.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardsync_module_readiness.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardsync_scope_reconciliation.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/forwardvalidationrun.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/inc/brand_bar.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/inc/diff.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/inc/logs_pending.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/inc/merge_form.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/inc/validation_force_allow_form.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/change_explainability.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/ingestion_all.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/ingestion_merge_button.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/ingestion_progress.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/ingestion_statistics.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/ingestion_status.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/job_logs.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/object_tabs.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/stage_switcher_buttons.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templates/forward_netbox/partials/validation_force_allow_button.html +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templatetags/__init__.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/templatetags/forward_netbox_helpers.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/__init__.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/fixtures/aci_command_inventory_expected.json +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/fixtures/aci_discovery_expected.json +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/scenarios.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_api_usage.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_api_views.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_apic_cimc_readiness_audit_command.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_apply_engine.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_branching.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_brand_bar.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_bulk_adapter_parity.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_bulk_merge.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_bulk_merge_scale.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_device_scope_reconciliation_audit_command.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_device_scope_tagging.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_forms.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_forward_api.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_health.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_ingestion_merge.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_interface_naming.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_ip_address_dedup_query.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_ipaddress_dup_global.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_issue_rendering.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_job_compat.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_job_liveness.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_log_export.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_logging.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_module_readiness.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_module_readiness_rollup.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_plugin_integrations.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_primary_ip.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_primary_ip_audit.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_primary_ip_integration.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_query_binding.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_query_diff_coverage_audit_command.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_query_registry.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_release_readiness_audit.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_runtime_dependency_check.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_scope_ipam_audit.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_scope_matched_tags.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_sensitive_content.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_stability_hardening.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_sync.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_sync_aci.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_sync_facade.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_sync_orchestration.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_sync_runner_contracts.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_sync_state.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_synthetic_scenarios.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_validation_org_query_audit_command.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/urls.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/__init__.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/api_usage.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/apply_engine.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/apply_engine_bulk.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/apply_engine_decision.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/branch_budget.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/branching.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/bulk_merge.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/change_explainability.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/density_learning.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/direct_changes.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/drift_report.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/execution_ledger.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/execution_telemetry.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/fast_bootstrap_executor.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/fetch_artifacts.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/forward_api.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/forward_api_impl.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/health.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/health_apply_fetch.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/health_checks.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/health_summary_blocks.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/ingestion_issues.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/ingestion_merge.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/ingestion_presentation.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/interface_naming.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/job_compat.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/job_liveness.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/json_safe.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/logging.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/merge.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/model_contracts.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/module_readiness.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/multi_branch_lifecycle.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/plugin_integrations/__init__.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/plugin_integrations/registry.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/primary_ip.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/primary_ip_audit.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/query_binding.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/query_diagnostics.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/query_fetch.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/query_fetch_execution.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/query_registry.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/runtime_guidance.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/scope_ipam_audit.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/scope_reconciliation.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sensitive_content.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/single_branch_executor.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/snapshot_freshness.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/support_bundle_archive.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_aci.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_cable.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_contracts.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_core_models.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_device.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_events.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_execution.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_facade.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_interface.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_inventory_module.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_ipam.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_orchestration.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_primitives.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_reporting.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_routing.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_routing_impl.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_runner_adapters.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_runner_contracts.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/sync_state.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/validation.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/validation_org_query.py +0 -0
- {forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: forward-netbox
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.3
|
|
4
4
|
Summary: NetBox plugin to sync Forward data into NetBox via built-in NQE queries
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE
|
|
@@ -31,7 +31,8 @@ Forward 26.6 is the baseline for async NQE.
|
|
|
31
31
|
|
|
32
32
|
| Plugin Release | NetBox Version | Status |
|
|
33
33
|
| --- | --- | --- |
|
|
34
|
-
| `v2.2.
|
|
34
|
+
| `v2.2.3` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Field-feedback fixes: delete-count labeling, vsys/vdom auto-link, skip empty VRFs, per-device CVE list, churn pinpoint, query-ID status clarify |
|
|
35
|
+
| `v2.2.2` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.3`; Fix 504 gateway timeouts on large syncs: stop recomputing change-explainability on every poll during a long merge + back off poll to 15s |
|
|
35
36
|
| `v2.2.1` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.2`; Add read-only forward_apply_identity_audit diagnostic to pinpoint 1-created/1-deleted idempotency churn |
|
|
36
37
|
| `v2.2.0` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.1`; Fix devices mis-assigned the ACI platform; link Palo vsys / Fortinet vdom firewalls to their physical chassis |
|
|
37
38
|
| `v2.1.5` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.0`; Fix Prune orphans erroring on empty sites that still hold a VLAN/VM/prefix (delete only truly-empty sites) |
|
|
@@ -8,7 +8,8 @@ Forward 26.6 is the baseline for async NQE.
|
|
|
8
8
|
|
|
9
9
|
| Plugin Release | NetBox Version | Status |
|
|
10
10
|
| --- | --- | --- |
|
|
11
|
-
| `v2.2.
|
|
11
|
+
| `v2.2.3` | `4.6.3` required; needs netbox-branching `1.1.0+` | Current release; Field-feedback fixes: delete-count labeling, vsys/vdom auto-link, skip empty VRFs, per-device CVE list, churn pinpoint, query-ID status clarify |
|
|
12
|
+
| `v2.2.2` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.3`; Fix 504 gateway timeouts on large syncs: stop recomputing change-explainability on every poll during a long merge + back off poll to 15s |
|
|
12
13
|
| `v2.2.1` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.2`; Add read-only forward_apply_identity_audit diagnostic to pinpoint 1-created/1-deleted idempotency churn |
|
|
13
14
|
| `v2.2.0` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.1`; Fix devices mis-assigned the ACI platform; link Palo vsys / Fortinet vdom firewalls to their physical chassis |
|
|
14
15
|
| `v2.1.5` | `4.6.3` required; needs netbox-branching `1.1.0+` | Superseded by `v2.2.0`; Fix Prune orphans erroring on empty sites that still hold a VLAN/VM/prefix (delete only truly-empty sites) |
|
|
@@ -8,7 +8,7 @@ class NetboxForwardConfig(PluginConfig):
|
|
|
8
8
|
name = "forward_netbox"
|
|
9
9
|
verbose_name = "Forward Field Integration"
|
|
10
10
|
description = "Sync Forward data into NetBox using built-in NQE queries."
|
|
11
|
-
version = "2.2.
|
|
11
|
+
version = "2.2.3"
|
|
12
12
|
base_url = "forward"
|
|
13
13
|
min_version = "4.6.3"
|
|
14
14
|
|
|
@@ -167,13 +167,15 @@ def _maybe_enqueue_backfilled_tag_refresh(sync):
|
|
|
167
167
|
|
|
168
168
|
|
|
169
169
|
def _maybe_enqueue_vsys_parent_link(sync):
|
|
170
|
-
"""
|
|
170
|
+
"""After a successful sync, link virtual-context firewalls (Palo vsys /
|
|
171
171
|
Fortinet vdom) to their physical chassis via the ``forward_parent_device``
|
|
172
|
-
custom field.
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
custom field. Non-destructive, idempotent, and never affects the sync result.
|
|
173
|
+
|
|
174
|
+
Runs by DEFAULT (unlike the opt-in overlays): a blank ``Parent Device`` on
|
|
175
|
+
every vsys/vdom is a confusing default, so the link auto-refreshes each sync
|
|
176
|
+
unless the sync explicitly opts out with ``auto_link_vsys_parents=False``.
|
|
175
177
|
"""
|
|
176
|
-
if
|
|
178
|
+
if (sync.parameters or {}).get("auto_link_vsys_parents") is False:
|
|
177
179
|
return
|
|
178
180
|
try:
|
|
179
181
|
from django.utils.module_loading import import_string
|
|
@@ -188,6 +190,28 @@ def _maybe_enqueue_vsys_parent_link(sync):
|
|
|
188
190
|
logger.warning("Auto vsys parent-link enqueue failed: %s", exc)
|
|
189
191
|
|
|
190
192
|
|
|
193
|
+
def _maybe_enqueue_auto_prune(sync):
|
|
194
|
+
"""Opt-in: after a successful sync, run "Prune orphans" (delete out-of-scope
|
|
195
|
+
devices + empty orphan sites). OFF by default because it deletes NetBox data;
|
|
196
|
+
enable per sync with ``auto_prune_orphans=True``. The prune keeps its own
|
|
197
|
+
guards (refuses when the Forward scope returned 0 devices). Never lets a prune
|
|
198
|
+
problem affect the sync result.
|
|
199
|
+
"""
|
|
200
|
+
if not (sync.parameters or {}).get("auto_prune_orphans"):
|
|
201
|
+
return
|
|
202
|
+
try:
|
|
203
|
+
from django.utils.module_loading import import_string
|
|
204
|
+
|
|
205
|
+
Job.enqueue(
|
|
206
|
+
import_string("forward_netbox.jobs.prune_forward_orphans"),
|
|
207
|
+
instance=sync,
|
|
208
|
+
user=sync.user,
|
|
209
|
+
name=f"{sync.name} - prune orphans (auto)",
|
|
210
|
+
)
|
|
211
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
212
|
+
logger.warning("Auto prune-orphans enqueue failed: %s", exc)
|
|
213
|
+
|
|
214
|
+
|
|
191
215
|
def sync_forwardsync(job, *args, **kwargs):
|
|
192
216
|
sync = ForwardSync.objects.get(pk=job.object_id)
|
|
193
217
|
|
|
@@ -198,6 +222,7 @@ def sync_forwardsync(job, *args, **kwargs):
|
|
|
198
222
|
_maybe_enqueue_device_analysis_refresh(sync)
|
|
199
223
|
_maybe_enqueue_backfilled_tag_refresh(sync)
|
|
200
224
|
_maybe_enqueue_vsys_parent_link(sync)
|
|
225
|
+
_maybe_enqueue_auto_prune(sync)
|
|
201
226
|
job.terminate()
|
|
202
227
|
except Exception as exc:
|
|
203
228
|
safe_save_job_data(job, sync)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Generated by Django 6.0.6 on 2026-07-01 17:10
|
|
2
|
+
from django.db import migrations
|
|
3
|
+
from django.db import models
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Migration(migrations.Migration):
|
|
7
|
+
|
|
8
|
+
dependencies = [
|
|
9
|
+
("forward_netbox", "0029_forward_parent_device_cf"),
|
|
10
|
+
]
|
|
11
|
+
|
|
12
|
+
operations = [
|
|
13
|
+
migrations.AddField(
|
|
14
|
+
model_name="forwarddeviceanalysis",
|
|
15
|
+
name="cve_ids",
|
|
16
|
+
field=models.JSONField(blank=True, default=list),
|
|
17
|
+
),
|
|
18
|
+
]
|
|
@@ -959,6 +959,9 @@ class ForwardDeviceAnalysis(ForwardPluginModelDocsMixin, ChangeLoggedModel):
|
|
|
959
959
|
collection_result = models.CharField(max_length=64, blank=True, default="")
|
|
960
960
|
blast_radius = models.PositiveIntegerField(default=0)
|
|
961
961
|
cve_count = models.PositiveIntegerField(default=0)
|
|
962
|
+
# The actual confirmed-vulnerable CVE IDs behind cve_count, so the device
|
|
963
|
+
# panel can list them (not just the exposure number).
|
|
964
|
+
cve_ids = models.JSONField(default=list, blank=True)
|
|
962
965
|
up_interfaces = models.PositiveIntegerField(default=0)
|
|
963
966
|
detail = models.CharField(max_length=255, blank=True, default="")
|
|
964
967
|
snapshot_id = models.CharField(max_length=100, blank=True, default="")
|
{forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/queries/forward_device_analysis.nqe
RENAMED
|
@@ -30,16 +30,17 @@ let physicalLinks =
|
|
|
30
30
|
where i.operStatus == OperStatus.UP
|
|
31
31
|
foreach l in i.links
|
|
32
32
|
select l)
|
|
33
|
-
let
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
let cveIds =
|
|
34
|
+
foreach c in device.cveFindings
|
|
35
|
+
where c.isVulnerable
|
|
36
|
+
select c.cveId
|
|
37
37
|
select {
|
|
38
38
|
name: device.name,
|
|
39
39
|
reachable: device.snapshotInfo.result == DeviceSnapshotResult.completed,
|
|
40
40
|
collection_result: toString(device.snapshotInfo.result),
|
|
41
41
|
blast_radius: physicalLinks,
|
|
42
|
-
cve_count:
|
|
42
|
+
cve_count: length(cveIds),
|
|
43
|
+
cve_ids: cveIds,
|
|
43
44
|
up_interfaces: upInterfaces,
|
|
44
45
|
detail: device.locationName
|
|
45
46
|
}
|
|
@@ -19,6 +19,20 @@ foreach ni in device.networkInstances
|
|
|
19
19
|
where ni.name != "default"
|
|
20
20
|
where toString(ni.instanceType) != "NetworkInstanceType.DEFAULT_INSTANCE"
|
|
21
21
|
where isEmpty(forward_netbox_shard_keys) || ni.name in forward_netbox_shard_keys
|
|
22
|
+
// Skip VRFs with no addressing: NetBox derives prefixes/IPs from subinterface
|
|
23
|
+
// addresses keyed by networkInstanceName, so a VRF whose subinterfaces carry no
|
|
24
|
+
// IPv4/IPv6 address imports as an empty VRF and re-imports every sync. Only emit
|
|
25
|
+
// a VRF that has at least one addressed subinterface (mirrors how the prefix/IP
|
|
26
|
+
// queries associate addressing to a VRF).
|
|
27
|
+
where !isEmpty(
|
|
28
|
+
foreach interface in device.interfaces
|
|
29
|
+
foreach subinterface in interface.subinterfaces
|
|
30
|
+
where isPresent(subinterface.networkInstanceName)
|
|
31
|
+
&& subinterface.networkInstanceName == ni.name
|
|
32
|
+
&& (!isEmpty(subinterface.ipv4.addresses)
|
|
33
|
+
|| !isEmpty(subinterface.ipv6.addresses))
|
|
34
|
+
select subinterface.networkInstanceName
|
|
35
|
+
)
|
|
22
36
|
select distinct {
|
|
23
37
|
name: ni.name,
|
|
24
38
|
rd: null : String,
|
|
@@ -551,11 +551,11 @@
|
|
|
551
551
|
<td>{{ item.commit_message }}</td>
|
|
552
552
|
<td>
|
|
553
553
|
{% if item.severity == "pass" %}
|
|
554
|
-
<span class="badge text-bg-success">{{ item.status }}</span>
|
|
554
|
+
<span class="badge text-bg-success">{{ item.status_label|default:item.status }}</span>
|
|
555
555
|
{% elif item.severity == "warn" %}
|
|
556
|
-
<span class="badge text-bg-warning">{{ item.status }}</span>
|
|
556
|
+
<span class="badge text-bg-warning">{{ item.status_label|default:item.status }}</span>
|
|
557
557
|
{% else %}
|
|
558
|
-
<span class="badge text-bg-secondary">{{ item.status }}</span>
|
|
558
|
+
<span class="badge text-bg-secondary" title="{{ item.message }}">{{ item.status_label|default:item.status }}</span>
|
|
559
559
|
{% endif %}
|
|
560
560
|
</td>
|
|
561
561
|
<td>{{ item.message }}</td>
|
|
@@ -34,6 +34,14 @@
|
|
|
34
34
|
{% endif %}
|
|
35
35
|
</td>
|
|
36
36
|
</tr>
|
|
37
|
+
{% if analysis.cve_ids %}
|
|
38
|
+
<tr>
|
|
39
|
+
<th scope="row">{% trans "CVEs" %}</th>
|
|
40
|
+
<td class="lh-lg">
|
|
41
|
+
{% for cve in analysis.cve_ids %}<a href="https://nvd.nist.gov/vuln/detail/{{ cve|urlencode }}" target="_blank" rel="noopener"><code>{{ cve }}</code></a>{% if not forloop.last %} {% endif %}{% endfor %}
|
|
42
|
+
</td>
|
|
43
|
+
</tr>
|
|
44
|
+
{% endif %}
|
|
37
45
|
{% if analysis.detail %}
|
|
38
46
|
<tr>
|
|
39
47
|
<th scope="row">{% trans "Location" %}</th>
|
|
@@ -74,7 +74,8 @@
|
|
|
74
74
|
</div>
|
|
75
75
|
<div class="text-end text-muted small">
|
|
76
76
|
<div>{{ item.execution_mode }} / {{ item.fetch_mode }}</div>
|
|
77
|
-
|
|
77
|
+
{# delete_count is PLANNED intent from the Forward snapshot diff (rows Forward stopped reporting), computed before apply. The APPLIED delete count is the ingestion's num_deleted / change log — an item already absent from NetBox plans a delete but applies none, so these legitimately differ. Label as planned to avoid contradicting the change log. #}
|
|
78
|
+
<div>{{ item.row_count }} rows, {{ item.delete_count }} delete(s) planned</div>
|
|
78
79
|
</div>
|
|
79
80
|
</div>
|
|
80
81
|
</li>
|
{forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/tests/test_apply_identity_audit.py
RENAMED
|
@@ -39,6 +39,36 @@ class ApplyIdentityAuditTest(TestCase):
|
|
|
39
39
|
self.assertEqual(result["would_create_count"], 0)
|
|
40
40
|
self.assertFalse(result["churn_suspect"])
|
|
41
41
|
|
|
42
|
+
def test_churn_pairs_names_the_object_and_diff(self):
|
|
43
|
+
# The 1/1 churn signature should pinpoint the SAME logical object and the
|
|
44
|
+
# exact key disagreement (here: underscore vs dash in the slug).
|
|
45
|
+
# Both keys must differ for it to be churn (a matching name would match
|
|
46
|
+
# on the name lookup_set): NetBox slug dc_one / name "DC 1"; Forward
|
|
47
|
+
# slug dc-one / name "DC One".
|
|
48
|
+
Site.objects.create(name="DC 1", slug="dc_one")
|
|
49
|
+
rows = [{"slug": "dc-one", "name": "DC One"}]
|
|
50
|
+
result = audit_model_identity("dcim.site", rows)
|
|
51
|
+
self.assertTrue(result["churn_suspect"])
|
|
52
|
+
self.assertEqual(len(result["churn_pairs"]), 1)
|
|
53
|
+
pair = result["churn_pairs"][0]
|
|
54
|
+
self.assertIn("dc_one", pair["netbox"])
|
|
55
|
+
self.assertIn("dc-one", pair["forward"])
|
|
56
|
+
self.assertEqual(pair["differs_on"], "underscore-vs-dash")
|
|
57
|
+
self.assertGreater(pair["similarity"], 0.8)
|
|
58
|
+
|
|
59
|
+
def test_device_name_churn_is_audited(self):
|
|
60
|
+
from dcim.models import Device, DeviceRole, DeviceType, Manufacturer
|
|
61
|
+
|
|
62
|
+
site = Site.objects.create(name="S", slug="s")
|
|
63
|
+
mfr = Manufacturer.objects.create(name="M", slug="m")
|
|
64
|
+
dt = DeviceType.objects.create(manufacturer=mfr, model="DT", slug="dt")
|
|
65
|
+
role = DeviceRole.objects.create(name="R", slug="r")
|
|
66
|
+
Device.objects.create(name="fw01-vsys1", device_type=dt, role=role, site=site)
|
|
67
|
+
# Forward now emits a trailing-space variant -> name-key flip-flop.
|
|
68
|
+
result = audit_model_identity("dcim.device", [{"name": "fw01-vsys1 "}])
|
|
69
|
+
self.assertTrue(result["churn_suspect"])
|
|
70
|
+
self.assertEqual(result["churn_pairs"][0]["differs_on"], "whitespace")
|
|
71
|
+
|
|
42
72
|
def test_audit_filters_to_enabled_models(self):
|
|
43
73
|
Site.objects.create(name="A", slug="a")
|
|
44
74
|
sync = SimpleNamespace(get_model_strings=lambda: ["dcim.site", "dcim.device"])
|
|
@@ -46,5 +76,6 @@ class ApplyIdentityAuditTest(TestCase):
|
|
|
46
76
|
payload = audit_apply_identity(
|
|
47
77
|
sync, fetch_rows=lambda s, model: canned.get(model, [])
|
|
48
78
|
)
|
|
49
|
-
|
|
79
|
+
# dcim.device is now an audited model; with no device rows it is clean.
|
|
80
|
+
self.assertEqual(payload["models_audited"], ["dcim.site", "dcim.device"])
|
|
50
81
|
self.assertEqual(payload["churn_suspect_models"], [])
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from django.template.loader import render_to_string
|
|
2
|
+
from django.test import TestCase
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ExecutionInsightsDeleteLabelTest(TestCase):
|
|
6
|
+
"""The per-model panel shows delete_count, which is PLANNED intent from the
|
|
7
|
+
Forward snapshot diff (rows Forward stopped reporting), computed before apply.
|
|
8
|
+
An item already absent from NetBox plans a delete but applies none, so this
|
|
9
|
+
number legitimately differs from the applied num_deleted / change log. It must
|
|
10
|
+
be labeled "planned" so it does not read as "48 deleted" and contradict an
|
|
11
|
+
empty change log (the sync-23 confusion).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def _render(self):
|
|
15
|
+
return render_to_string(
|
|
16
|
+
"forward_netbox/inc/execution_insights.html",
|
|
17
|
+
{
|
|
18
|
+
"latest_execution_insights": {
|
|
19
|
+
"available": True,
|
|
20
|
+
"top_model_results": [
|
|
21
|
+
{
|
|
22
|
+
"model": "dcim.inventoryitem",
|
|
23
|
+
"query_name": "Forward Inventory Items",
|
|
24
|
+
"execution_mode": "query_path",
|
|
25
|
+
"fetch_mode": "nqe_parameters",
|
|
26
|
+
"row_count": 61129,
|
|
27
|
+
"delete_count": 48,
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def test_delete_count_is_labeled_planned(self):
|
|
35
|
+
html = self._render()
|
|
36
|
+
self.assertIn("48 delete(s) planned", html)
|
|
37
|
+
|
|
38
|
+
def test_bare_deleted_label_is_gone(self):
|
|
39
|
+
# The old "{n} deletes" label read as applied and contradicted the change
|
|
40
|
+
# log; it must not reappear.
|
|
41
|
+
html = self._render()
|
|
42
|
+
self.assertNotIn("48 deletes", html)
|
|
@@ -64,6 +64,40 @@ class ForwardJobsTest(TestCase):
|
|
|
64
64
|
enqueue.assert_called_once()
|
|
65
65
|
self.assertEqual(enqueue.call_args.kwargs["instance"], self.sync)
|
|
66
66
|
|
|
67
|
+
def test_vsys_parent_link_enqueues_by_default_and_respects_opt_out(self):
|
|
68
|
+
from forward_netbox.jobs import _maybe_enqueue_vsys_parent_link
|
|
69
|
+
|
|
70
|
+
# DEFAULT-ON: no parameter -> the parent-link overlay enqueues (a blank
|
|
71
|
+
# Parent Device on every vsys/vdom is a confusing default).
|
|
72
|
+
with patch("forward_netbox.jobs.Job.enqueue") as enqueue:
|
|
73
|
+
_maybe_enqueue_vsys_parent_link(self.sync)
|
|
74
|
+
enqueue.assert_called_once()
|
|
75
|
+
self.assertEqual(enqueue.call_args.kwargs["instance"], self.sync)
|
|
76
|
+
|
|
77
|
+
# Explicit opt-out with auto_link_vsys_parents=False -> no enqueue.
|
|
78
|
+
self.sync.parameters = {
|
|
79
|
+
**self.sync.parameters,
|
|
80
|
+
"auto_link_vsys_parents": False,
|
|
81
|
+
}
|
|
82
|
+
with patch("forward_netbox.jobs.Job.enqueue") as enqueue:
|
|
83
|
+
_maybe_enqueue_vsys_parent_link(self.sync)
|
|
84
|
+
enqueue.assert_not_called()
|
|
85
|
+
|
|
86
|
+
def test_auto_prune_orphans_enqueues_only_when_enabled(self):
|
|
87
|
+
from forward_netbox.jobs import _maybe_enqueue_auto_prune
|
|
88
|
+
|
|
89
|
+
# OFF by default (it deletes NetBox data): no enqueue.
|
|
90
|
+
with patch("forward_netbox.jobs.Job.enqueue") as enqueue:
|
|
91
|
+
_maybe_enqueue_auto_prune(self.sync)
|
|
92
|
+
enqueue.assert_not_called()
|
|
93
|
+
|
|
94
|
+
# Opt-in via auto_prune_orphans: enqueues the prune job.
|
|
95
|
+
self.sync.parameters = {**self.sync.parameters, "auto_prune_orphans": True}
|
|
96
|
+
with patch("forward_netbox.jobs.Job.enqueue") as enqueue:
|
|
97
|
+
_maybe_enqueue_auto_prune(self.sync)
|
|
98
|
+
enqueue.assert_called_once()
|
|
99
|
+
self.assertIn("prune orphans", enqueue.call_args.kwargs["name"])
|
|
100
|
+
|
|
67
101
|
def test_record_timeout_issue_creates_single_issue_per_ingestion_phase(self):
|
|
68
102
|
issue_1 = record_timeout_issue(
|
|
69
103
|
self.ingestion,
|
|
@@ -1604,6 +1604,10 @@ class ForwardSyncModelTest(TestCase):
|
|
|
1604
1604
|
parameters={
|
|
1605
1605
|
"snapshot_id": LATEST_PROCESSED_SNAPSHOT,
|
|
1606
1606
|
"dcim.device": True,
|
|
1607
|
+
# Isolate this test to the recurring-reschedule enqueue; the
|
|
1608
|
+
# vsys/vdom parent-link overlay is default-on and would add a
|
|
1609
|
+
# second enqueue (covered by its own test).
|
|
1610
|
+
"auto_link_vsys_parents": False,
|
|
1607
1611
|
},
|
|
1608
1612
|
)
|
|
1609
1613
|
user = get_user_model().objects.create_user(username="recurring-user")
|
|
@@ -384,6 +384,7 @@ class ScopeModuleUiTest(TestCase):
|
|
|
384
384
|
"collection_result": "DeviceSnapshotResult.completed",
|
|
385
385
|
"blast_radius": 7,
|
|
386
386
|
"cve_count": 2,
|
|
387
|
+
"cve_ids": ["CVE-2022-5678", "CVE-2021-1234"],
|
|
387
388
|
"up_interfaces": 5,
|
|
388
389
|
"detail": "DC1",
|
|
389
390
|
},
|
|
@@ -421,6 +422,8 @@ class ScopeModuleUiTest(TestCase):
|
|
|
421
422
|
self.assertTrue(analysis.reachable)
|
|
422
423
|
self.assertEqual(analysis.blast_radius, 7)
|
|
423
424
|
self.assertEqual(analysis.cve_count, 2)
|
|
425
|
+
# CVE IDs stored (deduped + sorted) so the panel can list them.
|
|
426
|
+
self.assertEqual(analysis.cve_ids, ["CVE-2021-1234", "CVE-2022-5678"])
|
|
424
427
|
self.assertEqual(analysis.up_interfaces, 5)
|
|
425
428
|
self.assertEqual(analysis.collection_result, "completed")
|
|
426
429
|
# The failed device surfaces the specific Forward collection error token.
|
|
@@ -442,6 +445,9 @@ class ScopeModuleUiTest(TestCase):
|
|
|
442
445
|
)
|
|
443
446
|
rendered = panel.right_page()
|
|
444
447
|
self.assertIn("Forward Analysis", rendered)
|
|
448
|
+
# The actual CVE IDs behind the exposure count are listed + linked to NVD.
|
|
449
|
+
self.assertIn("CVE-2021-1234", rendered)
|
|
450
|
+
self.assertIn("nvd.nist.gov/vuln/detail/CVE-2022-5678", rendered)
|
|
445
451
|
# Deep-link pivot into the Forward app.
|
|
446
452
|
self.assertIn("Open in Forward", rendered)
|
|
447
453
|
self.assertIn("https://fwd.app", rendered)
|
|
@@ -39,6 +39,25 @@ class VsysParentTest(TestCase):
|
|
|
39
39
|
self.assertEqual(vsys.custom_field_data.get(PARENT_DEVICE_CF), chassis.pk)
|
|
40
40
|
# The physical chassis itself is never linked.
|
|
41
41
|
self.assertIn(chassis.custom_field_data.get(PARENT_DEVICE_CF), (None, ""))
|
|
42
|
+
# The context is also modeled as a VirtualDeviceContext under the chassis.
|
|
43
|
+
from dcim.models import VirtualDeviceContext
|
|
44
|
+
|
|
45
|
+
self.assertEqual(result["vdc_created"], 1)
|
|
46
|
+
self.assertTrue(
|
|
47
|
+
VirtualDeviceContext.objects.filter(
|
|
48
|
+
device=chassis, name=vsys.name, status="active"
|
|
49
|
+
).exists()
|
|
50
|
+
)
|
|
51
|
+
# Re-running is idempotent — the VDC is found, not re-created.
|
|
52
|
+
again = link_vsys_parents(
|
|
53
|
+
None, fetch_rows=self._rows([(vsys.name, chassis.name)])
|
|
54
|
+
)
|
|
55
|
+
self.assertEqual(again["vdc_created"], 0)
|
|
56
|
+
self.assertEqual(again["vdc_existing"], 1)
|
|
57
|
+
self.assertEqual(
|
|
58
|
+
VirtualDeviceContext.objects.filter(device=chassis, name=vsys.name).count(),
|
|
59
|
+
1,
|
|
60
|
+
)
|
|
42
61
|
|
|
43
62
|
def test_orphan_parent_not_linked(self):
|
|
44
63
|
# vsys present in NetBox, but its physical chassis is not collected.
|
{forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/apply_identity_audit.py
RENAMED
|
@@ -16,12 +16,20 @@
|
|
|
16
16
|
# * would_delete — NetBox objects matched by no Forward row.
|
|
17
17
|
# A model with would_create AND would_delete both small (often 1/1) names the
|
|
18
18
|
# churning object and shows the differing key. NEVER writes anything.
|
|
19
|
+
import difflib
|
|
20
|
+
|
|
19
21
|
from .apply_engine_bulk import lookup_key_from_object
|
|
20
22
|
from .apply_engine_bulk import lookup_key_from_values
|
|
21
23
|
|
|
22
24
|
# Identity mirrors apply_engine_bulk's simple-model specs. The Forward query for
|
|
23
25
|
# each model emits the lookup fields directly (slug/name/rd/vid/site_slug); we
|
|
24
26
|
# read those off the row rather than recomputing slugify.
|
|
27
|
+
#
|
|
28
|
+
# dcim.device is also audited (name-level, best-effort): it is the largest model
|
|
29
|
+
# and the most likely carrier of a "1 created + 1 deleted" name-key flip-flop
|
|
30
|
+
# (e.g. a virtual-context firewall whose Forward name drifts). It is NOT a simple
|
|
31
|
+
# dimension — name uniqueness is site-scoped — so a device legitimately moved
|
|
32
|
+
# between sites can show here; treat device pairs as a lead, not a verdict.
|
|
25
33
|
SIMPLE_MODEL_IDENTITY = {
|
|
26
34
|
"dcim.site": {"lookup_sets": (("slug",), ("name",)), "row": ("slug", "name")},
|
|
27
35
|
"dcim.manufacturer": {
|
|
@@ -35,11 +43,13 @@ SIMPLE_MODEL_IDENTITY = {
|
|
|
35
43
|
"dcim.platform": {"lookup_sets": (("slug",), ("name",)), "row": ("slug", "name")},
|
|
36
44
|
"dcim.devicetype": {"lookup_sets": (("slug",),), "row": ("slug",)},
|
|
37
45
|
"ipam.vrf": {"lookup_sets": (("rd",), ("name",)), "row": ("rd", "name")},
|
|
46
|
+
"dcim.device": {"lookup_sets": (("name",),), "row": ("name",)},
|
|
38
47
|
}
|
|
39
48
|
SIMPLE_MODELS = tuple(SIMPLE_MODEL_IDENTITY)
|
|
40
49
|
|
|
41
50
|
|
|
42
51
|
def _model_class(model_string):
|
|
52
|
+
from dcim.models import Device
|
|
43
53
|
from dcim.models import DeviceRole
|
|
44
54
|
from dcim.models import DeviceType
|
|
45
55
|
from dcim.models import Manufacturer
|
|
@@ -54,9 +64,58 @@ def _model_class(model_string):
|
|
|
54
64
|
"dcim.platform": Platform,
|
|
55
65
|
"dcim.devicetype": DeviceType,
|
|
56
66
|
"ipam.vrf": VRF,
|
|
67
|
+
"dcim.device": Device,
|
|
57
68
|
}[model_string]
|
|
58
69
|
|
|
59
70
|
|
|
71
|
+
def _pair_churn(would_create, would_delete, *, limit=10):
|
|
72
|
+
"""Match each would_delete (NetBox stored) to its most-similar would_create
|
|
73
|
+
(Forward computed) so the diagnostic names the churning object and the exact
|
|
74
|
+
key disagreement, instead of two disconnected lists.
|
|
75
|
+
|
|
76
|
+
``would_create`` / ``would_delete`` are lists of (key, label) tuples — key is
|
|
77
|
+
the raw comparable string, label is the human-readable display. Returns
|
|
78
|
+
``[{netbox, forward, similarity, differs_on}]`` for the best matches.
|
|
79
|
+
"""
|
|
80
|
+
pairs = []
|
|
81
|
+
remaining = list(would_create)
|
|
82
|
+
for del_key, del_label in would_delete:
|
|
83
|
+
best = None
|
|
84
|
+
best_ratio = 0.0
|
|
85
|
+
for idx, (cre_key, cre_label) in enumerate(remaining):
|
|
86
|
+
ratio = difflib.SequenceMatcher(None, del_key, cre_key).ratio()
|
|
87
|
+
if ratio > best_ratio:
|
|
88
|
+
best_ratio = ratio
|
|
89
|
+
best = (idx, cre_key, cre_label)
|
|
90
|
+
if best is None:
|
|
91
|
+
continue
|
|
92
|
+
idx, cre_key, cre_label = best
|
|
93
|
+
pairs.append(
|
|
94
|
+
{
|
|
95
|
+
"netbox": del_label,
|
|
96
|
+
"forward": cre_label,
|
|
97
|
+
"similarity": round(best_ratio, 3),
|
|
98
|
+
"differs_on": _describe_diff(del_key, cre_key),
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
remaining.pop(idx)
|
|
102
|
+
pairs.sort(key=lambda p: p["similarity"], reverse=True)
|
|
103
|
+
return pairs[:limit]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _describe_diff(netbox_key, forward_key):
|
|
107
|
+
"""Short human hint for why two near-identical keys disagree."""
|
|
108
|
+
if netbox_key == forward_key:
|
|
109
|
+
return "identical"
|
|
110
|
+
if netbox_key.lower() == forward_key.lower():
|
|
111
|
+
return "case"
|
|
112
|
+
if netbox_key.strip() == forward_key.strip():
|
|
113
|
+
return "whitespace"
|
|
114
|
+
if netbox_key.replace("_", "-") == forward_key.replace("_", "-"):
|
|
115
|
+
return "underscore-vs-dash"
|
|
116
|
+
return "other"
|
|
117
|
+
|
|
118
|
+
|
|
60
119
|
def audit_model_identity(model_string, rows, *, sample_limit=15):
|
|
61
120
|
"""Compare Forward ``rows`` against NetBox objects for ``model_string`` and
|
|
62
121
|
report the would-create / would-delete leftovers (the churn candidates)."""
|
|
@@ -76,7 +135,7 @@ def audit_model_identity(model_string, rows, *, sample_limit=15):
|
|
|
76
135
|
|
|
77
136
|
netbox_keys = {lookup_set: set() for lookup_set in lookup_sets}
|
|
78
137
|
netbox_count = 0
|
|
79
|
-
would_delete = []
|
|
138
|
+
would_delete = [] # (primary_key, label)
|
|
80
139
|
for obj in model.objects.all().iterator():
|
|
81
140
|
netbox_count += 1
|
|
82
141
|
obj_keys = {
|
|
@@ -98,9 +157,10 @@ def audit_model_identity(model_string, rows, *, sample_limit=15):
|
|
|
98
157
|
key_parts = ", ".join(
|
|
99
158
|
f"{field}={getattr(obj, field, None)}" for field in lookup_sets[0]
|
|
100
159
|
)
|
|
101
|
-
|
|
160
|
+
primary_key = obj_keys[lookup_sets[0]] or str(obj)
|
|
161
|
+
would_delete.append((str(primary_key), f"{obj} ({key_parts})"))
|
|
102
162
|
|
|
103
|
-
would_create = []
|
|
163
|
+
would_create = [] # (primary_key, label)
|
|
104
164
|
for row in rows:
|
|
105
165
|
values = {field: (row.get(field) or None) for field in row_fields}
|
|
106
166
|
matched = False
|
|
@@ -113,19 +173,32 @@ def audit_model_identity(model_string, rows, *, sample_limit=15):
|
|
|
113
173
|
label = " / ".join(
|
|
114
174
|
str(values.get(field)) for field in row_fields if values.get(field)
|
|
115
175
|
)
|
|
116
|
-
|
|
176
|
+
primary_key = lookup_key_from_values(
|
|
177
|
+
values, lookup_sets[0], model_string=model_string
|
|
178
|
+
)
|
|
179
|
+
would_create.append((str(primary_key), label or "?"))
|
|
117
180
|
|
|
181
|
+
churn_suspect = bool(would_create) and bool(would_delete)
|
|
118
182
|
return {
|
|
119
183
|
"model": model_string,
|
|
120
184
|
"forward_rows": len(rows),
|
|
121
185
|
"netbox_count": netbox_count,
|
|
122
186
|
"would_create_count": len(would_create),
|
|
123
187
|
"would_delete_count": len(would_delete),
|
|
124
|
-
"would_create_sample": sorted(would_create)[
|
|
125
|
-
|
|
188
|
+
"would_create_sample": sorted(label for _, label in would_create)[
|
|
189
|
+
:sample_limit
|
|
190
|
+
],
|
|
191
|
+
"would_delete_sample": sorted(label for _, label in would_delete)[
|
|
192
|
+
:sample_limit
|
|
193
|
+
],
|
|
126
194
|
# The churn signature: both sides non-empty and small => one logical
|
|
127
195
|
# object with two different keys (create the new, delete the orphan).
|
|
128
|
-
"churn_suspect":
|
|
196
|
+
"churn_suspect": churn_suspect,
|
|
197
|
+
# Pinpoint: pair each stale NetBox object with the Forward row most likely
|
|
198
|
+
# to be the SAME logical object, and name the exact key disagreement.
|
|
199
|
+
"churn_pairs": (
|
|
200
|
+
_pair_churn(would_create, would_delete) if churn_suspect else []
|
|
201
|
+
),
|
|
129
202
|
}
|
|
130
203
|
|
|
131
204
|
|
|
@@ -43,6 +43,23 @@ def _coerce_int(value):
|
|
|
43
43
|
return 0
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
# Bound the stored CVE-id list so a device with a very large finding set can't
|
|
47
|
+
# bloat the row; the count (cve_count) remains authoritative for the total.
|
|
48
|
+
_CVE_IDS_LIMIT = 200
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _coerce_cve_ids(value):
|
|
52
|
+
if not isinstance(value, (list, tuple)):
|
|
53
|
+
return []
|
|
54
|
+
seen = []
|
|
55
|
+
for item in value:
|
|
56
|
+
cve = str(item or "").strip()
|
|
57
|
+
if cve and cve not in seen:
|
|
58
|
+
seen.append(cve)
|
|
59
|
+
seen.sort()
|
|
60
|
+
return seen[:_CVE_IDS_LIMIT]
|
|
61
|
+
|
|
62
|
+
|
|
46
63
|
def refresh_device_analysis(sync) -> dict:
|
|
47
64
|
"""Upsert ForwardDeviceAnalysis rows from the device-analysis NQE.
|
|
48
65
|
|
|
@@ -80,6 +97,7 @@ def refresh_device_analysis(sync) -> dict:
|
|
|
80
97
|
)[:64],
|
|
81
98
|
"blast_radius": _coerce_int(row.get("blast_radius")),
|
|
82
99
|
"cve_count": _coerce_int(row.get("cve_count")),
|
|
100
|
+
"cve_ids": _coerce_cve_ids(row.get("cve_ids")),
|
|
83
101
|
"up_interfaces": _coerce_int(row.get("up_interfaces")),
|
|
84
102
|
"detail": str(row.get("detail") or "")[:255],
|
|
85
103
|
"snapshot_id": str(snapshot_id or ""),
|
|
@@ -332,6 +332,12 @@ def clean_forward_sync(sync):
|
|
|
332
332
|
"skip_unchanged_snapshot",
|
|
333
333
|
"set_primary_ip_from_mgmt_tag",
|
|
334
334
|
"diff_fallback_mode",
|
|
335
|
+
# Post-sync overlay toggles (opt-in, except vsys parent-link which is
|
|
336
|
+
# default-on and opts OUT with auto_link_vsys_parents=False).
|
|
337
|
+
"auto_tag_backfilled",
|
|
338
|
+
"auto_refresh_device_analysis",
|
|
339
|
+
"auto_link_vsys_parents",
|
|
340
|
+
"auto_prune_orphans",
|
|
335
341
|
BRANCH_RUN_STATE_PARAMETER,
|
|
336
342
|
MODEL_CHANGE_DENSITY_PARAMETER,
|
|
337
343
|
MODEL_CHANGE_DENSITY_PROFILE_PARAMETER,
|
{forward_netbox-2.2.2 → forward_netbox-2.2.3}/forward_netbox/utilities/query_binding_resolution.py
RENAMED
|
@@ -430,6 +430,15 @@ def _committed_query_source(committed_query: dict) -> str:
|
|
|
430
430
|
return ""
|
|
431
431
|
|
|
432
432
|
|
|
433
|
+
# Friendly, non-alarming display labels for the raw drift status codes (the raw
|
|
434
|
+
# code is kept for keying/counting; only the badge text changes). "unverified"
|
|
435
|
+
# is expected + non-blocking for org-managed direct query IDs, so it should not
|
|
436
|
+
# read like an error.
|
|
437
|
+
_QUERY_DRIFT_STATUS_LABELS = {
|
|
438
|
+
"direct_query_id_unverified": "Org-managed (pinned)",
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
|
|
433
442
|
def _query_drift_result(
|
|
434
443
|
query_map: ForwardNQEMap,
|
|
435
444
|
*,
|
|
@@ -449,6 +458,9 @@ def _query_drift_result(
|
|
|
449
458
|
"model": query_map.model_string,
|
|
450
459
|
"mode": query_map.execution_mode,
|
|
451
460
|
"status": status,
|
|
461
|
+
"status_label": _QUERY_DRIFT_STATUS_LABELS.get(
|
|
462
|
+
status, status.replace("_", " ").capitalize()
|
|
463
|
+
),
|
|
452
464
|
"severity": severity,
|
|
453
465
|
"message": message,
|
|
454
466
|
"remediation": remediation,
|