From 4d44660285951c36bc1cca48eb2e20bdc1cd05d1 Mon Sep 17 00:00:00 2001 From: Aaron Axvig Date: Wed, 8 Jul 2026 13:10:46 -0500 Subject: [PATCH 1/5] Processed recent additions --- .../issues/PAN-OS/addressed/10.2.10-h37.md | 13 + .../issues/PAN-OS/addressed/10.2.10-h39.md | 40 + .../issues/PAN-OS/addressed/10.2.13-h22.md | 9 + .../issues/PAN-OS/addressed/10.2.13-h23.md | 20 + .../issues/PAN-OS/addressed/10.2.16-h8.md | 9 + .../issues/PAN-OS/addressed/10.2.16-h9.md | 20 + .../issues/PAN-OS/addressed/10.2.18-h7.md | 9 + .../issues/PAN-OS/addressed/10.2.18-h8.md | 32 + .../issues/PAN-OS/addressed/10.2.7-h35.md | 13 + .../issues/PAN-OS/addressed/10.2.7-h36.md | 20 + .../issues/PAN-OS/addressed/11.1.10-h30.md | 56 + .../issues/PAN-OS/addressed/11.1.13-h9.md | 72 + web/data/issues/PAN-OS/addressed/11.1.16.md | 321 +++++ .../issues/PAN-OS/addressed/11.1.4-h34.md | 9 + .../issues/PAN-OS/addressed/11.1.4-h35.md | 20 + .../issues/PAN-OS/addressed/11.1.6-h35.md | 41 + web/data/issues/PAN-OS/addressed/11.1.7-h7.md | 9 + web/data/issues/PAN-OS/addressed/11.1.7-h8.md | 20 + .../issues/PAN-OS/addressed/11.2.10-h12.md | 72 + web/data/issues/PAN-OS/addressed/11.2.13.md | 222 +++ .../issues/PAN-OS/addressed/11.2.4-h20.md | 20 + .../issues/PAN-OS/addressed/11.2.7-h17.md | 55 + .../issues/PAN-OS/addressed/11.2.7-h18.md | 44 + web/data/issues/PAN-OS/addressed/12.1.4-h7.md | 13 + web/data/issues/PAN-OS/addressed/12.1.4-h8.md | 20 + web/data/issues/PAN-OS/addressed/12.1.7-h1.md | 25 + web/data/issues/PAN-OS/addressed/12.1.7-h2.md | 44 + web/data/issues/PAN-OS/addressed/12.1.8.md | 1195 +++++++++++++++++ web/data/products.json | 36 +- 29 files changed, 2475 insertions(+), 4 deletions(-) create mode 100644 web/data/issues/PAN-OS/addressed/10.2.10-h37.md create mode 100644 web/data/issues/PAN-OS/addressed/10.2.10-h39.md create mode 100644 web/data/issues/PAN-OS/addressed/10.2.13-h22.md create mode 100644 web/data/issues/PAN-OS/addressed/10.2.13-h23.md create mode 100644 web/data/issues/PAN-OS/addressed/10.2.16-h8.md create mode 100644 web/data/issues/PAN-OS/addressed/10.2.16-h9.md create mode 100644 web/data/issues/PAN-OS/addressed/10.2.18-h7.md create mode 100644 web/data/issues/PAN-OS/addressed/10.2.18-h8.md create mode 100644 web/data/issues/PAN-OS/addressed/10.2.7-h35.md create mode 100644 web/data/issues/PAN-OS/addressed/10.2.7-h36.md create mode 100644 web/data/issues/PAN-OS/addressed/11.1.10-h30.md create mode 100644 web/data/issues/PAN-OS/addressed/11.1.13-h9.md create mode 100644 web/data/issues/PAN-OS/addressed/11.1.16.md create mode 100644 web/data/issues/PAN-OS/addressed/11.1.4-h34.md create mode 100644 web/data/issues/PAN-OS/addressed/11.1.4-h35.md create mode 100644 web/data/issues/PAN-OS/addressed/11.1.6-h35.md create mode 100644 web/data/issues/PAN-OS/addressed/11.1.7-h7.md create mode 100644 web/data/issues/PAN-OS/addressed/11.1.7-h8.md create mode 100644 web/data/issues/PAN-OS/addressed/11.2.10-h12.md create mode 100644 web/data/issues/PAN-OS/addressed/11.2.13.md create mode 100644 web/data/issues/PAN-OS/addressed/11.2.4-h20.md create mode 100644 web/data/issues/PAN-OS/addressed/11.2.7-h17.md create mode 100644 web/data/issues/PAN-OS/addressed/11.2.7-h18.md create mode 100644 web/data/issues/PAN-OS/addressed/12.1.4-h7.md create mode 100644 web/data/issues/PAN-OS/addressed/12.1.4-h8.md create mode 100644 web/data/issues/PAN-OS/addressed/12.1.7-h1.md create mode 100644 web/data/issues/PAN-OS/addressed/12.1.7-h2.md create mode 100644 web/data/issues/PAN-OS/addressed/12.1.8.md diff --git a/web/data/issues/PAN-OS/addressed/10.2.10-h37.md b/web/data/issues/PAN-OS/addressed/10.2.10-h37.md new file mode 100644 index 0000000..2b31baf --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/10.2.10-h37.md @@ -0,0 +1,13 @@ +--- +type: Addressed +product: PAN-OS +version: 10.2.10-h37 +--- + +## BLANK-000000 + +Fixes were made to address [CVE-2026-0273](https://security-stg.paloaltonetworks.com/CVE-2026-0273) and [CVE-2026-0272](https://security-stg.paloaltonetworks.com/CVE-2026-0272). + +## PAN-325786 + +Fixed an issue where content updates with active traffic failed. diff --git a/web/data/issues/PAN-OS/addressed/10.2.10-h39.md b/web/data/issues/PAN-OS/addressed/10.2.10-h39.md new file mode 100644 index 0000000..6f3b59a --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/10.2.10-h39.md @@ -0,0 +1,40 @@ +--- +type: Addressed +product: PAN-OS +version: 10.2.10-h39 +--- + +## BLANK-000000 + +Fixes were made to address the following CVEs: + +- [CVE-2026-0283](https://security.paloaltonetworks.com/CVE-2026-0283) +- [CVE-2026-0287](https://security.paloaltonetworks.com/CVE-2026-0287) +- [CVE-2026-0279](https://security.paloaltonetworks.com/CVE-2026-0279) +- [CVE-2026-0282](https://security.paloaltonetworks.com/CVE-2026-0282) +- [CVE-2026-0288](https://security.paloaltonetworks.com/CVE-2026-0288) +- [CVE-2026-0286](https://security.paloaltonetworks.com/CVE-2026-0286) +- [CVE-2026-0285](https://security.paloaltonetworks.com/CVE-2026-0285) +- [CVE-2026-0280](https://security.paloaltonetworks.com/CVE-2026-0280) +- [CVE-2026-0284](https://security.paloaltonetworks.com/CVE-2026-0284) +- [CVE-2026-0281](https://security.paloaltonetworks.com/CVE-2026-0281) + +## PAN-314630 + +Fixed an issue where the firewall repeatedly rebooted and entered maintenance mode, and a factory reset was required. + +## PAN-300055 + +Fixed an issue where the firewall experienced high disk utilization in the /opt/pancfg/mgmt/content-preview directory due to older content data not being automatically removed when an error occurred during the process. + +## PAN-298788 + +Fixed an issue where the /pancfg partition on the Azure Cloud NGFW reached 100% utilization, which caused commit failures. + +## PAN-284073 + +Fixed an issue on the firewall that caused commits to fail and the web interface to become inaccessible. + +## PAN-278611 + +Fixed an issue on Panorama where software images were not purged from the /opt/pancfg/mgmt/sw-images folder. diff --git a/web/data/issues/PAN-OS/addressed/10.2.13-h22.md b/web/data/issues/PAN-OS/addressed/10.2.13-h22.md new file mode 100644 index 0000000..1de7652 --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/10.2.13-h22.md @@ -0,0 +1,9 @@ +--- +type: Addressed +product: PAN-OS +version: 10.2.13-h22 +--- + +## BLANK-000000 + +Fixes were made to address [CVE-2026-0273](https://security-stg.paloaltonetworks.com/CVE-2026-0273) and [CVE-2026-0272](https://security-stg.paloaltonetworks.com/CVE-2026-0272). diff --git a/web/data/issues/PAN-OS/addressed/10.2.13-h23.md b/web/data/issues/PAN-OS/addressed/10.2.13-h23.md new file mode 100644 index 0000000..0d2a2e4 --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/10.2.13-h23.md @@ -0,0 +1,20 @@ +--- +type: Addressed +product: PAN-OS +version: 10.2.13-h23 +--- + +## BLANK-000000 + +Fixes were made to address the following CVEs: + +- [CVE-2026-0283](https://security.paloaltonetworks.com/CVE-2026-0283) +- [CVE-2026-0287](https://security.paloaltonetworks.com/CVE-2026-0287) +- [CVE-2026-0279](https://security.paloaltonetworks.com/CVE-2026-0279) +- [CVE-2026-0282](https://security.paloaltonetworks.com/CVE-2026-0282) +- [CVE-2026-0288](https://security.paloaltonetworks.com/CVE-2026-0288) +- [CVE-2026-0286](https://security.paloaltonetworks.com/CVE-2026-0286) +- [CVE-2026-0285](https://security.paloaltonetworks.com/CVE-2026-0285) +- [CVE-2026-0280](https://security.paloaltonetworks.com/CVE-2026-0280) +- [CVE-2026-0284](https://security.paloaltonetworks.com/CVE-2026-0284) +- [CVE-2026-0281](https://security.paloaltonetworks.com/CVE-2026-0281) diff --git a/web/data/issues/PAN-OS/addressed/10.2.16-h8.md b/web/data/issues/PAN-OS/addressed/10.2.16-h8.md new file mode 100644 index 0000000..57e1736 --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/10.2.16-h8.md @@ -0,0 +1,9 @@ +--- +type: Addressed +product: PAN-OS +version: 10.2.16-h8 +--- + +## BLANK-000000 + +Fixes were made to address [CVE-2026-0273](https://security-stg.paloaltonetworks.com/CVE-2026-0273) and [CVE-2026-0272](https://security-stg.paloaltonetworks.com/CVE-2026-0272). diff --git a/web/data/issues/PAN-OS/addressed/10.2.16-h9.md b/web/data/issues/PAN-OS/addressed/10.2.16-h9.md new file mode 100644 index 0000000..003aecf --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/10.2.16-h9.md @@ -0,0 +1,20 @@ +--- +type: Addressed +product: PAN-OS +version: 10.2.16-h9 +--- + +## BLANK-000000 + +Fixes were made to address the following CVEs: + +- [CVE-2026-0283](https://security.paloaltonetworks.com/CVE-2026-0283) +- [CVE-2026-0287](https://security.paloaltonetworks.com/CVE-2026-0287) +- [CVE-2026-0279](https://security.paloaltonetworks.com/CVE-2026-0279) +- [CVE-2026-0282](https://security.paloaltonetworks.com/CVE-2026-0282) +- [CVE-2026-0288](https://security.paloaltonetworks.com/CVE-2026-0288) +- [CVE-2026-0286](https://security.paloaltonetworks.com/CVE-2026-0286) +- [CVE-2026-0285](https://security.paloaltonetworks.com/CVE-2026-0285) +- [CVE-2026-0280](https://security.paloaltonetworks.com/CVE-2026-0280) +- [CVE-2026-0284](https://security.paloaltonetworks.com/CVE-2026-0284) +- [CVE-2026-0281](https://security.paloaltonetworks.com/CVE-2026-0281) diff --git a/web/data/issues/PAN-OS/addressed/10.2.18-h7.md b/web/data/issues/PAN-OS/addressed/10.2.18-h7.md new file mode 100644 index 0000000..7b62f09 --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/10.2.18-h7.md @@ -0,0 +1,9 @@ +--- +type: Addressed +product: PAN-OS +version: 10.2.18-h7 +--- + +## BLANK-000000 + +Fixes were made to address [CVE-2026-0273](https://security-stg.paloaltonetworks.com/CVE-2026-0273) and [CVE-2026-0272](https://security-stg.paloaltonetworks.com/CVE-2026-0272). diff --git a/web/data/issues/PAN-OS/addressed/10.2.18-h8.md b/web/data/issues/PAN-OS/addressed/10.2.18-h8.md new file mode 100644 index 0000000..debb1ce --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/10.2.18-h8.md @@ -0,0 +1,32 @@ +--- +type: Addressed +product: PAN-OS +version: 10.2.18-h8 +--- + +## BLANK-000000 + +Fixes were made to address the following CVEs: + +- [CVE-2026-0283](https://security.paloaltonetworks.com/CVE-2026-0283) +- [CVE-2026-0287](https://security.paloaltonetworks.com/CVE-2026-0287) +- [CVE-2026-0279](https://security.paloaltonetworks.com/CVE-2026-0279) +- [CVE-2026-0282](https://security.paloaltonetworks.com/CVE-2026-0282) +- [CVE-2026-0288](https://security.paloaltonetworks.com/CVE-2026-0288) +- [CVE-2026-0286](https://security.paloaltonetworks.com/CVE-2026-0286) +- [CVE-2026-0285](https://security.paloaltonetworks.com/CVE-2026-0285) +- [CVE-2026-0280](https://security.paloaltonetworks.com/CVE-2026-0280) +- [CVE-2026-0284](https://security.paloaltonetworks.com/CVE-2026-0284) +- [CVE-2026-0281](https://security.paloaltonetworks.com/CVE-2026-0281) + +## PAN-317466 + +Fixed an issue where SIP sessions stopped progressing after the firewall received fragmented packets, fragmented at header field. + +## PAN-306555 + +Fixed an issue where the firewall stopped responding, which led to service outages. + +## PAN-295803 + +Addressed a memory leak issue under sc3 and automatic commit recovery (ACR) code path. diff --git a/web/data/issues/PAN-OS/addressed/10.2.7-h35.md b/web/data/issues/PAN-OS/addressed/10.2.7-h35.md new file mode 100644 index 0000000..83c4c95 --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/10.2.7-h35.md @@ -0,0 +1,13 @@ +--- +type: Addressed +product: PAN-OS +version: 10.2.7-h35 +--- + +## BLANK-000000 + +Fixes were made to address [CVE-2026-0273](https://security-stg.paloaltonetworks.com/CVE-2026-0273) and [CVE-2026-0272](https://security-stg.paloaltonetworks.com/CVE-2026-0272). + +## PAN-293673 + +Fixed an issue where the firewall stopped all tasks due to an OOM condition caused by a scheduled log export using FTP to an external FTP server. diff --git a/web/data/issues/PAN-OS/addressed/10.2.7-h36.md b/web/data/issues/PAN-OS/addressed/10.2.7-h36.md new file mode 100644 index 0000000..d9c58d2 --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/10.2.7-h36.md @@ -0,0 +1,20 @@ +--- +type: Addressed +product: PAN-OS +version: 10.2.7-h36 +--- + +## BLANK-000000 + +Fixes were made to address the following CVEs: + +- [CVE-2026-0283](https://security.paloaltonetworks.com/CVE-2026-0283) +- [CVE-2026-0287](https://security.paloaltonetworks.com/CVE-2026-0287) +- [CVE-2026-0279](https://security.paloaltonetworks.com/CVE-2026-0279) +- [CVE-2026-0282](https://security.paloaltonetworks.com/CVE-2026-0282) +- [CVE-2026-0288](https://security.paloaltonetworks.com/CVE-2026-0288) +- [CVE-2026-0286](https://security.paloaltonetworks.com/CVE-2026-0286) +- [CVE-2026-0285](https://security.paloaltonetworks.com/CVE-2026-0285) +- [CVE-2026-0280](https://security.paloaltonetworks.com/CVE-2026-0280) +- [CVE-2026-0284](https://security.paloaltonetworks.com/CVE-2026-0284) +- [CVE-2026-0281](https://security.paloaltonetworks.com/CVE-2026-0281) diff --git a/web/data/issues/PAN-OS/addressed/11.1.10-h30.md b/web/data/issues/PAN-OS/addressed/11.1.10-h30.md new file mode 100644 index 0000000..1f03366 --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/11.1.10-h30.md @@ -0,0 +1,56 @@ +--- +type: Addressed +product: PAN-OS +version: 11.1.10-h30 +--- + +## BLANK-000000 + +Fixes were made to address the following CVEs: + +- [CVE-2026-0283](https://security.paloaltonetworks.com/CVE-2026-0283) +- [CVE-2026-0287](https://security.paloaltonetworks.com/CVE-2026-0287) +- [CVE-2026-0279](https://security.paloaltonetworks.com/CVE-2026-0279) +- [CVE-2026-0282](https://security.paloaltonetworks.com/CVE-2026-0282) +- [CVE-2026-0288](https://security.paloaltonetworks.com/CVE-2026-0288) +- [CVE-2026-0286](https://security.paloaltonetworks.com/CVE-2026-0286) +- [CVE-2026-0285](https://security.paloaltonetworks.com/CVE-2026-0285) +- [CVE-2026-0280](https://security.paloaltonetworks.com/CVE-2026-0280) +- [CVE-2026-0284](https://security.paloaltonetworks.com/CVE-2026-0284) +- [CVE-2026-0281](https://security.paloaltonetworks.com/CVE-2026-0281) + +## PAN-326354 + +Fixed an issue where the sslmgr process stopped responding when attempting to display the OSCP host cache. + +## PAN-324370 + +Fixed an issue where IDE traffic did not function as expected when both HTTP head insertion and DLP inspection were enabled. + +## PAN-322281 + +```caveat +Firewalls in HA configurations only +``` + +Fixed an issue where the HA 2 interface did not come up on the passive firewall, which resulted in the firewall being unable to join the HA pair. + +## PAN-321816 + +Fixed an issue where processes stopped responding unexpectedly. + +## PAN-319504 + +Fixed an issue where telemetry data was not sent to the cloud due to the firewall being unable to resolve the destination server's FQDN even when a proxy server was configured. With this fix, the firewall properly sends telemetry data through the configured proxy server without requiring direct public DNS resolution for the telemetry server's FQDN. + +## PAN-304360 + +Fixed an issue where the firewall did not redistribute its application routes to BGP peers. This occurred in multi-mesh deployments with the multi-cloud networking feature enabled. + +## PAN-286386 + +Fixed an issue where GlobalProtect users were unable to connect. + +## PAN-285327 + +Fixed an issue where a memory leak occurred when processing device and vsys tags. diff --git a/web/data/issues/PAN-OS/addressed/11.1.13-h9.md b/web/data/issues/PAN-OS/addressed/11.1.13-h9.md new file mode 100644 index 0000000..ed7200e --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/11.1.13-h9.md @@ -0,0 +1,72 @@ +--- +type: Addressed +product: PAN-OS +version: 11.1.13-h9 +--- + +## BLANK-000000 + +Fixes were made to address the following CVEs: + +- [CVE-2026-0283](https://security.paloaltonetworks.com/CVE-2026-0283) +- [CVE-2026-0287](https://security.paloaltonetworks.com/CVE-2026-0287) +- [CVE-2026-0279](https://security.paloaltonetworks.com/CVE-2026-0279) +- [CVE-2026-0282](https://security.paloaltonetworks.com/CVE-2026-0282) +- [CVE-2026-0288](https://security.paloaltonetworks.com/CVE-2026-0288) +- [CVE-2026-0286](https://security.paloaltonetworks.com/CVE-2026-0286) +- [CVE-2026-0285](https://security.paloaltonetworks.com/CVE-2026-0285) +- [CVE-2026-0280](https://security.paloaltonetworks.com/CVE-2026-0280) +- [CVE-2026-0284](https://security.paloaltonetworks.com/CVE-2026-0284) +- [CVE-2026-0281](https://security.paloaltonetworks.com/CVE-2026-0281) + +## PAN-326677 + +Fixed an issue where a selective push from Panorama to the firewall was successful even when applying rename operation failed in selective push, which resulted in configurations on the firewall being deleted. With this fix, the selective push will fail when applying rename operation fails. + +## PAN-326354 + +Fixed an issue where the sslmgr process stopped responding when attempting to display the OSCP host cache. + +## PAN-324370 + +Fixed an issue where IDE traffic did not function as expected when both HTTP head insertion and DLP inspection were enabled. + +## PAN-321816 + +Fixed an issue where processes stopped responding unexpectedly. + +## PAN-321699 + +Fixed an issue where device telemetry intermittently failed to send files, which resulted in critical alerts in system files. + +## PAN-319504 + +Fixed an issue where telemetry data was not sent to the cloud due to the firewall being unable to resolve the destination server's FQDN even when a proxy server was configured. With this fix, the firewall properly sends telemetry data through the configured proxy server without requiring direct public DNS resolution for the telemetry server's FQDN. + +## PAN-318106 + +Fixed an issue where SCM did not update device telemetry for the firewall after upgrading to an affected release. + +## PAN-312442 + +Fixed an issue where 403 errors occurred when performing "show config effective-running" API query after downgrading to an affected PAN-OS release. + +## PAN-308876 + +Fixed an issue where upgrades to managed firewalls from Panorama failed. + +## PAN-304360 + +Fixed an issue where the firewall did not redistribute its application routes to BGP peers. This occurred in multi-mesh deployments with the multi-cloud networking feature enabled. + +## PAN-297370 + +Fixed an issue where pushing a new object from Panorama to a Cloud NGFW Device Group unexpectedly removed existing Panorama-pushed policy rules, even though the **Push Preview** did not show any deletions, which led to traffic disruptions. + +## PAN-286386 + +Fixed an issue where GlobalProtect users were unable to connect + +## PAN-285327 + +Fixed an issue where a memory leak occurred when processing device and vsys tags. diff --git a/web/data/issues/PAN-OS/addressed/11.1.16.md b/web/data/issues/PAN-OS/addressed/11.1.16.md new file mode 100644 index 0000000..1572e6d --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/11.1.16.md @@ -0,0 +1,321 @@ +--- +type: Addressed +product: PAN-OS +version: 11.1.16 +--- + +## BLANK-000000 + +Fixes were made to address the following CVEs: + +- [CVE-2026-0283](https://security.paloaltonetworks.com/CVE-2026-0283) +- [CVE-2026-0287](https://security.paloaltonetworks.com/CVE-2026-0287) +- [CVE-2026-0279](https://security.paloaltonetworks.com/CVE-2026-0279) +- [CVE-2026-0282](https://security.paloaltonetworks.com/CVE-2026-0282) +- [CVE-2026-0288](https://security.paloaltonetworks.com/CVE-2026-0288) +- [CVE-2026-0286](https://security.paloaltonetworks.com/CVE-2026-0286) +- [CVE-2026-0285](https://security.paloaltonetworks.com/CVE-2026-0285) +- [CVE-2026-0280](https://security.paloaltonetworks.com/CVE-2026-0280) +- [CVE-2026-0284](https://security.paloaltonetworks.com/CVE-2026-0284) +- [CVE-2026-0281](https://security.paloaltonetworks.com/CVE-2026-0281) + +## PAN-327009 + +Fixed an issue where the all_task process stopped responding. + +## PAN-326677 + +Fixed an issue where a selective push from Panorama to the firewall was successful even when applying rename operation failed in selective push, which resulted in configurations on the firewall being deleted. With this fix, the selective push will fail when applying rename operation fails. + +## PAN-326354 + +Fixed an issue where the sslmgr process stopped responding when attempting to display the OSCP host cache. + +## PAN-324370 + +Fixed an issue where IDE traffic did not function as expected when both HTTP head insertion and DLP inspection were enabled. + +## PAN-323485 + +Fixed an issue where multicast radio RTP based traffic was dropped after an upgrade when the firewall performed Cloud Inline inspection, which led to an exceeded session queue for Cloud Threat Detection. + +## PAN-321816 + +Fixed an issue where processes stopped responding unexpectedly. + +## PAN-321699 + +Fixed an issue where device telemetry intermittently failed to send files, which resulted in critical alerts in system files. + +## PAN-321527 + +```caveat +PA-7500 firewalls in HA cluster configurations only +``` + +Fixed an issue where, when one firewall suspended operations, the other firewall also suspended operations instead of initiating a failover, which resulted in a complete traffic outage. + +## PAN-321516 + +Fixed an issue where the dataplane restarted due to a race condition in the dataplane cache infrastructure. + +## PAN-321340 + +```caveat +Firewalls in FIPS mode only +``` + +Fixed an issue where GlobalProtect unexpectedly prompted for RADIUS authentication instead of client certificate authentication due to an OSCP validation error and subsequent CRL verification failure, which led to certificates being marked as invalid. + +## PAN-321060 + +Fixed an issue where an ethernet interface remained in a down state after repeated automated enable/disable cycles and required manual intervention, which resulted in backup outages. + +## PAN-320598 + +Fixed an issue where internal and external DNS names did not resolve when connected to a GlobalProtect gateway. + +## PAN-320245 + +```caveat +PA-7500 Series firewalls in vwire mode only +``` + +Fixed an issue where Oracle application traffic was intermittently not processed even though connected devices sent the traffic, which led to service distruptions. + +## PAN-319793 + +Fixed an issue where, after upgrading to PAN-OS 12.1.5, GlobalProtect Clientless VPN failed to access JavaScripts. + +## PAN-319504 + +Fixed an issue where telemetry data was not sent to the cloud due to the firewall being unable to resolve the destination server's FQDN even when a proxy server was configured. With this fix, the firewall properly sends telemetry data through the configured proxy server without requiring direct public DNS resolution for the telemetry server's FQDN. + +## PAN-319419 + +```caveat +Firewalls in active/passive HA configurations only +``` + +Fixed an issue where active firewalls were unable to send device telemetry data to CDL. + +## PAN-319352 + +Fixed an issue where the firewall rebooted unexpectedly without any configuration or power changes. + +## PAN-319343 + +```caveat +Prisma Access Gateways only +``` + +Fixed an issue where the global management plane stopped responding, which caused SSH and HTML disconnections, HIP database lookup failures, and significantly slower SCM commits. This occurred when egress IP allow listing was enabled in SCM and changes were made to EDLs. + +## PAN-319288 + +Fixed an issue where a DPC in Slot 4 restarted repeatedly, which caused internal path monitoring failures and a failover event. + +## PAN-319228 + +Fixed an issue where External Dynamic List (EDL) refresh and commit operations remained in a pending state, which prevented any subsequent operations from completing. + +## PAN-318580 + +Fixed an issue where processes restarted and the firewall unexpectedly rebooted when you configured a Security policy rule with **Source Device > quarantine**. + +## PAN-318382 + +```caveat +Firewalls in HA configurations only +``` + +Fixed an issue where the secondary firewall remained at an **Initial** state after an upgrade. + +## PAN-318120 + +Fixed an issue where SSL traffic was silently dropped when traffic was processed by a Security policy with an Anti-Spyware profile that had Inline cloud Analysis enabled for SSL C2 Detector with an action other than allow or alert. + +## PAN-318106 + +Fixed an issue where SCM did not update device telemetry for the firewall after upgrading to an affected release. + +## PAN-317755 + +Fixed an issue on Panorama where selective push operations failed when plugin configurations included access-domain or log-collector references. + +## PAN-317648 + +```caveat +PA-5450 firewalls and PA-7000 Series firewalls with 100G NPCs only +``` + +Fixed an issue where intermittent packet loss occurred when traversing the dataplane after upgrading the firewall. This occurred when a dataplane HA interface was configured in an environment where Slot 1 was unpopulated , which resulted in a wildcard entry being created within the QMAP table. + +## PAN-317614 + +Fixed an issue where high throughput and increased packet rates caused high dataplane CPU usage. + +## PAN-316435 + +Fixed an issue where the firewall restarted unexpectedly due to an OOM condition after upgrading to an affected release. + +## PAN-316120 + +Fixed an issue where, after Advanced Routing was enabled, the firewall advertised routes to internal BGP neighbors with the original external BGP next-hop address. + +## PAN-315337 + +Fixed an issue where GlobalProtect throughput was reduced after an upgrade. + +## PAN-315326 + +```caveat +PA-7500 firewalls only +``` + +Fixed an issue where zone protection threshold values per dataplane were unexpectedly low. + +## PAN-315314 + +Fixed an issue where, when a push operation from Panorama to the firewall failed, accounting logs stopped forwarding. + +## PAN-315160 + +```caveat +PA-7500 firewalls only +``` + +Fixed an issue where internal path monitoring logs incorrectly reported internal path monitoring failures when they did not occur. + +## PAN-314776 + +Fixed an issue where the configd process stopped responding after pushing configuration changes from Panorama to the firewall. + +## PAN-314623 + +```caveat +Firewalls in active/passive HA configurations only +``` + +Fixed an issue where, after a failover, routing information within OSPF protocol was not correctly translated or propagated, which affected network path convergence and FRR capabilities. + +## PAN-314512 + +Fixed an issue where the GlobalProtect portal became inaccessible when the dataplane was configured with a DHCP assigned IP address. + +## PAN-314104 + +Fixed an issue where running BCM counter commands from the administrative shell did not consistently return output, and commands to modify queue sizes did not take effect. + +## PAN-313787 + +Fixed an issue where some system log filters with the eventid operator for a BGP event did not work. + +## PAN-313606 + +Fixed an issue where Panorama pushed commits took longer than expected to complete without displaying an error message when committing due to slow cloud-app compilation. + +## PAN-313575 + +Fixed an issue where 10G connections on built-in RJ45 interfaces (ethernet1/1 through ethernet1/5) intermittently experienced interface flapping when connected to Cisco switchports. + +## PAN-313523 + +Fixed an issue where generating a tech support file caused GlobalProtect users to be forcibly logged out. + +## PAN-313443 + +Fixed an issue where firewalls acting as an accumulation proxy sent a server hello with an earlier TCP timestamp value than a preceding ACK packet, which prevented successful session establishment. This occurred when the client hello messages were split across multiple network segments. + +To use this fix, run the CLI command debug dataplane set ssl-decrypt accumulate-client-hello ts-relay yes. + +## PAN-313218 + +Added the following CLI commands to address QoS packet drops due to bursty traffic: + +- debug dataplane set qos-setting qos-param qlimit 300 +- debug dataplane set qos-setting qos-param red low 50 high 90 + +To utilize this fix, change the parameters, disable QoS, commit changes, enable QOS, and then re-commit changes. + +## PAN-313036 + +Fixed an issue where the firewall dataplane continuously accumulated packets in the ctd_pkt_queue and packet buffers, which caused resource exhaustion and prematurely terminated sessions. + +## PAN-312442 + +Fixed an issue where 403 errors occurred when performing "show config effective-running" API query after downgrading to an affected PAN-OS release. + +## PAN-312330 + +```caveat +Firewalls in active/passive HA configurations only +``` + +Fixed an issue where the Clientless VPN applications failed to load due to the firewall dataplane incorrectly processing session information. + +## PAN-312157 + +Fixed an issue where, during a commit, the firewall intermittently stopped sending SNMP messages, which caused interface counters to stop updating for brief periods of time. + +## PAN-311658 + +Fixed an issue where the reportd process stopped responding, which caused the firewall to reboot. + +## PAN-311419 + +Fixed an issue where the recommended filter for identifying traffic from unidentified users in traffic logs reported an incorrectly low number of results. + +## PAN-310240 + +Fixed an issue where software packet buffers were completely utilized when performing a Data Loss Prevention longevity test. + +## PAN-308876 + +Fixed an issue where upgrades to managed firewalls from Panorama failed. + +## PAN-308775 + +```caveat +Firewalls in active/passive configurations only +``` + +Fixed an issue where NTP status intermittently showed as rejected on the active firewall, which prevented the firewalls from synchronizing time. + +## PAN-308444 + +Fixed an issue where pushing multiple policy rules failed when the policy rules contained a large number of dynamic address object groups or user groups. + +## PAN-307190 + +Fixed an issue where LED indicators on combo ports remained off even when the network link was active. + +## PAN-304360 + +Fixed an issue where the firewall did not redistribute its application routes to BGP peers. This occurred in multi-mesh deployments with the multi-cloud networking feature enabled. + +## PAN-295082 + +Fixed an issue on the Panorama web interface where you were unable to delete or change a logical router for tunnel, SD-WAN, VLAN, or loopback interfaces under a template. + +## PAN-289460 + +Fixed an issue where the timestamp value in SNMPv3 trap headers was incorrect. + +To use this fix, run the CLI command debug log-receiver enginetime-from-snmptime yes. + +## PAN-286386 + +Fixed an issue where GlobalProtect users were unable to connect + +## PAN-285327 + +Fixed an issue where a memory leak occurred when processing device and vsys tags. + +## PAN-267067 + +Fixed an issue where VXLAN traffic failed and packet loss occurred in networks sensors after upgrading to an affected release. + +## PAN-240066 + +Fixed a duplicate MAC address issue where an ethernet interface sent out Gratuitous ARP (GARP) messages for an IP address that was not configured on it. diff --git a/web/data/issues/PAN-OS/addressed/11.1.4-h34.md b/web/data/issues/PAN-OS/addressed/11.1.4-h34.md new file mode 100644 index 0000000..93bd319 --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/11.1.4-h34.md @@ -0,0 +1,9 @@ +--- +type: Addressed +product: PAN-OS +version: 11.1.4-h34 +--- + +## BLANK-000000 + +Fixes were made to address [CVE-2026-0273](https://security-stg.paloaltonetworks.com/CVE-2026-0273) and [CVE-2026-0272](https://security-stg.paloaltonetworks.com/CVE-2026-0272). diff --git a/web/data/issues/PAN-OS/addressed/11.1.4-h35.md b/web/data/issues/PAN-OS/addressed/11.1.4-h35.md new file mode 100644 index 0000000..40c62a8 --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/11.1.4-h35.md @@ -0,0 +1,20 @@ +--- +type: Addressed +product: PAN-OS +version: 11.1.4-h35 +--- + +## BLANK-000000 + +Fixes were made to address the following CVEs: + +- [CVE-2026-0283](https://security.paloaltonetworks.com/CVE-2026-0283) +- [CVE-2026-0287](https://security.paloaltonetworks.com/CVE-2026-0287) +- [CVE-2026-0279](https://security.paloaltonetworks.com/CVE-2026-0279) +- [CVE-2026-0282](https://security.paloaltonetworks.com/CVE-2026-0282) +- [CVE-2026-0288](https://security.paloaltonetworks.com/CVE-2026-0288) +- [CVE-2026-0286](https://security.paloaltonetworks.com/CVE-2026-0286) +- [CVE-2026-0285](https://security.paloaltonetworks.com/CVE-2026-0285) +- [CVE-2026-0280](https://security.paloaltonetworks.com/CVE-2026-0280) +- [CVE-2026-0284](https://security.paloaltonetworks.com/CVE-2026-0284) +- [CVE-2026-0281](https://security.paloaltonetworks.com/CVE-2026-0281) diff --git a/web/data/issues/PAN-OS/addressed/11.1.6-h35.md b/web/data/issues/PAN-OS/addressed/11.1.6-h35.md new file mode 100644 index 0000000..2b1cbf2 --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/11.1.6-h35.md @@ -0,0 +1,41 @@ +--- +type: Addressed +product: PAN-OS +version: 11.1.6-h35 +--- + +## BLANK-000000 + +Fixes were made to address the following CVEs: + +- [CVE-2026-0283](https://security.paloaltonetworks.com/CVE-2026-0283) +- [CVE-2026-0287](https://security.paloaltonetworks.com/CVE-2026-0287) +- [CVE-2026-0279](https://security.paloaltonetworks.com/CVE-2026-0279) +- [CVE-2026-0282](https://security.paloaltonetworks.com/CVE-2026-0282) +- [CVE-2026-0288](https://security.paloaltonetworks.com/CVE-2026-0288) +- [CVE-2026-0286](https://security.paloaltonetworks.com/CVE-2026-0286) +- [CVE-2026-0285](https://security.paloaltonetworks.com/CVE-2026-0285) +- [CVE-2026-0280](https://security.paloaltonetworks.com/CVE-2026-0280) +- [CVE-2026-0284](https://security.paloaltonetworks.com/CVE-2026-0284) +- [CVE-2026-0281](https://security.paloaltonetworks.com/CVE-2026-0281) + +## PAN-319504 + +Fixed an issue where telemetry data was not sent to the cloud due to the firewall being unable to resolve the destination server's FQDN even when a proxy server was configured. With this fix, the firewall properly sends telemetry data through the configured proxy server without requiring direct public DNS resolution for the telemetry server's FQDN. + +## PAN-313218 + +Added the following CLI commands to address QoS packet drops due to bursty traffic: + +- debug dataplane set qos-setting qos-param qlimit 300 +- debug dataplane set qos-setting qos-param red low 50 high 90 + +To utilize this fix, change the parameters, disable QoS, commit changes, enable QOS, and then re-commit changes. + +## PAN-304360 + +Fixed an issue where the firewall did not redistribute its application routes to BGP peers. This occurred in multi-mesh deployments with the multi-cloud networking feature enabled. + +## PAN-285327 + +Fixed an issue where a memory leak occurred when processing device and vsys tags. diff --git a/web/data/issues/PAN-OS/addressed/11.1.7-h7.md b/web/data/issues/PAN-OS/addressed/11.1.7-h7.md new file mode 100644 index 0000000..bba2dce --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/11.1.7-h7.md @@ -0,0 +1,9 @@ +--- +type: Addressed +product: PAN-OS +version: 11.1.7-h7 +--- + +## BLANK-000000 + +Fixes were made to address [CVE-2026-0273](https://security-stg.paloaltonetworks.com/CVE-2026-0273) and [CVE-2026-0272](https://security-stg.paloaltonetworks.com/CVE-2026-0272). diff --git a/web/data/issues/PAN-OS/addressed/11.1.7-h8.md b/web/data/issues/PAN-OS/addressed/11.1.7-h8.md new file mode 100644 index 0000000..d4a9a6d --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/11.1.7-h8.md @@ -0,0 +1,20 @@ +--- +type: Addressed +product: PAN-OS +version: 11.1.7-h8 +--- + +## BLANK-000000 + +Fixes were made to address the following CVEs: + +- [CVE-2026-0283](https://security.paloaltonetworks.com/CVE-2026-0283) +- [CVE-2026-0287](https://security.paloaltonetworks.com/CVE-2026-0287) +- [CVE-2026-0279](https://security.paloaltonetworks.com/CVE-2026-0279) +- [CVE-2026-0282](https://security.paloaltonetworks.com/CVE-2026-0282) +- [CVE-2026-0288](https://security.paloaltonetworks.com/CVE-2026-0288) +- [CVE-2026-0286](https://security.paloaltonetworks.com/CVE-2026-0286) +- [CVE-2026-0285](https://security.paloaltonetworks.com/CVE-2026-0285) +- [CVE-2026-0280](https://security.paloaltonetworks.com/CVE-2026-0280) +- [CVE-2026-0284](https://security.paloaltonetworks.com/CVE-2026-0284) +- [CVE-2026-0281](https://security.paloaltonetworks.com/CVE-2026-0281) diff --git a/web/data/issues/PAN-OS/addressed/11.2.10-h12.md b/web/data/issues/PAN-OS/addressed/11.2.10-h12.md new file mode 100644 index 0000000..9a4654b --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/11.2.10-h12.md @@ -0,0 +1,72 @@ +--- +type: Addressed +product: PAN-OS +version: 11.2.10-h12 +--- + +## BLANK-000000 + +Fixes were made to address the following CVEs: + +- [CVE-2026-0283](https://security.paloaltonetworks.com/CVE-2026-0283) +- [CVE-2026-0287](https://security.paloaltonetworks.com/CVE-2026-0287) +- [CVE-2026-0279](https://security.paloaltonetworks.com/CVE-2026-0279) +- [CVE-2026-0282](https://security.paloaltonetworks.com/CVE-2026-0282) +- [CVE-2026-0288](https://security.paloaltonetworks.com/CVE-2026-0288) +- [CVE-2026-0286](https://security.paloaltonetworks.com/CVE-2026-0286) +- [CVE-2026-0285](https://security.paloaltonetworks.com/CVE-2026-0285) +- [CVE-2026-0280](https://security.paloaltonetworks.com/CVE-2026-0280) +- [CVE-2026-0284](https://security.paloaltonetworks.com/CVE-2026-0284) +- [CVE-2026-0281](https://security.paloaltonetworks.com/CVE-2026-0281) + +## PAN-328145 + +Fixed an issue where a firewall functioning as an Area Border Router did not correctly translate NSSA Type-7 LSAs to Type-5 LSAs when OSPF neighbors set the Nt bit in the NSSA Area, and routes were not advertised to upstream OSPF neighbors in the backbone area, which resulted in traffic being silently discarded. + +## PAN-321699 + +Fixed an issue where device telemetry intermittently failed to send files, which resulted in critical alerts in system files. + +## PAN-319228 + +Fixed an issue where External Dynamic List (EDL) refresh and commit operations remained in a pending state, which prevented any subsequent operations from completing. + +## PAN-317466 + +Fixed an issue where SIP sessions stopped progressing after the firewall received fragmented packets, fragmented at header field. + +## PAN-313700 + +Fixed an issue where an unexpected reboot occurred when Inline Cloud Analysis was enabled in an Anti-Spyware and Vulnerability profile. + +## PAN-311658 + +Fixed an issue where the reportd process stopped responding, which caused the firewall to reboot. + +## PAN-308775 + +```caveat +Firewalls in active/passive configurations only +``` + +Fixed an issue where NTP status intermittently showed as rejected on the active firewall, which prevented the firewalls from synchronizing time. + +## PAN-308606 + +Fixed an issue where traffic was blocked due to a mismatch between the URL category specified in the Security policy rule and the URL filter profile when custom URL categories with the same FQDN were configured. + +## PAN-305835 + +Fixed an issue where firewalls with Memory Integrity Checking Architecture enabled rebooted unexpectedly due to accessing an invalid memory address. This occurred because the forwarding data structure index exceeded its designed limit. + +## PAN-291804 + +Fixed an issue on Panorama where deleting objects resulted in errors indicating references in Security policy rules. + +## PAN-250445 + +Fixed an issue where DLP logs accumulated in the logrcvr cache when using DLP in mirror mode. + +## PAN-246699 + +Fixed an issue on Panorama where **Rule Usage** and **Apps Seen** under Security policy rules stopped incrementing. diff --git a/web/data/issues/PAN-OS/addressed/11.2.13.md b/web/data/issues/PAN-OS/addressed/11.2.13.md new file mode 100644 index 0000000..7a8c95e --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/11.2.13.md @@ -0,0 +1,222 @@ +--- +type: Addressed +product: PAN-OS +version: 11.2.13 +--- + +## BLANK-000000 + +Fixes were made to address the following CVEs: + +- [CVE-2026-0283](https://security.paloaltonetworks.com/CVE-2026-0283) +- [CVE-2026-0287](https://security.paloaltonetworks.com/CVE-2026-0287) +- [CVE-2026-0279](https://security.paloaltonetworks.com/CVE-2026-0279) +- [CVE-2026-0282](https://security.paloaltonetworks.com/CVE-2026-0282) +- [CVE-2026-0288](https://security.paloaltonetworks.com/CVE-2026-0288) +- [CVE-2026-0286](https://security.paloaltonetworks.com/CVE-2026-0286) +- [CVE-2026-0285](https://security.paloaltonetworks.com/CVE-2026-0285) +- [CVE-2026-0280](https://security.paloaltonetworks.com/CVE-2026-0280) +- [CVE-2026-0284](https://security.paloaltonetworks.com/CVE-2026-0284) +- [CVE-2026-0281](https://security.paloaltonetworks.com/CVE-2026-0281) + +## PAN-328145 + +Fixed an issue where a firewall functioning as an Area Border Router did not correctly translate NSSA Type-7 LSAs to Type-5 LSAs when OSPF neighbors set the Nt bit in the NSSA Area, and routes were not advertised to upstream OSPF neighbors in the backbone area, which resulted in traffic being silently discarded. + +## PAN-321699 + +Fixed an issue where device telemetry intermittently failed to send files, which resulted in critical alerts in system files. + +## PAN-321150 + +Fixed an issue where the interface remained down after an upgrade. + +## PAN-320598 + +Fixed an issue where internal and external DNS names did not resolve when connected to a GlobalProtect gateway. + +## PAN-319798 + +```caveat +Panorama virtual appliances in AWS environments only +``` + +Fixed an issue where logging disks failed to mount or reported an unknown file system type. + +## PAN-319793 + +Fixed an issue where, after upgrading to PAN-OS 12.1.5, GlobalProtect Clientless VPN failed to access JavaScripts. + +## PAN-319266 + +```caveat +Cloud IPS only +``` + +Increased scale limit for zone mappings. + +## PAN-319228 + +Fixed an issue where External Dynamic List (EDL) refresh and commit operations remained in a pending state, which prevented any subsequent operations from completing. + +## PAN-318120 + +Fixed an issue where SSL traffic was silently dropped when traffic was processed by a Security policy with an Anti-Spyware profile that had Inline cloud Analysis enabled for SSL C2 Detector with an action other than allow or alert. + +## PAN-318106 + +Fixed an issue where SCM did not update device telemetry for the firewall after upgrading to an affected release. + +## PAN-318030 + +```caveat +VM-Series firewalls in Hyper-V only +``` + +) Fixed an issue where the throughput was reported to be twice as high as the actual traffic rate. + +## PAN-317755 + +Fixed an issue on Panorama where selective push operations failed when plugin configurations included access-domain or log-collector references. + +## PAN-317614 + +Fixed an issue where high throughput and increased packet rates caused high dataplane CPU usage. + +## PAN-317466 + +Fixed an issue where SIP sessions stopped progressing after the firewall received fragmented packets, fragmented at header field. + +## PAN-317215 + +```caveat +VM-Series firewalls on ESXi with Intel E810 NICs using PCI passthrough +``` + +Fixed an issue where the brdagent process became unresponsive during data port initialization, which resulted in system instability, interface outages, HA split-brain conditions, and unexpected reboots during failover. + +## PAN-315919 + +Fixed an issue where GlobalProtect pre-logon tunnel session was not cleared even after the user was logged in. With this fix, the session is cleared after the session timeout expires. + +## PAN-315337 + +Fixed an issue where GlobalProtect throughput was reduced after an upgrade. + +## PAN-315314 + +Fixed an issue where, when a push operation from Panorama to the firewall failed, accounting logs stopped forwarding. + +## PAN-314512 + +Fixed an issue where the GlobalProtect portal became inaccessible when the dataplane was configured with a DHCP assigned IP address. + +## PAN-314061 + +Fixed an issue where traffic was disrupted during IPSec rekey operations due to a 2 second delay in sending the DELETE message for the previous Security Association (SA) to the peer gateway after a new SA was negotiated. + +## PAN-314020 + +Fixed an issue where the firewall did not decapsulate GENEVE packets when DNS Security retransmitted a DNS query after receiving a verdict from the cloud. + +## PAN-313850 + +```caveat +PA-1400 Series firewalls in HA configurations only +``` + +Fixed an issue where a split-brain condition occurred and HA1/HA2 links went down while upgrading when the HA configuration used dataplane interfaces for HA1 and a combination of HSCI and Ethernet interfaces for HA2. + +## PAN-313828 + +Fixed an issue where the firewall did not forward traffic due to memory issues on a forwarding component. + +## PAN-312330 + +```caveat +Firewalls in active/passive HA configurations only +``` + +Fixed an issue where the Clientless VPN applications failed to load due to the firewall dataplane incorrectly processing session information. + +## PAN-311658 + +Fixed an issue where the reportd process stopped responding, which caused the firewall to reboot. + +## PAN-311285 + +Fixed an issue where a memory leak occurred related to the ospfd process, which caused RAM usage to continuously increase until the device stopped responding. + +## PAN-311192 + +Fixed an issue where the device-telemetry collect-now process became unresponsive when the process was initiated multiple times with other processes running concurrently, which prevented subsequent telemetry collection. + +## PAN-311040 + +Fixed an issue where the all_task process stopped responding and caused the firewall to reboot unexpectedly. + +## PAN-310240 + +Fixed an issue where software packet buffers were completely utilized when performing a Data Loss Prevention longevity test. + +## PAN-308775 + +```caveat +Firewalls in active/passive configurations only +``` + +Fixed an issue where NTP status intermittently showed as rejected on the active firewall, which prevented the firewalls from synchronizing time. + +## PAN-307976 + +```caveat +Firewalls in active/active HA configurations only +``` + +Fixed an issue where tunnels failed to come up with the error message failed to find a socket for transmission. + +## PAN-307618 + +Added a debug CLI command to address where remote networks for Prisma Access tenants randomly dropped monitoring packets from peer devices, which caused tunnels to be marked as down. This occurred when a CPU core suddenly experienced high utilization. + +To utilize this fix, run debug dataplane set ssl-decrypt use-new-peek-window yes. + +## PAN-307470 + +Fixed an issue where an External Dynamic List (EDL) fetch with an invalid certificate was skipped on newly provisioned GlobalProtect gateway instances. + +## PAN-306356 + +Fixed an issue where the logrcvr process on a firewall stopped responding due to a document node being unexpectedly freed. + +## PAN-300615 + +Fixed an issue where the pan_comm process stopped after multiple content versions were installed and the memory limits were reached. + +## PAN-298960 + +Fixed an issue where the firewall continuously rebooted when the useridd process repeatedly restarted. + +## PAN-296246 + +Fixed an issue where policy cache corruption led to unexpected policy rule behavior or operational instability. This occurred when an internal system process restarted while a commit was in progress or when a commit operation failed. + +## PAN-295806 + +Fixed an issue where memory leaks on the configd process occurred due to a hash insert operation failing during connection management and SSL connections. + +## PAN-294434 + +Fixed an issue where memory leaks occurred. These leaks were caused by two distinct scenarios: the failure to deallocate memory for a nodeset when a new nodeset was assigned to the same variable, and the failure to free a UUID hash table during error conditions. + +## PAN-250445 + +Fixed an issue where DLP logs accumulated in the logrcvr cache when using DLP in mirror mode. + +## PAN-246699 + +Fixed an issue on Panorama where **Rule Usage** and **Apps Seen** under Security policy rules stopped incrementing. + +## PAN-234302 + +Fixed an issue where commit operations took longer than expected to complete due to EDL timeouts occurring on passive nodes when a service route was enabled. diff --git a/web/data/issues/PAN-OS/addressed/11.2.4-h20.md b/web/data/issues/PAN-OS/addressed/11.2.4-h20.md new file mode 100644 index 0000000..95e04e2 --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/11.2.4-h20.md @@ -0,0 +1,20 @@ +--- +type: Addressed +product: PAN-OS +version: 11.2.4-h20 +--- + +## BLANK-000000 + +Fixes were made to address the following CVEs: + +- [CVE-2026-0283](https://security.paloaltonetworks.com/CVE-2026-0283) +- [CVE-2026-0287](https://security.paloaltonetworks.com/CVE-2026-0287) +- [CVE-2026-0279](https://security.paloaltonetworks.com/CVE-2026-0279) +- [CVE-2026-0282](https://security.paloaltonetworks.com/CVE-2026-0282) +- [CVE-2026-0288](https://security.paloaltonetworks.com/CVE-2026-0288) +- [CVE-2026-0286](https://security.paloaltonetworks.com/CVE-2026-0286) +- [CVE-2026-0285](https://security.paloaltonetworks.com/CVE-2026-0285) +- [CVE-2026-0280](https://security.paloaltonetworks.com/CVE-2026-0280) +- [CVE-2026-0284](https://security.paloaltonetworks.com/CVE-2026-0284) +- [CVE-2026-0281](https://security.paloaltonetworks.com/CVE-2026-0281) diff --git a/web/data/issues/PAN-OS/addressed/11.2.7-h17.md b/web/data/issues/PAN-OS/addressed/11.2.7-h17.md new file mode 100644 index 0000000..a611f5a --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/11.2.7-h17.md @@ -0,0 +1,55 @@ +--- +type: Addressed +product: PAN-OS +version: 11.2.7-h17 +--- + +## PAN-320598 + +Fixed an issue where internal and external DNS names did not resolve when connected to a GlobalProtect gateway. + +## PAN-317755 + +Fixed an issue on Panorama where selective push operations failed when plugin configurations included access-domain or log-collector references. + +## PAN-315337 + +Fixed an issue where GlobalProtect throughput was reduced after an upgrade. + +## PAN-314319 + +Added a CLI command to enable and disable AHO software offload optimization. + +## PAN-313606 + +Fixed an issue where Panorama pushed commits took longer than expected to complete without displaying an error message when committing due to slow cloud-app compilation. + +## PAN-310263 + +```caveat +VM-Series firewalls only +``` + +Fixed an issue where enabling TLS1.3 in a decryption profile prevented access to websites. + +## PAN-310240 + +Fixed an issue where software packet buffers were completely utilized when performing a Data Loss Prevention longevity test. + +## PAN-307618 + +Added a debug CLI command to address where remote networks for Prisma Access tenants randomly dropped monitoring packets from peer devices, which caused tunnels to be marked as down. This occurred when a CPU core suddenly experienced high utilization. + +To utilize this fix, run debug dataplane set ssl-decrypt use-new-peek-window yes. + +## PAN-307470 + +Fixed an issue where an External Dynamic List (EDL) fetch with an invalid certificate was skipped on newly provisioned GlobalProtect gateway instances. + +## PAN-266905 + +Fixed an issue where sessions ended with the message decrypt error in the logs for traffic that matched a **no-decrypt** policy. + +## PAN-234302 + +Fixed an issue where commit operations took longer than expected to complete due to EDL timeouts occurring on passive nodes when a service route was enabled. diff --git a/web/data/issues/PAN-OS/addressed/11.2.7-h18.md b/web/data/issues/PAN-OS/addressed/11.2.7-h18.md new file mode 100644 index 0000000..8e80974 --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/11.2.7-h18.md @@ -0,0 +1,44 @@ +--- +type: Addressed +product: PAN-OS +version: 11.2.7-h18 +--- + +## BLANK-000000 + +Fixes were made to address the following CVEs: + +- [CVE-2026-0283](https://security.paloaltonetworks.com/CVE-2026-0283) +- [CVE-2026-0287](https://security.paloaltonetworks.com/CVE-2026-0287) +- [CVE-2026-0279](https://security.paloaltonetworks.com/CVE-2026-0279) +- [CVE-2026-0282](https://security.paloaltonetworks.com/CVE-2026-0282) +- [CVE-2026-0288](https://security.paloaltonetworks.com/CVE-2026-0288) +- [CVE-2026-0286](https://security.paloaltonetworks.com/CVE-2026-0286) +- [CVE-2026-0285](https://security.paloaltonetworks.com/CVE-2026-0285) +- [CVE-2026-0280](https://security.paloaltonetworks.com/CVE-2026-0280) +- [CVE-2026-0284](https://security.paloaltonetworks.com/CVE-2026-0284) +- [CVE-2026-0281](https://security.paloaltonetworks.com/CVE-2026-0281) + +## PAN-308775 + +```caveat +Firewalls in active/passive configurations only +``` + +Fixed an issue where NTP status intermittently showed as rejected on the active firewall, which prevented the firewalls from synchronizing time. + +## PAN-308606 + +Fixed an issue where traffic was blocked due to a mismatch between the URL category specified in the Security policy rule and the URL filter profile when custom URL categories with the same FQDN were configured. + +## PAN-295854 + +Fixed an issue where the firewall generated two URL logs for a single session. + +## PAN-293707 + +Fixed an issue where the iotd process failed to install DPI Cloud server FQDN due to a configuration parsing failure, caused by the configuration XML memory buffer not being NULL terminated. This resulted in the accumulation of EAL logs and DLP forwarding being stopped. + +## PAN-289895 + +Fixed an issue where, when SSL decryption was enabled, traffic matching a deny rule was incorrectly allowed until the SSL handshake was complete. diff --git a/web/data/issues/PAN-OS/addressed/12.1.4-h7.md b/web/data/issues/PAN-OS/addressed/12.1.4-h7.md new file mode 100644 index 0000000..205e63d --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/12.1.4-h7.md @@ -0,0 +1,13 @@ +--- +type: Addressed +product: PAN-OS +version: 12.1.4-h7 +--- + +## BLANK-000000 + +Fixes were made to address [CVE-2026-0273](https://security-stg.paloaltonetworks.com/CVE-2026-0273) and [CVE-2026-0272](https://security-stg.paloaltonetworks.com/CVE-2026-0272). + +## PAN-317755 + +Fixed an issue on Panorama where selective push operations failed when plugin configurations included access-domain or log-collector references. diff --git a/web/data/issues/PAN-OS/addressed/12.1.4-h8.md b/web/data/issues/PAN-OS/addressed/12.1.4-h8.md new file mode 100644 index 0000000..69111b4 --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/12.1.4-h8.md @@ -0,0 +1,20 @@ +--- +type: Addressed +product: PAN-OS +version: 12.1.4-h8 +--- + +## BLANK-000000 + +Fixes were made to address the following CVEs: + +- [CVE-2026-0283](https://security.paloaltonetworks.com/CVE-2026-0283) +- [CVE-2026-0287](https://security.paloaltonetworks.com/CVE-2026-0287) +- [CVE-2026-0279](https://security.paloaltonetworks.com/CVE-2026-0279) +- [CVE-2026-0282](https://security.paloaltonetworks.com/CVE-2026-0282) +- [CVE-2026-0288](https://security.paloaltonetworks.com/CVE-2026-0288) +- [CVE-2026-0286](https://security.paloaltonetworks.com/CVE-2026-0286) +- [CVE-2026-0285](https://security.paloaltonetworks.com/CVE-2026-0285) +- [CVE-2026-0280](https://security.paloaltonetworks.com/CVE-2026-0280) +- [CVE-2026-0284](https://security.paloaltonetworks.com/CVE-2026-0284) +- [CVE-2026-0281](https://security.paloaltonetworks.com/CVE-2026-0281) diff --git a/web/data/issues/PAN-OS/addressed/12.1.7-h1.md b/web/data/issues/PAN-OS/addressed/12.1.7-h1.md new file mode 100644 index 0000000..a3cbfef --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/12.1.7-h1.md @@ -0,0 +1,25 @@ +--- +type: Addressed +product: PAN-OS +version: 12.1.7-h1 +--- + +## PAN-320598 + +Fixed an issue where internal and external DNS names did not resolve when connected to a GlobalProtect gateway. + +## PAN-317755 + +Fixed an issue on Panorama where selective push operations failed when plugin configurations included access-domain or log-collector references. + +## PAN-317133 + +Fixed an issue where you were unable to generate a ticket for the GlobalProtect portal. + +## PAN-302150 + +```caveat +Panorama appliances only +``` + +Fixed an issue where you were unable to successfully configure log collector groups due to the master node settings not populating automatically. diff --git a/web/data/issues/PAN-OS/addressed/12.1.7-h2.md b/web/data/issues/PAN-OS/addressed/12.1.7-h2.md new file mode 100644 index 0000000..d97357b --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/12.1.7-h2.md @@ -0,0 +1,44 @@ +--- +type: Addressed +product: PAN-OS +version: 12.1.7-h2 +--- + +## BLANK-000000 + +Fixes were made to address the following CVEs: + +- [CVE-2026-0283](https://security.paloaltonetworks.com/CVE-2026-0283) +- [CVE-2026-0287](https://security.paloaltonetworks.com/CVE-2026-0287) +- [CVE-2026-0279](https://security.paloaltonetworks.com/CVE-2026-0279) +- [CVE-2026-0282](https://security.paloaltonetworks.com/CVE-2026-0282) +- [CVE-2026-0288](https://security.paloaltonetworks.com/CVE-2026-0288) +- [CVE-2026-0286](https://security.paloaltonetworks.com/CVE-2026-0286) +- [CVE-2026-0285](https://security.paloaltonetworks.com/CVE-2026-0285) +- [CVE-2026-0280](https://security.paloaltonetworks.com/CVE-2026-0280) +- [CVE-2026-0284](https://security.paloaltonetworks.com/CVE-2026-0284) +- [CVE-2026-0281](https://security.paloaltonetworks.com/CVE-2026-0281) + +## PAN-324014 + +Fixed an issue where logging disks were reported with a byte size of zero in the system status even when they were properly mounted. + +## PAN-323809 + +Fixed an issue where attempting to generate a ticket for the GlobalProtect portal caused Panorama to restart unexpectedly with the error message tpl is invalid. + +## PAN-318619 + +Fixed an issue where Geneve ingress traffic did not use the correct public IP address for return traffic. + +## PAN-317867 + +Fixed an issue where Panorama became inaccessible and a manual reboot was required to restore access. This occurred due rapid increase in memory usage on the reportd process, which led to OOM events. + +## PAN-314512 + +Fixed an issue where the GlobalProtect portal became inaccessible when the dataplane was configured with a DHCP assigned IP address. + +## PAN-313700 + +Fixed an issue where an unexpected reboot occurred when Inline Cloud Analysis was enabled in an Anti-Spyware and Vulnerability profile. diff --git a/web/data/issues/PAN-OS/addressed/12.1.8.md b/web/data/issues/PAN-OS/addressed/12.1.8.md new file mode 100644 index 0000000..d5e5a43 --- /dev/null +++ b/web/data/issues/PAN-OS/addressed/12.1.8.md @@ -0,0 +1,1195 @@ +--- +type: Addressed +product: PAN-OS +version: 12.1.8 +--- + +## PAN-327009 + +Fixed an issue where the all_task process stopped responding. + +## PAN-326677 + +Fixed an issue where a selective push from Panorama to the firewall was successful even when applying rename operation failed in selective push, which resulted in configurations on the firewall being deleted. With this fix, the selective push will fail when applying rename operation fails. + +## PAN-325903 + +Fixed an issue where, after upgrading Panorama, a custom admin role with Object Level Changes disabled did not automatically populate out-of-sync firewalls in the push scope. + +## PAN-325890 + +Fixed an issue where licenses were not installed after bootstrapping a VM-Series firewall in an air-gapped environment. + +## PAN-325120 + +Fixed an issue on PA-415, PA-415-5G, PA-445, PA-455, and PA-455-5G platforms where certain PAN-OS versions caused intermittent connectivity failures on the Eth1/1 data port and loss of power on PoE ports. + +## PAN-324966 + +Fixed an issue on the web interface where you were unable to view new or modified App-IDs under **Review Policy** or **Review Apps**. + +## PAN-324370 + +Fixed an issue where IDE traffic did not function as expected when both HTTP head insertion and DLP inspection were enabled. + +## PAN-324275 + +Fixed an issue where requesting logging service forwarding certification information via the CLI did not work. + +## PAN-324014 + +Fixed an issue where logging disks were reported with a byte size of zero in the system status even when they were properly mounted. + +## PAN-323974 + +Fixed an issue where you were unable to add logging drives to the firewall, and validation errors occurred when pushing configurations from Panorama. + +## PAN-323862 + +Fixed an issue where SAML re-authentication failed when both IP address-to-user mapping and session cookies expired simultatneously. + +## PAN-323825 + +```caveat +Panorama appliances in Microsoft Azure environments only +``` + +Fixed an issue where Panorama continuously displayed disk-related read/write errors in the console logs. + +## PAN-323809 + +Fixed an issue where attempting to generate a ticket for the GlobalProtect portal caused Panorama to restart unexpectedly with the error message tpl is invalid. + +## PAN-323485 + +Fixed an issue where multicast radio RTP based traffic was dropped after an upgrade when the firewall performed Cloud Inline inspection, which led to an exceeded session queue for Cloud Threat Detection. + +## PAN-323243 + +Fixed an issue where the configd process stopped responding occurred when a Security policy rule was updated or refreshed in the web interface. + +## PAN-322815 + +```caveat +VM-Series firewalls on Microsoft Azure environments only +``` + +Fixed an issue where the firewall entered maintenance mode after enabling FIPS-CC mode and rebooted. + +## PAN-322681 + +Fixed an issue where the PDF Summary Reports were not generated correctly after upgrading to an affected release. + +## PAN-322630 + +Fixed an issue where IKE gateways were not visible within Panorama Templates under **Network Profiles** from a custom administrator role after upgrading to an affected PAN-OS release. + +## PAN-322402 + +Fixed an issue where ACC reports for a duration of seven or more days did not fully load or displayed partial information. + +## PAN-322390 + +Fixed an issue where the Enhanced Application Logging status in the Logging Service Status dashboard displayed as gray and indicated 0/0 connections, even though EAL logs were successfully forwarded to Cortex XDR. + +## PAN-322325 + +```caveat +PA-3400, PA-5400, PA-5500, and PA-5500l platforms with dedicated log interfaces only +``` + +Fixed an issue where email forwarding failed silently when the SMTP gateway was reachable only via the log-interface, even when test emails were forwarded successfully. + +## PAN-321937 + +Fixed an issue where an expired SD-WAN license caused SD-WAN tunnels to become unavailable, which resulted in traffic interruptions. With this fix, the device provides logs and commit messages about expired licenses. + +## PAN-321816 + +Fixed an issue where processes stopped responding unexpectedly. + +## PAN-321699 + +Fixed an issue where device telemetry intermittently failed to send files, which resulted in critical alerts in system files. + +## PAN-321527 + +```caveat +PA-7500 firewalls in HA cluster configurations only +``` + +Fixed an issue where, when one firewall suspended operations, the other firewall also suspended operations instead of initiating a failover, which resulted in a complete traffic outage. + +## PAN-321516 + +Fixed an issue where the dataplane restarted due to a race condition in the dataplane cache infrastructure. + +## PAN-321340 + +```caveat +Firewalls in FIPS mode only +``` + +Fixed an issue where GlobalProtect unexpectedly prompted for RADIUS authentication instead of client certificate authentication due to an OSCP validation error and subsequent CRL verification failure, which led to certificates being marked as invalid. + +## PAN-321222 + +Fixed an issue where you were unable to create an HTTP server profile an API key certificate was configured. This occurred because the generated API key exceeded the maximum character limit. + +## PAN-321150 + +Fixed an issue where the interface remained down after an upgrade. + +## PAN-321084 + +```caveat +VM-Series firewalls on ESXi platforms only +``` + +Fixed an issue where enabling link monitoring caused the brdagent process to stop responding, which caused system instability, interface outages, split-brain conditions in HA pairs, and a reboot during failover. + +## PAN-321081 + +Fixed an issue where **Log Quotas** incorrectly displayed a value that was higher than possible. + +## PAN-320598 + +Fixed an issue where internal and external DNS names did not resolve when connected to a GlobalProtect gateway. + +## PAN-320420 + +Fixed an issue where the show running resource-monitor ingress-backlogs API call returned an unexpected error instead of the expected resource monitoring information. + +## PAN-320290 + +Fixed an issue where ACC reports did not display data under the **Area** and **Column** graphs. This occurred when the report included dates prior to March 8. + +## PAN-320245 + +```caveat +PA-7500 Series firewalls in vwire mode only +``` + +Fixed an issue where Oracle application traffic was intermittently not processed even though connected devices sent the traffic, which led to service distruptions. + +## PAN-319798 + +```caveat +Panorama virtual appliances in AWS environments only +``` + +Fixed an issue where logging disks failed to mount or reported an unknown file system type. + +## PAN-319793 + +Fixed an issue where, after upgrading to PAN-OS 12.1.5, GlobalProtect Clientless VPN failed to access JavaScripts. + +## PAN-319557 + +Fixed an issue where graphical counters did not display correctly in the control plane or dataplane monitor logs. + +## PAN-319481 + +Fixed an issue on Panorama where system logs did not display the firewall serial numbers when Panorama retrieved logs from the SLS. + +## PAN-319419 + +```caveat +Firewalls in active/passive HA configurations only +``` + +Fixed an issue where active firewalls were unable to send device telemetry data to CDL. + +## PAN-319335 + +Fixed an issue where the firewall did not follow configured management proxy settings for OCSP and CRL queries, and instead reverted to default configurations after a process restart. + +## PAN-319288 + +Fixed an issue where a DPC in Slot 4 restarted repeatedly, which caused internal path monitoring failures and a failover event. + +## PAN-319266 + +```caveat +Cloud IPS only +``` + +Increased scale limit for zone mappings. + +## PAN-319228 + +Fixed an issue where External Dynamic List (EDL) refresh and commit operations remained in a pending state, which prevented any subsequent operations from completing. + +## PAN-319136 + +Fixed an issue where the firewall generated high-severity system log alerts due to a certificate trust issue during SSL handshakes. + +## PAN-318990 + +```caveat +GlobalProtect dual-profile MacOS/Windows deployments only +``` + +Fixed an issue where GlobalProtect commit warnings incorrectly flagged SAML **default browser** mismatches between authentication profiles and agent configurations. + +## PAN-318949 + +Fixed an issue where irrelevant error messages related to IoT devices filled the logrcvr logs. + +## PAN-318784 + +Fixed an issue where the firewall stopped processing traffic and all VPN tunnels went down even when the firewall remained in an active state, and the CLI became unresponsive. + +## PAN-318619 + +Fixed an issue where Geneve ingress traffic did not use the correct public IP address for return traffic. + +## PAN-318567 + +Fixed an issue where the OpenConfig plugin stopped working after a configuration update. + +## PAN-318288 + +Fixed an issue where traffic initiated from Microsoft Azure to an on-premises firewall was not decrypted, which caused the firewall to drop the traffic. This occurred due to the firewall incorrectly identifying SPI values. + +## PAN-318275 + +```caveat +VM-Series firewalls only +``` + +Fixed an issue where the firewall became unresponsive and did not automatically reboot, which led to prolonged outages. With this fix, the Linux kernel configuration will trigger a system panic and reboot. + +## PAN-318120 + +Fixed an issue where SSL traffic was silently dropped when traffic was processed by a Security policy with an Anti-Spyware profile that had Inline cloud Analysis enabled for SSL C2 Detector with an action other than allow or alert. + +## PAN-318106 + +Fixed an issue where SCM did not update device telemetry for the firewall after upgrading to an affected release. + +## PAN-318030 + +```caveat +VM-Series firewalls in Hyper-V only +``` + +) Fixed an issue where the throughput was reported to be twice as high as the actual traffic rate. + +## PAN-317867 + +Fixed an issue where Panorama became inaccessible and a manual reboot was required to restore access. This occurred due rapid increase in memory usage on the reportd process, which led to OOM events. + +## PAN-317858 + +Added a CLI command to address an issue where ethernet trailer padding was not removed during IPv4-to-IPv6 packet translation. This occurred when the original packet contained ethernet trailers and the translated packet exceeded the minimum MTU. + +## PAN-317772 + +Added a fix to improve performance in lossy network conditions. + +## PAN-317755 + +Fixed an issue on Panorama where selective push operations failed when plugin configurations included access-domain or log-collector references. + +## PAN-317749 + +Fixed an issue where the commit scope preview for a vsys incorrectly displayed configuration changes made in other vsys, even when the commit only applied changes to the intended vsys. + +## PAN-317614 + +Fixed an issue where high throughput and increased packet rates caused high dataplane CPU usage. + +## PAN-317600 + +```caveat +Firewalls in HA configurations only +``` + +Fixed an issue where autocommit operations took longer than expected to complete when the firewalls were configured with multiple vsys and EDLs. This occurred because the firewalls were unable to reach the DNS server during the autocommit process. + +## PAN-317583 + +Fixed an issue with intermittent ICMP ping drops and packet loss in traffic flows between a hub and branch after upgrading to an affected PAN-OS release due to incorrect SD-WAN path monitor state. + +## PAN-317548 + +Fixed an issue where an IMA violation occurred when Panorama accessed GRUB during the installation process, which caused upgrades from PAN-OS 12.1.4 to PAN-OS 12.1.5 to fail. + +## PAN-317466 + +Fixed an issue where SIP sessions stopped progressing after the firewall received fragmented packets, fragmented at header field. + +## PAN-317372 + +Fixed an issue where custom administrators received an **access denied** error when attempting to view specific policy rule details from the **Rule Shadow** tab after a push from Panorama, even when the administrator had permissions to view Security policy rules. + +## PAN-317215 + +```caveat +VM-Series firewalls on ESXi with Intel E810 NICs using PCI passthrough +``` + +Fixed an issue where the brdagent process became unresponsive during data port initialization, which resulted in system instability, interface outages, HA split-brain conditions, and unexpected reboots during failover. + +## PAN-317177 + +Fixed an issue on firewalls in DHCP Client mode where, after upgrading to an affected release, the SNMP process unexpectedly restarted after a commit, which led to false interface flap notifications on SNMP managers. + +## PAN-317133 + +Fixed an issue where you were unable to generate a ticket for the GlobalProtect portal. + +## PAN-317068 + +Fixed an issue on the Panorama web interface where you were able to enable IPv6 for IKE gateways and IPSec tunnels even when IPv6 WAN was disabled, which resulted in an invalid configuration. To utilize this fix, upgrade to the latest Panorama plugin. + +## PAN-316978 + +Fixed an issue where system log error messages were displayed after every firewall reboot, even when the firewall functioned correctly after the reboot. + +## PAN-316937 + +Fixed an issue where GlobalProtect users intermittently received incorrect private IP addresses after connecting to a gateway behind a Network Load Balancer (NLB). + +## PAN-316911 + +```caveat +VM-Series firewalls on Amazon Web Services (AWS) environments only +``` + +Fixed an issue where a newly bootstrapped firewall required a management server restart, relicensing, or license push from Panorama to invoke the device certificate. + +## PAN-316856 + +Fixed an issue where an error message displayed when attempting to delete the Logging Service certificate or view the Logging Service customer information. + +## PAN-316761 + +Fixed an issue where reportd process timeout errors occurred during a manual management server restart. + +## PAN-316740 + +Fixed an issue where, after upgrading to an affected release, HCE profiles exceeded the maximum character length when generated automatically, which caused subsequent commit operations to fail with a validation error. This occurred when HIP objects were associated with HIP profiles prior to the upgrade. + +## PAN-316718 + +Fixed an issue where the firewall stopped forwarding logs or generating system and configuration logs to Panorama after restarting the mgmtsrvr process. + +## PAN-316631 + +Fixed an issue BGP sessions experienced short disruptions across all peers, interfaces, and slots when a multicast event persisted longer than the NGP negotiated hold timers. + +## PAN-316605 + +Fixed an issue where HIP redistribution to remote network nodes from external gateways resulted in a large amount of error messages in User-ID logs. + +## PAN-316556 + +Fixed an issue where a race condition between the session ager and packet processing resulted in memory corruption and caused the pan_task process to stop responding, which resulted in the firewall becoming unresponsive + +## PAN-316435 + +Fixed an issue where the firewall restarted unexpectedly due to an OOM condition after upgrading to an affected release. + +## PAN-316433 + +Fixed an issue on the web interface where the last digit of entries in policy rule descriptions were truncated. + +## PAN-316263 + +Fixed an issue where an incorrect validation error was displayed, falsely indicating that IKE Gateway and IPSec tunnel names can begin with a numeral + +## PAN-316120 + +Fixed an issue where, after Advanced Routing was enabled, the firewall advertised routes to internal BGP neighbors with the original external BGP next-hop address. + +## PAN-316106 + +```caveat +Panorama appliances in Log Collector mode only +``` + +Fixed an issue where commit validation failed after an upgrade when the previous configuration included a shared-optimization setting. + +## PAN-316070 + +Fixed an issue where a BGP peer automatically established a BGP connection after manually adding it via the CLI when Advanced Routing was enabled. + +## PAN-315965 + +Fixed an issue to address TCP proxy fast recovery behavior to follow RFC 5681. + +## PAN-315964 + +Fixed an issue on the web interface where you were unable to sort an AS path list by its sequencing number (**Network > Routing > Routing Profiles > Filters > Filters AS Path Access List*). + +## PAN-315958 + +```caveat +PA-1410 firewalls only +``` + +Fixed an issue where the SaaS Quality Profile HTTP/HTTPS monitoring feature failed to send probes due to the firewall being unable to determine the correct egress interface and source IP address for the monitoring probes. + +## PAN-315913 + +Fixed an issue where, after a User-ID restart on a redistribution firewall, some expiring IP tag entries became permanent instead of aging out as intended, which affected Dynamic Address Group policy rule enforcement. + +## PAN-315912 + +Fixed an issue where the Maximum Segment Size (MSS) rewrite functionality for packets ingressing through SD-WAN interfaces on firewalls was not optimized. + +## PAN-315424 + +Fixed an issue where the BGP peer filter match condition incorrectly identified neighbors in the Advanced routing Engine, which led to incorrect Logical Preference assignments and illogical path selections. This occurred when a BGP Inbound Route Map was configured to prioritize a path from a specific peer by setting its **Local Reference**. + +## PAN-315337 + +Fixed an issue where GlobalProtect throughput was reduced after an upgrade. + +## PAN-315326 + +```caveat +PA-7500 firewalls only +``` + +Fixed an issue where zone protection threshold values per dataplane were unexpectedly low. + +## PAN-315314 + +Fixed an issue where, when a push operation from Panorama to the firewall failed, accounting logs stopped forwarding. + +## PAN-315176 + +Added an enable and disable CLI command to address an issue where the firewall experienced increased packet drops and slower performance after an upgrade due to high burst traffic. + +## PAN-315160 + +```caveat +PA-7500 firewalls only +``` + +Fixed an issue where internal path monitoring logs incorrectly reported internal path monitoring failures when they did not occur. + +## PAN-315134 + +Fixed an issue where, after an upgrade, **IoT Devices > Asset Inventory** did not display device data even though the system reported a total count of devices. + +## PAN-315005 + +Fixed an issue where configured RIPv2 timer parameters were not applied when the profile was configured with custom update, expire, and delete values, and the system continued to use the default timer settings, which caused unexpected route removal and network disconnections. + +## PAN-314873 + +Fixed an issue where the firewall intermittently stopped forwarding traffic to the internet. + +## PAN-314823 + +Fixed an issue where the management interface became unresponsive when attempting to untag an IP address via the web interface. + +## PAN-314818 + +Fixed an issue where the firewall dropped IPv6 packets after enabling **Strict IP Check** under **Zone Protection** in an SD-WAN configuration. + +## PAN-314764 + +Fixed an issue where a shared object appeared in the push scope during every push to devices even when it was not applicable to the committed changes. + +## PAN-314752 + +Fixed an issue on Panorama where, after removing a scheduled configuration push, Panorama still initiated the push at its previously scheduled time. + +## PAN-314724 + +Fixed an issue where the OpenConfig plugin was unavailable for installation after installing PAN-OS due to the plugin package not being included in the PAN-OS software bundle. + +## PAN-314712 + +```caveat +PA-7500 Series firewalls only +``` + +Fixed an issue where the source IP Dynamic Address Group mappings were intermittently not displayed under **Monitor > Traffic logs**. This occurred even when dynamic address groups were updated via XML API without an expiry time and no unregister requests were observed. + +## PAN-314630 + +Fixed an issue where the firewall repeatedly rebooted and entered maintenance mode, and a factory reset was required. + +## PAN-314623 + +```caveat +Firewalls in active/passive HA configurations only +``` + +Fixed an issue where, after a failover, routing information within OSPF protocol was not correctly translated or propagated, which affected network path convergence and FRR capabilities. + +## PAN-314512 + +Fixed an issue where the GlobalProtect portal became inaccessible when the dataplane was configured with a DHCP assigned IP address. + +## PAN-314477 + +Fixed an issue where committing configuration changes failed due to the routed process not responding. + +## PAN-314435 + +Fixed an issue on the Panorama web interface where custom application tags for cloud applications were not consistently displayed in the Application Filter or application details even though the tags were configured via CLI and successfully enforced traffic blocking policy rules. + +## PAN-314398 + +```caveat +PA-7500 firewalls in a cluster configuration only +``` + +Fixed an issue where the firewall was unable to establish a TCP connection to CDL endpoints, which prevented forwarding of traffic, system, configuration, and threat logs to the CDL. + +## PAN-314385 + +```caveat +Firewalls in active/passive HA clusters only +``` + +Fixed an issue where high dataplane CPU usage occurred and traffic offloading decreased when a failover occurred from the active firewall to the passive firewall, and then back to the active firewall. + +## PAN-314372 + +Fixed an issue where, when SSL Inbound Decryption was enabled, the inbound SMTP email delivery to an internal mail server failed due to the firewall silently dropping application packets containing SMTP commands after successful decryption. + +## PAN-314365 + +Fixed an issue where the logrcvr process stopped responding for traffic containing multiple XFF headers when URL XFF header logging was enabled along with additional XFF header logging, which caused subsequent commits to fail. + +## PAN-314319 + +Added a CLI command to enable and disable AHO software offload optimization. + +## PAN-314300 + +Fixed an issue where the firewall continued to send LLDP learned information via SNMP for an interface even after disabling LLDP on that interface. This occurred when a third-party tool polled SNMP and it received outdated topology information. + +## PAN-314223 + +Fixed an issue where the Panorama web interface did not display all Security policy rules when using a Chromium-based browser, and you were unable to scroll to the bottom of the page to view the complete list. + +## PAN-314201 + +Fixed an issue on PAN-OS 12.1 releases where intermittent traffic drops occurred over IPSec VPN tunnels to third-party firewalls during the IPSec rekey due to the firewall failing to inform the peer to delete the old SA after moving to the new one. + +## PAN-314147 + +Fixed an issue where SSL traffic was dropped on SD-WAN DIA interfaces with member having different MTU. + +## PAN-314142 + +Fixed an issue where establishing log forwarding connections to the Strata Logging Service (SLS) took longer than expected, which resulted in delayed log visibility on SLS. + +## PAN-314126 + +Fixed an issue where session rematch did not properly apply updated Security policy rules to existing traffic flows after committing changes, which caused traffic to still be allowed when a new Security policy was set to **Deny**. + +## PAN-314020 + +Fixed an issue where the firewall did not decapsulate GENEVE packets when DNS Security retransmitted a DNS query after receiving a verdict from the cloud. + +## PAN-314018 + +```caveat +VM-Series firewalls in AWS environments only +``` + +Fixed an issue where the decrypt mirror port did not function expected, which prevented decrypted traffic from reaching the intended destination collector. + +## PAN-313976 + +Fixed an issue on Panorama where traffic, URL, and unified log entries were duplicated, which led to inaccurate Security logging after applying a time filter for the previous 6 hours. + +## PAN-313828 + +Fixed an issue where the firewall did not forward traffic due to memory issues on a forwarding component. + +## PAN-313827 + +Fixed an issue where a memory leak occurred related to the reportd process when custom reports were run via API. + +## PAN-313787 + +Fixed an issue where some system log filters with the eventid operator for a BGP event did not work. + +## PAN-313779 + +```caveat +PA-7500 Series only +``` + +Fixed an issue where the request high-availability session-reestablish CLI command did not work due to encryption not being supported on HA1 and HA1-backup interfaces. + +## PAN-313700 + +Fixed an issue where an unexpected reboot occurred when Inline Cloud Analysis was enabled in an Anti-Spyware and Vulnerability profile. + +## PAN-313623 + +Fixed an issue where the /opt/pancfg/mgmt/ssl/private/ directory on Palo Alto Networks devices with TPM support became 100% utilized due to an accumulation of undeleted .pub_pem files. This occurred because executing the show device-certificate status CLI command initiated a process that generated these files but failed to remove them, which prevented the fetching of new device certificates. + +## PAN-313606 + +Fixed an issue where Panorama pushed commits took longer than expected to complete without displaying an error message when committing due to slow cloud-app compilation. + +## PAN-313575 + +Fixed an issue where 10G connections on built-in RJ45 interfaces (ethernet1/1 through ethernet1/5) intermittently experienced interface flapping when connected to Cisco switchports. + +## PAN-313572 + +```caveat +VM-Series firewalls only +``` + +) Fixed an issue where the dataplane restarted due to a segmentation fault. + +## PAN-313523 + +Fixed an issue where generating a tech support file caused GlobalProtect users to be forcibly logged out. + +## PAN-313494 + +Fixed an issue where ARP entries remained in a **complete** state with a TTL of 0 on the active-secondary node, which prevented affected devices from reliably communicating when traffic routes routed through that node. + +## PAN-313443 + +Fixed an issue where firewalls acting as an accumulation proxy sent a server hello with an earlier TCP timestamp value than a preceding ACK packet, which prevented successful session establishment. This occurred when the client hello messages were split across multiple network segments. + +To use this fix, run the CLI command debug dataplane set ssl-decrypt accumulate-client-hello ts-relay yes. + +## PAN-313258 + +Fixed an issue where PIM multicast routing failed on appliances with advanced routing enabled. + +## PAN-313216 + +Fixed an issue where firewalls with Prisma Access incorrectly displayed some traffic as unsanctioned in traffic logs for cloud applications that were tagged as sanctioned. + +## PAN-313193 + +```caveat +Firewalls in Layer 2 mode only +``` + +) Fixed an issue where the new sessions were not able to be established due to the firewall intermittently dropping valid MAC address entries for specific VLANs when a manual switchover sent a high volume of traffic to the firewall. + +## PAN-313048 + +Fixed an issue where the BGP default route was lost from the forwarding table during a failover, which caused a temporary service interruption. + +## PAN-313036 + +Fixed an issue where the firewall dataplane continuously accumulated packets in the ctd_pkt_queue and packet buffers, which caused resource exhaustion and prematurely terminated sessions. + +## PAN-312706 + +Fixed an issue where the firewalls restarted due to a function lacking a NULL-pointer sanity check. + +## PAN-312697 + +Fixed an issue where firewalls intermittently failed to send all logs to the SLS. + +## PAN-312618 + +Fixed an issue where the firewall was unable to activate GlobalProtect client software and displayed SW LIMIT messages related to max-profiles and unsupported major and minor versions in the downgrade list, which prevented successful software installation. + +## PAN-312514 + +Fixed an issue where correlation logs were not forwarded via syslog or email. + +## PAN-312354 + +Fixed an issue where Captive Portal authentication redirects failed for HTTPS traffic when a user attempted to access internal HTTPS websites via URL, which led to **ERR_CONNECTION_RESET** error messages in the browser with SSL decryption and CTD handshake inspection enabled. + +## PAN-312277 + +Fixed an issue where, after manually restarting the mgmtsrvr process caused the firewall to stop generating or forwarding system and configuration logs to Panorama, and a reboot was required to restore logging functionality. + +## PAN-312267 + +Fixed an issue where the firewall lost its MAC entry which caused IPv6 traffic sessions to become unresponsive or drop. This occurred when PBF rules were configured with symmetric return and no-pbf. + +## PAN-312156 + +Fixed an issue where firewalls did not correctly apply SD-WAN policy rules, which caused traffic to be incorrectly routed via local breakout instead of VPN backhaul. + +## PAN-311938 + +Fixed an issue where autocommits failed after an upgrade due to configuration memory allocation issues and 100% policy rule cache usage when both DNS Rewrite and URL Custom Category Match were configured. + +## PAN-311658 + +Fixed an issue where the reportd process stopped responding, which caused the firewall to reboot. + +## PAN-311512 + +Fixed an issue where HIP (Host Information Profile) reports were blocked on GlobalProtect when **Authentication Cookie Usage Restrictions** was enabled and the Prisma Access Agent protocol was in use. This occurred because the system failed to correctly process HIP messages that were relayed via IPSec tunnels with a Virtual IP as the source, leading to their rejection. + +## PAN-311456 + +Enhanced the SCP-based export script by adding comprehensive logging to identify and diagnose the root cause for failed or incomplete traffic log exports. + +## PAN-311449 + +Fixed an issue where global search did not return comprehensive results after an upgrade and only displayed top-level objects. + +## PAN-311419 + +Fixed an issue where the recommended filter for identifying traffic from unidentified users in traffic logs reported an incorrectly low number of results. + +## PAN-311412 + +Fixed an issue where the show advanced-routing resource CLI command failed to execute successfully when invoked through the XML API and returned an error message. + +## PAN-311352 + +Fixed an issue in SD-WAN deployments where DIA traffic was disrupted when DIA AnyPath was enabled during path transitions from the SD-WAN VIF to the physical interface. With this fix, the drop the packet even on zone change configuration is not needed to prevent interrupted DIA traffic during path switching. + +## PAN-311285 + +Fixed an issue where a memory leak occurred related to the ospfd process, which caused RAM usage to continuously increase until the device stopped responding. + +## PAN-311261 + +Fixed an issue where the firewall generated duplicate URL Filtering logs due to an error condition when the new XFF feature was enabled. + +## PAN-311250 + +```caveat +Panorama appliances and Log Collectors only +``` + +Fixed an issue where logs from multiple devices were not visible on Panorama even though the Elasticsearch health status on the dedicated Log Collectors appeared green. + +## PAN-311248 + +Fixed an issue where the ABR failed to translate and advertise the default route (0.0.0.0/0) from an OSPF NSSA area into the OSPF backbone area as a Type-5 LSA. + +## PAN-311218 + +Fixed an issue on Panorama where a system health check Security policy rule was applied to **any** zones instead of **Public** or **Private**. + +## PAN-311205 + +Fixed an issue where XML queries failed when you attempted to compare configuration versions. + +## PAN-311166 + +Fixed an issue where the firewall rebooted unexpectedly to the all_task_1 process repeatedly restarting. + +## PAN-311113 + +Fixed an issue where the firewall was unable to clear sessions using the CLI command clear session all filter rule when the specified rule name exceeded 32 characters, even though the limit is 63 characters. + +## PAN-311098 + +Fixed an issue where firewalls entered a nonfunctional state due to L7 running out of resources due to a high volume of traffic. + +## PAN-311074 + +Fixed an issue where GRE tunnels took significantly longer to establish when the hold timer was configured to a value of 10 or higher, which resulted in a tunnel requiring more successful keepalive packets than expected to transition to an **Up** state. + +## PAN-311040 + +Fixed an issue where the all_task process stopped responding and caused the firewall to reboot unexpectedly. + +## PAN-310851 + +Fixed an issue where firewalls experienced snmpd log flooding with messages such as update_ifTable_utilization_rates(pan_interfacecache.c:1720): Last time is 0 for dedicated-ha2., which caused the snmpd log to overflow and be cleared every five minutes. This occurred because the snmpd process attempted to calculate interface utilization rates without first verifying if the interface had valid sysd configuration data, as the code incorrectly assumed all interfaces in the MIB would possess valid sysd data. + +## PAN-310743 + +Fixed an issue where you were unable to change an administrator's authentication profile to **None**. + +## PAN-310526 + +Fixed an issue where you were unable to download cellular firmware through Panorama. + +## PAN-310473 + +Fixed an issue where committing configuration changes to an Advanced Logical router caused a 20-30 second loss of management access in the firewall when IPv4 and IPv6 default static routes were configured with identical attributes including interface, next-hop, and metrics, which triggered an unnecessary routing table refresh. + +## PAN-310472 + +Fixed an issue on the web interface where checkboxes for **default information originate** and ABR in OSPF NSSA configurations were automatically enabled which resulted in unexpected configuration changes. + +## PAN-310452 + +Fixed an issue where a configuration setting was not reset to its default value after an upgrade, which caused pre-checks and post-checks to fail. + +## PAN-310362 + +Fixed an issue where IPv6 Routed HA did not function correctly when the HA1 (control link) was configured with an IPv6 routed connection. + +## PAN-310267 + +Fixed an issue where a process stopped responding during Go garbage collection (GC). + +## PAN-310240 + +Fixed an issue where software packet buffers were completely utilized when performing a Data Loss Prevention longevity test. + +## PAN-309960 + +Fixed an issue where a memory leak related to the useridd process on the passive device led to an OOM condition. + +## PAN-309944 + +Fixed an issue where an error message was incorrectly displayed instead of a debug message. + +## PAN-309927 + +Fixed an issue on Panorama where the multi-clone XML API operation reported a successful configuration change even when the specific device group did not exist. + +## PAN-309828 + +Fixed an issue where, after a firewall serial number was updated via Panorama, a subsequent policy rule push from Panorama incorrectly deleted target policy rules from managed firewalls with the updated serial numbers. + +## PAN-309676 + +Fixed an issue on Panorama where a database component unexpectedly stopped when Panorama was deployed using an .ova file or upgraded/downgraded to an affected PAN-OS version. This occurred due to a required directory not being created during the initial provisioning workflow. With this fix, the necessary directory is created automatically during deployment. + +## PAN-309493 + +Fixed an issue where the URL cloud connection was impacted, which caused a traffic outage. + +## PAN-309300 + +Fixed an issue where management plane system resources configuration size exceeded 28 MB for over 4 hours, and the following error message was displayed: Configuration size reaching device capacity limit. + +## PAN-308928 + +Fixed an issue where OSPF routes did not install correctly when you performed a traffic switch between firewalls with the Advanced Routing Engine enabled, which led to routing instability. + +## PAN-308876 + +Fixed an issue where upgrades to managed firewalls from Panorama failed. + +## PAN-308775 + +```caveat +Firewalls in active/passive configurations only +``` + +Fixed an issue where NTP status intermittently showed as rejected on the active firewall, which prevented the firewalls from synchronizing time. + +## PAN-308732 + +```caveat +Multi-vsys firewalls only +``` + +Fixed an issue where GlobalProtect clients were unable to use custom source region objects for gateway selection criteria due to region objects defined in Panorama not being correctly recognized or displayed in the GlobalProtect Portal configuration. + +## PAN-308711 + +Fixed an issue where superusers with read-only privileges on Panorama were unable to execute show device-certificate CLI commands. + +## PAN-308651 + +Fixed an issue on the firewall web interface where the TLSv1.3_Default certificate setting and SSL/TLS profile were not displayed." + +## PAN-308563 + +Fixed an issue where multiple pan_task processes attempted to clear the packet queue of the same session. + +## PAN-308507 + +```caveat +Panorama managed firewalls only +``` + +Fixed an issue where the firewall intermittently failed to maintain active log forwarding streams to Strata Logging Service (SLS) even when duplicate logging and enhanced application logging were enabled. + +## PAN-308461 + +Fixed an issue where the CLI command request system software download to-version failed to download multiple software images due with a Download terminated due to timeout error message. + +## PAN-308444 + +Fixed an issue where pushing multiple policy rules failed when the policy rules contained a large number of dynamic address object groups or user groups. + +## PAN-308418 + +Fixed an issue where, when Advanced DNS Security was enabled and experienced unusually high loads, DNS resolution failures occurred with the error **resources-unavailable**. + +## PAN-308377 + +```caveat +PA-7000 Series firewalls with an LFC in HA configurations only +``` + +Fixed an issue where the firewall reached 100% disk utilization due to the logrcvr process repeatedly restarting and dumping core files due to a blocked hints processing thread, which caused a failover. + +## PAN-308261 + +Fixed an issue where the firewall failed to send SNMPv3 traps when the SNMP destination was configured with an FQDN that resolved to multiple IP address through DNS load balancing. + +## PAN-307937 + +Fixed an issue on the web interface where the global filter set in **ACC > Threat Activity** did not apply when you navigated to the **Network Activity** tab. + +## PAN-307773 + +Fixed an issue on Panorama where enabling Post-Quantum Pre-Shared Key (PPK) within an IKE Gateway profile that was configured as a part of a template stack failed or was inconsistent when attempted via the web interface, even when the keys were properly configured. + +## PAN-307717 + +Fixed an issue on Panorama where administrators were unable to override SNMP setup configurations within device groups due to the configured override not being retained. + +## PAN-307618 + +Added a debug CLI command to address where remote networks for Prisma Access tenants randomly dropped monitoring packets from peer devices, which caused tunnels to be marked as down. This occurred when a CPU core suddenly experienced high utilization. + +To utilize this fix, run debug dataplane set ssl-decrypt use-new-peek-window yes. + +## PAN-307491 + +Fixed an issue where the firewall entered maintenance mode after a reboot when ZTP was enabled. + +## PAN-307470 + +Fixed an issue where an External Dynamic List (EDL) fetch with an invalid certificate was skipped on newly provisioned GlobalProtect gateway instances. + +## PAN-306533 + +Fixed an issue where system logging for NTP events was delayed by approximately 15 minutes. + +## PAN-306356 + +Fixed an issue where the logrcvr process on a firewall stopped responding due to a document node being unexpectedly freed. + +## PAN-306217 + +Fixed an issue on Panorama where scheduled reports with specific queries did not include any data. + +## PAN-305950 + +Fixed an issue where, when attempting to install software upgrades on managed firewalls via **Device Deployments**, Panorama incorrectly reported that the firewalls did not have valid support licenses. + +## PAN-305619 + +Fixed an issue where HTTP management access appeared to fail and incorrectly displayed the error message **Error 503: Service Unavailable** even though it functioned correctly as allowed. This occurred when an interface was configured with an address object. + +## PAN-305369 + +Fixed an issue where the firewall dropped packets due to an invalid interface when attempting to ping the next-hop gateway from a VLAN interface due to the firewall incorrectly resolving the ARP for the gateway on an unintended interface. + +## PAN-305240 + +Fixed an issue where User-ID redistribution clients experienced delays in establishing initial communication with the redistribution server, which caused connection timeouts. + +## PAN-304718 + +Fixed an issue where OSPF and BGP outages occurred due to an all_task process restart during clientless VPN content rewrite processing. + +## PAN-304360 + +Fixed an issue where the firewall did not redistribute its application routes to BGP peers. This occurred in multi-mesh deployments with the multi-cloud networking feature enabled. + +## PAN-303662 + +Fixed an issue where PA-455 firewalls running PAN-OS 11.2.4-h7 intermittently failed to generate system logs and trigger an HA failover when a link-monitored interface was unplugged, despite the interface's status being reflected as down on the GUI. + +## PAN-303173 + +```caveat +Firewalls in Advanced Routing mode only +``` + +Fixed an issue where OSPF sessions using MD5 authentication experienced intermittent flapping due to out-of-order packets. + +## PAN-302855 + +Fixed an issue where multiple processes restarted which caused the firewall to become unstable when processing traffic. + +## PAN-302834 + +Fixed an issue where Panorama did not display decryption logs after a certain date due to the decryption index being purged. + +## PAN-302512 + +```caveat +Log Collectors in HA configurations only +``` + +Fixed an issue where log collectors displayed a disconnected inter-log collector status. + +## PAN-302387 + +Fixed an issue where on PA-7500 firewalls, SNMP incorrectly reported the administrative and operational status of High Speed Chassis Interconnect (HSCI) interfaces as down, even when the interfaces were physically up. Additionally, interface counters for these interfaces displayed all zeroes. + +## PAN-302150 + +```caveat +Panorama appliances only +``` + +Fixed an issue where you were unable to successfully configure log collector groups due to the master node settings not populating automatically. + +## PAN-301513 + +Fixed an issue on Panorama managed multi-vsys firewalls where, when the shared-to-shared feature was enabled, shared objects reverted to an older configuration after a selective push to a vsys. + +## PAN-300617 + +Fixed an issue where the Elasticsearch cluster status displayed as red due to unassigned shards, which prevented logs from updating. + +## PAN-300615 + +Fixed an issue where the pan_comm process stopped after multiple content versions were installed and the memory limits were reached. + +## PAN-300445 + +Fixed an issue where the firewall downloaded an Antivirus package but did not automatically install it. + +## PAN-300423 + +Fixed an issue where Data Processing Cards (DPCs) installed in slots 5 and 6 remained stuck in a starting state with the error **Signal detected for port xeS5-DP0 but Link Down** alerts, which resulted in device instability. + +## PAN-298960 + +Fixed an issue where the firewall continuously rebooted when the useridd process repeatedly restarted. + +## PAN-298788 + +Fixed an issue where the /pancfg partition on the Azure Cloud NGFW reached 100% utilization, which caused commit failures. + +## PAN-298252 + +Fixed an issue where Data Loss Prevention (DLP) inspection of chunked transfer encoding over TLS resulted in incomplete file downloads on Outlook Web App (OWA) due to the WIF page size limit, which led to corrupted or incomplete PDF attachments. + +## PAN-297819 + +Fixed an issue where the firewall was unable to send device telemetry files to Cortex Data Lake due to the firewall receiving an invalid upload token. + +## PAN-296635 + +Fixed an issue where the reportd process on passive Panorama management servers leaked memory due to scheduled report handling from the Strata Logging Service (SLS). This memory leak occurred daily, consuming available memory until the process was restarted. + +## PAN-296246 + +Fixed an issue where policy cache corruption led to unexpected policy rule behavior or operational instability. This occurred when an internal system process restarted while a commit was in progress or when a commit operation failed. + +## PAN-295806 + +Fixed an issue where memory leaks on the configd process occurred due to a hash insert operation failing during connection management and SSL connections. + +## PAN-295082 + +Fixed an issue on the Panorama web interface where you were unable to delete or change a logical router for tunnel, SD-WAN, VLAN, or loopback interfaces under a template. + +## PAN-295047 + +Fixed an issue where the staticd process stopped responding. + +## PAN-294998 + +Fixed an issue where the LogDB incorrectly reported that the database quota for extpcap logs was reached. + +## PAN-294434 + +Fixed an issue where memory leaks occurred. These leaks were caused by two distinct scenarios: the failure to deallocate memory for a nodeset when a new nodeset was assigned to the same variable, and the failure to free a UUID hash table during error conditions. + +## PAN-293586 + +```caveat +Panorama virtual appliances only +``` + +Introduced a CLI command to increase the limit of user types from 1 million to 3.6 million. + +## PAN-292447 + +Fixed an issue where Panorama did not display data in the **Feature Adoption** tab in Strata Cloud Manager due to the system creating and deleting a CLI user for each interval instead of reusing a permanent CLI user for telemetry. + +## PAN-292220 + +Fixed an issue where the Status LED on PA-7500 SFCs did not work. + +## PAN-292191 + +Fixed an issue where the firewall dropped packets related to call recording and voice calls, which resulted in communication failures, retransmissions, and disconnected calls. This occurred when the firewall was positioned between a Private Branch Exchange and an AES server and users registered phones across different data centers. + +## PAN-291785 + +Fixed an issue where the all_task process stopped responding. + +## PAN-291284 + +Fixed an issue where single-session IPSec VPN traffic was distributed across multiple member interfaces of a Link Aggregation Group configured with LACP. This resulted in packet reordering and loss, which impacted VPN performance. + +## PAN-290712 + +```caveat +PA-7500 Firewalls in cluster mode only +``` + +Fixed an issue where the firewall incorrectly advertised BGP routes back to the external BGP peer, which resulted in routing inefficiency. + +## PAN-289578 + +Fixed an issue on Panorama managed firewalls where the source user, source device vendor, source MAC address, and OS version information were not visible in traffic logs and SCM when the user and device access control lists were empty. + +## PAN-289460 + +Fixed an issue where the timestamp value in SNMPv3 trap headers was incorrect. + +To use this fix, run the CLI command debug log-receiver enginetime-from-snmptime yes. + +## PAN-287280 + +Fixed an issue where a configd crash occurred when the **Policies > Security** view was updated or refreshed in the web interface. + +## PAN-283704 + +Fixed an issue where the PAN-OS DoS protection feature by default blacklisted specific IP addresses, which caused outbound traffic domain resolution to fail for clusters. + +## PAN-282335 + +Fixed an issue where firewalls in a cluster experienced approximately 50% packet loss on IPSec NATT tunnels when tunnel acceleration was enabled. + +## PAN-280196 + +Fixed an issue in Prisma Access environments where the firewall matched a HIP object but not on the HIP profile that contained the object. + +## PAN-274622 + +Fixed an issue on the Panorama web interface where GlobalProtect client images were not exported via SCP. + +## PAN-273805 + +Fixed an issue where SAML authentication for GlobalProtect failed when the GlobalProtect portal was accessed externally on a non-standard port. + +## PAN-273028 + +Fixed an issue where manual SCP exports from firewalls in FIPS mode were successful to SCP servers that were not FIPS-compliant. This occurred because the manual SCP process did not enforce FIPS security checks. + +## PAN-272175 + +Fixed an issue where session rematch caused ACE cloud application traffic to match the wrong policy. + +## PAN-266843 + +Fixed an issue on airgapped firewalls where cloud connection errors flooded the system logs. + +## PAN-264762 + +Fixed an issue where the firewall showed the status of SFP+ interfaces as not up, or up but not configured, when a PAN-SFP-PLUS-SR cable was connected. + +## PAN-264349 + +Fixed an issue where the Management Processor Card (MPC) on modular firewalls became unresponsive when a disk drive entered a low-power state and failed to wake up. + +## PAN-260661 + +Fixed an issue where daily email reports generated from the custom report did not display the report details in PDF or CSV files. + +## PAN-250445 + +Fixed an issue where DLP logs accumulated in the logrcvr cache when using DLP in mirror mode. diff --git a/web/data/products.json b/web/data/products.json index 6ade5e3..c652e11 100644 --- a/web/data/products.json +++ b/web/data/products.json @@ -12,9 +12,14 @@ "12.1.4-h3.md", "12.1.4-h5.md", "12.1.4-h6.md", + "12.1.4-h7.md", + "12.1.4-h8.md", "12.1.5.md", "12.1.6.md", - "12.1.7.md" + "12.1.7.md", + "12.1.7-h1.md", + "12.1.7-h2.md", + "12.1.8.md" ], "known": [ "12.1.2.md", @@ -53,6 +58,7 @@ "11.2.4-h14.md", "11.2.4-h15.md", "11.2.4-h17.md", + "11.2.4-h20.md", "11.2.5.md", "11.2.5-h2.md", "11.2.6.md", @@ -69,6 +75,8 @@ "11.2.7-h13.md", "11.2.7-h14.md", "11.2.7-h15.md", + "11.2.7-h17.md", + "11.2.7-h18.md", "11.2.8.md", "11.2.9.md", "11.2.10.md", @@ -81,8 +89,10 @@ "11.2.10-h7.md", "11.2.10-h8.md", "11.2.10-h10.md", + "11.2.10-h12.md", "11.2.11.md", - "11.2.12.md" + "11.2.12.md", + "11.2.13.md" ], "known": [ "11.2.0.md", @@ -141,6 +151,8 @@ "11.1.4-h27.md", "11.1.4-h32.md", "11.1.4-h33.md", + "11.1.4-h34.md", + "11.1.4-h35.md", "11.1.5.md", "11.1.5-h1.md", "11.1.6.md", @@ -163,11 +175,14 @@ "11.1.6-h32.md", "11.1.6-h33.md", "11.1.6-h34.md", + "11.1.6-h35.md", "11.1.7.md", "11.1.7-h1.md", "11.1.7-h2.md", "11.1.7-h4.md", "11.1.7-h6.md", + "11.1.7-h7.md", + "11.1.7-h8.md", "11.1.8.md", "11.1.9.md", "11.1.10.md", @@ -183,6 +198,7 @@ "11.1.10-h26.md", "11.1.10-h27.md", "11.1.10-h28.md", + "11.1.10-h30.md", "11.1.11.md", "11.1.12.md", "11.1.13.md", @@ -193,8 +209,10 @@ "11.1.13-h6.md", "11.1.13-h7.md", "11.1.13-h8.md", + "11.1.13-h9.md", "11.1.14.md", - "11.1.15.md" + "11.1.15.md", + "11.1.16.md" ], "known": [ "11.1.0.md", @@ -300,6 +318,8 @@ "10.2.7-h18.md", "10.2.7-h19.md", "10.2.7-h34.md", + "10.2.7-h35.md", + "10.2.7-h36.md", "10.2.8.md", "10.2.8-h13.md", "10.2.8-h18.md", @@ -322,6 +342,8 @@ "10.2.10-h30.md", "10.2.10-h31.md", "10.2.10-h36.md", + "10.2.10-h37.md", + "10.2.10-h39.md", "10.2.11.md", "10.2.11-h1.md", "10.2.11-h6.md", @@ -339,6 +361,8 @@ "10.2.13-h16.md", "10.2.13-h18.md", "10.2.13-h21.md", + "10.2.13-h22.md", + "10.2.13-h23.md", "10.2.14-h1.md", "10.2.15.md", "10.2.16.md", @@ -346,10 +370,14 @@ "10.2.16-h4.md", "10.2.16-h6.md", "10.2.16-h7.md", + "10.2.16-h8.md", + "10.2.16-h9.md", "10.2.18.md", "10.2.18-h1.md", "10.2.18-h5.md", - "10.2.18-h6.md" + "10.2.18-h6.md", + "10.2.18-h7.md", + "10.2.18-h8.md" ], "known": [ "10.2.0.md", From c32a984c8e78540c62aca25dd7e4d4c4939dbe4e Mon Sep 17 00:00:00 2001 From: aaronaxvig Date: Sat, 11 Jul 2026 20:30:14 -0500 Subject: [PATCH 2/5] Refactor of tree stuff --- .vscode/settings.json | 3 + test/checkbox-tree.test.mjs | 100 +++++++ web/index.html | 3 +- web/js/tree.js | 342 ++++-------------------- web/lib/checkbox-tree/README.md | 47 ++++ web/lib/checkbox-tree/checkbox-tree.css | 78 ++++++ web/lib/checkbox-tree/checkbox-tree.js | 287 ++++++++++++++++++++ web/styles.css | 81 +----- 8 files changed, 566 insertions(+), 375 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 test/checkbox-tree.test.mjs create mode 100644 web/lib/checkbox-tree/README.md create mode 100644 web/lib/checkbox-tree/checkbox-tree.css create mode 100644 web/lib/checkbox-tree/checkbox-tree.js diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..96d3c81 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.root": "/web" +} diff --git a/test/checkbox-tree.test.mjs b/test/checkbox-tree.test.mjs new file mode 100644 index 0000000..7ad0a04 --- /dev/null +++ b/test/checkbox-tree.test.mjs @@ -0,0 +1,100 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { JSDOM } from 'jsdom'; + +const dom = new JSDOM('', { url: 'https://example.test/' }); +globalThis.window = dom.window; +globalThis.document = dom.window.document; +globalThis.Element = dom.window.Element; +globalThis.localStorage = dom.window.localStorage; + +const { CheckboxTree } = await import('../web/lib/checkbox-tree/checkbox-tree.js'); + +function createTree(options = {}) { + const container = document.createElement('div'); + document.body.replaceChildren(container); + const tree = new CheckboxTree(container, options); + tree.setData([ + { + id: 'parent', + label: 'Parent', + metadata: { kind: 'parent' }, + children: [ + { id: 'first', label: 'First', metadata: { kind: 'leaf' } }, + { id: 'second', label: 'Second', metadata: { kind: 'leaf' } } + ] + } + ]); + return { container, tree }; +} + +function checkbox(container, id) { + return container.querySelector(`.checkbox-tree__checkbox[data-node-id="${id}"]`); +} + +test('selecting a parent selects all descendants and emits selected nodes', () => { + let change; + const { container, tree } = createTree({ onSelectionChange: event => { change = event; } }); + + const parent = checkbox(container, 'parent'); + parent.checked = true; + parent.dispatchEvent(new window.Event('change', { bubbles: true })); + + assert.deepEqual(tree.getSelectedIds(), ['parent', 'first', 'second']); + assert.deepEqual(change.selectedIds, ['parent', 'first', 'second']); + assert.equal(change.selectedNodes[1].metadata.kind, 'leaf'); +}); + +test('partial child selection makes the parent indeterminate', () => { + const { container, tree } = createTree(); + const first = checkbox(container, 'first'); + first.checked = true; + first.dispatchEvent(new window.Event('change', { bubbles: true })); + + const parent = checkbox(container, 'parent'); + assert.equal(parent.checked, false); + assert.equal(parent.indeterminate, true); + assert.deepEqual(tree.getSelectedIds(), ['first']); +}); + +test('persists selection and collapsed branches per configured key', () => { + localStorage.clear(); + const first = createTree({ storageKey: 'example-tree' }); + checkbox(first.container, 'first').click(); + + const toggle = first.container.querySelector('.checkbox-tree__toggle'); + toggle.click(); + assert.equal(toggle.getAttribute('aria-expanded'), 'true'); + + const second = createTree({ storageKey: 'example-tree' }); + assert.deepEqual(second.tree.getSelectedIds(), ['first']); + assert.equal(second.container.querySelector('.checkbox-tree__toggle').getAttribute('aria-expanded'), 'true'); +}); + +test('supports independent instances and removes listeners on destroy', () => { + const firstContainer = document.createElement('div'); + const secondContainer = document.createElement('div'); + document.body.replaceChildren(firstContainer, secondContainer); + const first = new CheckboxTree(firstContainer); + const second = new CheckboxTree(secondContainer); + const data = [{ id: 'node', label: 'Node' }]; + first.setData(data); + second.setData(data); + + checkbox(firstContainer, 'node').click(); + assert.deepEqual(first.getSelectedIds(), ['node']); + assert.deepEqual(second.getSelectedIds(), []); + + first.destroy(); + assert.equal(firstContainer.childElementCount, 0); + assert.equal(firstContainer.classList.contains('checkbox-tree'), false); +}); + +test('rejects duplicate node ids', () => { + const container = document.createElement('div'); + const tree = new CheckboxTree(container); + assert.throws( + () => tree.setData([{ id: 'same', label: 'One' }, { id: 'same', label: 'Two' }]), + /Duplicate CheckboxTree node id/ + ); +}); diff --git a/web/index.html b/web/index.html index 024b8fc..f6a5e36 100644 --- a/web/index.html +++ b/web/index.html @@ -8,6 +8,7 @@ + @@ -55,4 +56,4 @@ - \ No newline at end of file + diff --git a/web/js/tree.js b/web/js/tree.js index c00aed5..141363a 100644 --- a/web/js/tree.js +++ b/web/js/tree.js @@ -1,85 +1,39 @@ +import { CheckboxTree } from '../lib/checkbox-tree/checkbox-tree.js'; import { getSortedKeys } from './sort.js'; -const STORAGE_KEY = 'tree-state'; - -let treeContainer = null; -let onSelectionChangeHandler = null; +const STORAGE_KEY = 'tree-state-v2'; +let productTree = null; export function initializeTree(containerId, onSelectionChange) { - treeContainer = document.getElementById(containerId); - onSelectionChangeHandler = onSelectionChange; -} - -export function renderProductTree(productsData) { - if (!treeContainer) { - throw new Error('Tree is not initialized. Call initializeTree first.'); + const container = document.getElementById(containerId); + if (!container) { + throw new Error(`Tree container not found: ${containerId}`); } - treeContainer.innerHTML = ''; - - getSortedKeys(productsData).forEach(product => { - const productNode = createTreeNode(product, productsData[product], [product]); - treeContainer.appendChild(productNode); + productTree?.destroy(); + productTree = new CheckboxTree(container, { + storageKey: STORAGE_KEY, + initiallyCollapsed: true, + onSelectionChange }); } +export function renderProductTree(productsData) { + requireTree().setData(toTreeNodes(productsData)); +} + export function getCheckedPaths() { - if (!treeContainer) { - return []; - } - - const checkedBoxes = treeContainer.querySelectorAll('input[type="checkbox"]:checked'); - - return Array.from(checkedBoxes) - .map(cb => cb.dataset.path) - .filter(Boolean) - .map(pathText => { - try { - return JSON.parse(pathText); - } catch (error) { - console.error('Invalid checkbox path data:', pathText, error); - return null; - } - }) - .filter(pathArray => Array.isArray(pathArray) && pathArray.length > 0); + return productTree ? productTree.getSelectedNodes().map(node => node.metadata.path) : []; } export function getCheckedFileRefs() { - if (!treeContainer) { - return { - addressedFiles: [], - knownFiles: [] - }; - } - const addressedFiles = []; const knownFiles = []; - const checkedBoxes = treeContainer.querySelectorAll('input[type="checkbox"]:checked[data-has-files="1"]'); - - checkedBoxes.forEach(cb => { - const pathText = cb.dataset.path; - if (!pathText) { - return; - } - - let path; - try { - path = JSON.parse(pathText); - } catch (error) { - console.error('Invalid checkbox path data:', pathText, error); - return; - } - - if (!Array.isArray(path) || path.length === 0) { - return; - } + productTree?.getSelectedNodes().forEach(node => { + const { path, addressed = [], known = [] } = node.metadata; const productKey = path[0]; - - const addressed = parseJsonArray(cb.dataset.addressed); addressed.forEach(fileName => addressedFiles.push({ productKey, fileName })); - - const known = parseJsonArray(cb.dataset.known); known.forEach(fileName => knownFiles.push({ productKey, fileName })); }); @@ -89,255 +43,51 @@ export function getCheckedFileRefs() { }; } -function createTreeNode(name, value, path) { - const container = document.createElement('div'); - container.className = 'tree-item'; +// State restoration now happens when setData renders the generic tree. +export function restoreTreeState() {} - const isObjectValue = value && typeof value === 'object'; - const hasFiles = isObjectValue && ('addressed' in value || 'known' in value); - const childKeys = isObjectValue - ? getSortedKeys(value).filter(key => key !== 'addressed' && key !== 'known') - : []; - const hasChildren = childKeys.length > 0; - - if (hasChildren) { - container.classList.add('parent'); - - const toggle = document.createElement('span'); - toggle.className = 'tree-toggle'; - toggle.textContent = '▼'; - toggle.style.cursor = 'pointer'; - - const label = document.createElement('label'); - const checkbox = createCheckbox(path, hasFiles ? value : undefined); - label.appendChild(checkbox); - label.appendChild(document.createTextNode(name)); - - const childrenContainer = document.createElement('div'); - childrenContainer.className = 'tree-children'; - - childKeys.forEach(childKey => { - const childNode = createTreeNode(childKey, value[childKey], [...path, childKey]); - childrenContainer.appendChild(childNode); - }); - - if (shouldStartCollapsed(value)) { - childrenContainer.classList.add('collapsed'); - toggle.textContent = '▶'; - } - - toggle.addEventListener('click', event => { - event.stopPropagation(); - childrenContainer.classList.toggle('collapsed'); - toggle.textContent = childrenContainer.classList.contains('collapsed') ? '▶' : '▼'; - saveTreeState(); - }); - - container.appendChild(toggle); - container.appendChild(label); - container.appendChild(childrenContainer); - return container; +function requireTree() { + if (!productTree) { + throw new Error('Tree is not initialized. Call initializeTree first.'); } - - const label = document.createElement('label'); - if (hasFiles) { - const checkbox = createCheckbox(path, value); - label.appendChild(checkbox); - } - - label.appendChild(document.createTextNode(name)); - container.appendChild(label); - return container; + return productTree; } -function shouldStartCollapsed(value) { - if (!value || typeof value !== 'object') { - return false; - } +function toTreeNodes(value, path = []) { + return getSortedKeys(value) + .filter(key => key !== 'addressed' && key !== 'known') + .map(key => { + const childValue = value[key]; + const childPath = [...path, key]; + const isObject = childValue && typeof childValue === 'object'; + const hasFiles = isObject && ('addressed' in childValue || 'known' in childValue); + const children = isObject ? toTreeNodes(childValue, childPath) : []; - const childKeys = Object.keys(value).filter(key => key !== 'addressed' && key !== 'known'); - return childKeys.length > 0; + return { + id: JSON.stringify(childPath), + label: key, + selectable: hasFiles || children.length > 0, + metadata: { + path: childPath, + addressed: hasFiles && Array.isArray(childValue.addressed) ? childValue.addressed : [], + known: hasFiles && Array.isArray(childValue.known) ? childValue.known : [] + }, + ...(children.length > 0 ? { children } : {}) + }; + }); } function dedupeFileRefs(fileRefs) { const seen = new Set(); - return fileRefs.filter(ref => { - const productKey = String(ref && ref.productKey ? ref.productKey : ''); - const fileName = String(ref && ref.fileName ? ref.fileName : ''); + return fileRefs.filter(({ productKey, fileName }) => { if (!productKey || !fileName) { return false; } - const key = `${productKey}::${fileName}`; if (seen.has(key)) { return false; } - seen.add(key); return true; }); } - -function createCheckbox(path, value) { - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.value = path.join('/'); - checkbox.dataset.path = JSON.stringify(path); - - if (value && typeof value === 'object' && ('addressed' in value || 'known' in value)) { - checkbox.dataset.hasFiles = '1'; - checkbox.dataset.addressed = JSON.stringify(Array.isArray(value.addressed) ? value.addressed : []); - checkbox.dataset.known = JSON.stringify(Array.isArray(value.known) ? value.known : []); - } - - checkbox.addEventListener('change', event => { - handleCheckboxChange(event); - }); - - return checkbox; -} - -function parseJsonArray(value) { - if (!value) { - return []; - } - - try { - const parsed = JSON.parse(value); - return Array.isArray(parsed) ? parsed : []; - } catch (error) { - console.error('Invalid checkbox file metadata:', value, error); - return []; - } -} - -function handleCheckboxChange(event) { - const checkbox = event.target; - const container = checkbox.closest('.tree-item'); - const childrenContainer = container.querySelector('.tree-children'); - - if (childrenContainer) { - const childCheckboxes = childrenContainer.querySelectorAll('input[type="checkbox"]'); - childCheckboxes.forEach(cb => { - cb.checked = checkbox.checked; - cb.indeterminate = false; - }); - } - - checkbox.indeterminate = false; - updateParentCheckboxes(checkbox); - - if (typeof onSelectionChangeHandler === 'function') { - onSelectionChangeHandler(); - } - saveTreeState(); -} - -function updateParentCheckboxes(checkbox) { - let currentItem = checkbox.closest('.tree-item'); - let parentItem = currentItem.parentElement.closest('.tree-item'); - - while (parentItem) { - const parentLabel = parentItem.querySelector(':scope > label'); - if (!parentLabel) { - parentItem = parentItem.parentElement.closest('.tree-item'); - continue; - } - - const parentCheckbox = parentLabel.querySelector('input[type="checkbox"]'); - if (!parentCheckbox) { - parentItem = parentItem.parentElement.closest('.tree-item'); - continue; - } - - const children = parentItem.querySelectorAll(':scope > .tree-children > .tree-item > label > input[type="checkbox"]'); - const checkedCount = Array.from(children).filter(cb => cb.checked || cb.indeterminate).length; - - if (checkedCount === 0) { - parentCheckbox.checked = false; - parentCheckbox.indeterminate = false; - } else if (checkedCount === children.length) { - parentCheckbox.checked = true; - parentCheckbox.indeterminate = false; - } else { - parentCheckbox.checked = false; - parentCheckbox.indeterminate = true; - } - - currentItem = parentItem; - parentItem = currentItem.parentElement.closest('.tree-item'); - } -} - -function saveTreeState() { - if (!treeContainer) { - return; - } - - const checkedPaths = Array.from( - treeContainer.querySelectorAll('input[type="checkbox"]:checked[data-has-files="1"]') - ).map(cb => cb.dataset.path).filter(Boolean); - - const collapsedPaths = Array.from( - treeContainer.querySelectorAll('.tree-children.collapsed') - ).map(children => { - const cb = children.parentElement.querySelector(':scope > label > input[type="checkbox"]'); - return cb?.dataset.path ?? null; - }).filter(Boolean); - - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify({ checkedPaths, collapsedPaths })); - } catch { - // Storage unavailable or quota exceeded — silently ignore - } -} - -export function restoreTreeState() { - if (!treeContainer) { - return; - } - - let state; - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) { - return; - } - state = JSON.parse(raw); - } catch { - return; - } - - if (!state || typeof state !== 'object') { - return; - } - - if (Array.isArray(state.checkedPaths) && state.checkedPaths.length > 0) { - const checkedSet = new Set(state.checkedPaths); - treeContainer.querySelectorAll('input[type="checkbox"][data-has-files="1"]').forEach(cb => { - cb.checked = checkedSet.has(cb.dataset.path); - }); - treeContainer.querySelectorAll('input[type="checkbox"][data-has-files="1"]:checked').forEach(cb => { - updateParentCheckboxes(cb); - }); - } - - if (Array.isArray(state.collapsedPaths)) { - const collapsedSet = new Set(state.collapsedPaths); - treeContainer.querySelectorAll('.tree-children').forEach(childrenDiv => { - const cb = childrenDiv.parentElement.querySelector(':scope > label > input[type="checkbox"]'); - if (!cb) { - return; - } - const toggle = childrenDiv.parentElement.querySelector(':scope > .tree-toggle'); - const shouldBeCollapsed = collapsedSet.has(cb.dataset.path); - const isCollapsed = childrenDiv.classList.contains('collapsed'); - if (shouldBeCollapsed !== isCollapsed) { - childrenDiv.classList.toggle('collapsed'); - if (toggle) { - toggle.textContent = shouldBeCollapsed ? '▶' : '▼'; - } - } - }); - } -} diff --git a/web/lib/checkbox-tree/README.md b/web/lib/checkbox-tree/README.md new file mode 100644 index 0000000..441aaf1 --- /dev/null +++ b/web/lib/checkbox-tree/README.md @@ -0,0 +1,47 @@ +# CheckboxTree + +A dependency-free, instance-based checkbox tree for browser ES modules. + +## Usage + +```html + +
+ +``` + +Every node requires unique string `id` and `label` properties. Nodes may also +have `children`, application-owned `metadata`, and `selectable` or `disabled` +flags. + +## API + +- `new CheckboxTree(container, options)` creates an independent instance. +- `setData(nodes)` validates and renders nodes, then restores saved state. +- `getSelectedIds()` returns selected node IDs. +- `getSelectedNodes()` returns selected source node objects. +- `setSelectedIds(ids, { notify })` replaces the selection. +- `restoreState()` restores selection and expansion state. +- `destroy()` removes listeners, markup, and the root CSS class. +- `serializeStateToFragment(manifest)` returns `#v=1&c=...&x=...` state. +- `restoreStateFromLocation(manifest, location)` safely applies URL state. +- `validateManifest(manifest)` reports unregistered and missing paths. + +Options: + +- `initiallyCollapsed` defaults to `true`. +- `initiallySelected` defaults to `false`. +- `storageKey` defaults to `null`, which disables persistence. +- `onSelectionChange` receives `{ selectedIds, selectedNodes }`. +- `manifest` optionally supplies a parsed manifest and automatically restores + state from `window.location.hash` after `setData()`. + +## Shareable URL state + +Create a JSON manifest whose array positions are permanent slots. Canonical node +paths join ancestor IDs with `>`: + +```json +{ "schema": 1, "nodes": ["fruit", "fruit>apple", "fruit>pear", null] } +``` + +Import `loadManifest`, pass its result as the `manifest` option, and call +`tree.serializeStateToFragment()` when creating a share link. Existing entries +must never be reordered. Append new paths, replace removed paths with `null`, and +edit a path in place for a logical rename or move. Node IDs therefore cannot +contain `>`. + +The fragment adaptively encodes checked (`c`) and expanded (`x`) state using the +shortest of a dense bitset, sparse enabled slots, or sparse disabled slots. +Sparse slot numbers are delta-encoded variable-length integers. A coverage value +in the disabled-slot form ensures nodes appended later still default to unchecked +and collapsed. Malformed or unsupported state is ignored. +The helpers `encodeTreeState`, `decodeTreeState`, `applyTreeState`, and +`restoreStateFromLocation` are also exported for custom integrations. +Use `validateManifestEvolution(previous, next)` in a build check to reject +removed slots and tombstone reuse; path changes are reported for deliberate +rename/move review. + +Styling is namespaced under `.checkbox-tree`. Override the custom properties +`--checkbox-tree-indent`, `--checkbox-tree-toggle-size`, and +`--checkbox-tree-hover-color` in the consuming application. + +## Development + +```sh +npm install +npm test +npm run pack:check +``` + +## Future demo ideas + +- Add population metadata to the country, state, and county nodes and display the + total population represented by the current selection. Define the aggregation + rule carefully so checked ancestors and their checked descendants are not + counted twice; one option is to total only the most specific checked nodes. diff --git a/web/vendor/checkbox-tree/checkbox-tree.js b/web/vendor/checkbox-tree/checkbox-tree.js index c470d48..4974fc6 100644 --- a/web/vendor/checkbox-tree/checkbox-tree.js +++ b/web/vendor/checkbox-tree/checkbox-tree.js @@ -2,9 +2,310 @@ const DEFAULT_OPTIONS = { initiallyCollapsed: true, initiallySelected: false, storageKey: null, - onSelectionChange: null + onSelectionChange: null, + manifest: null }; +const SERIALIZATION_VERSION = 1; +const PATH_DELIMITER = '>'; + +export function loadManifest(source) { + let records; + let schema = 1; + + if (typeof source === 'string') { + records = source.split(/\r?\n/) + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')) + .map(line => line === '!deleted' ? null : line); + } else if (source && typeof source === 'object' && Array.isArray(source.nodes)) { + schema = source.schema; + records = source.nodes; + } else if (source && Array.isArray(source.slots)) { + return normalizeManifest(source.schema, source.slots.map(slot => slot?.deleted ? null : slot?.path)); + } else { + throw new TypeError('CheckboxTree manifest must be text or an object with a nodes array.'); + } + + return normalizeManifest(schema, records); +} + +function normalizeManifest(schema, records) { + if (schema !== 1) { + throw new Error(`Unsupported CheckboxTree manifest schema: ${schema}`); + } + + const pathToSlot = new Map(); + const slots = records.map((record, index) => { + if (record === null || record === '!deleted') { + return { path: null, deleted: true }; + } + if (typeof record !== 'string' || !record || record.trim() !== record) { + throw new TypeError(`Invalid CheckboxTree manifest entry at slot ${index}.`); + } + if (pathToSlot.has(record)) { + throw new Error(`Duplicate CheckboxTree manifest path: ${record}`); + } + pathToSlot.set(record, index); + return { path: record, deleted: false }; + }); + + return { schema: 1, slots, pathToSlot }; +} + +export function setBit(bytes, bitIndex, value) { + const byteIndex = Math.floor(bitIndex / 8); + const mask = 1 << (bitIndex % 8); + if (value) bytes[byteIndex] |= mask; + else bytes[byteIndex] &= ~mask; +} + +export function getBit(bytes, bitIndex) { + const byteIndex = Math.floor(bitIndex / 8); + return byteIndex < bytes.length && (bytes[byteIndex] & (1 << (bitIndex % 8))) !== 0; +} + +export function bytesToBase64Url(bytes) { + let binary = ''; + for (const byte of bytes) binary += String.fromCharCode(byte); + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); +} + +export function base64UrlToBytes(value, maximumLength = Infinity) { + if (typeof value !== 'string' || !/^[A-Za-z0-9_-]*$/.test(value) || value.length % 4 === 1) { + throw new Error('Invalid Base64URL value.'); + } + if (Number.isFinite(maximumLength) && value.length > Math.ceil(maximumLength * 4 / 3) + 2) { + throw new Error('Encoded tree state exceeds the manifest size.'); + } + const base64 = value.replace(/-/g, '+').replace(/_/g, '/'); + const binary = atob(base64 + '='.repeat((4 - base64.length % 4) % 4)); + if (binary.length > maximumLength) throw new Error('Encoded tree state exceeds the manifest size.'); + return Uint8Array.from(binary, character => character.charCodeAt(0)); +} + +function trimTrailingZeroBytes(bytes) { + let end = bytes.length; + while (end && bytes[end - 1] === 0) end--; + return bytes.slice(0, end); +} + +function encodeUnsignedVarint(value) { + const bytes = []; + do { + let byte = value & 0x7f; + value = Math.floor(value / 128); + if (value) byte |= 0x80; + bytes.push(byte); + } while (value); + return bytes; +} + +function decodeUnsignedVarint(bytes, offset) { + let value = 0; + let multiplier = 1; + for (let index = offset; index < bytes.length && index < offset + 8; index++) { + const byte = bytes[index]; + value += (byte & 0x7f) * multiplier; + if ((byte & 0x80) === 0) return { value, offset: index + 1 }; + multiplier *= 128; + if (!Number.isSafeInteger(value) || !Number.isSafeInteger(multiplier)) break; + } + throw new Error('Invalid adaptive tree-state integer.'); +} + +function encodeSparseIndexes(indexes) { + const bytes = []; + let previous = -1; + for (const index of indexes) { + bytes.push(...encodeUnsignedVarint(index - previous - 1)); + previous = index; + } + return bytes; +} + +function decodeSparseIndexes(bytes, offset, limit) { + const indexes = new Set(); + let previous = -1; + while (offset < bytes.length) { + const decoded = decodeUnsignedVarint(bytes, offset); + const index = previous + decoded.value + 1; + if (index >= limit) throw new Error('Sparse tree state contains an out-of-range slot.'); + indexes.add(index); + previous = index; + offset = decoded.offset; + } + return indexes; +} + +export function encodeAdaptiveBitset(values, slotCount) { + const enabled = []; + const disabled = []; + const denseBytes = new Uint8Array(Math.ceil(slotCount / 8)); + for (const [slot, value] of values) { + (value ? enabled : disabled).push(slot); + setBit(denseBytes, slot, value); + } + enabled.sort((left, right) => left - right); + disabled.sort((left, right) => left - right); + if (enabled.length === 0) return new Uint8Array(); + + const candidates = [ + Uint8Array.from([0, ...trimTrailingZeroBytes(denseBytes)]), + Uint8Array.from([1, ...encodeSparseIndexes(enabled)]), + Uint8Array.from([2, ...encodeUnsignedVarint(slotCount), ...encodeSparseIndexes(disabled)]) + ]; + return candidates.reduce((shortest, candidate) => + candidate.length < shortest.length ? candidate : shortest + ); +} + +export function decodeAdaptiveBitset(bytes, slotCount) { + if (bytes.length === 0) return () => false; + const mode = bytes[0]; + if (mode === 0) { + const dense = bytes.slice(1); + if (dense.length > Math.ceil(slotCount / 8)) { + throw new Error('Dense tree state exceeds the manifest size.'); + } + return slot => getBit(dense, slot); + } + if (mode === 1) { + const enabled = decodeSparseIndexes(bytes, 1, slotCount); + return slot => enabled.has(slot); + } + if (mode === 2) { + const coverage = decodeUnsignedVarint(bytes, 1); + if (coverage.value > slotCount) throw new Error('Tree-state coverage exceeds the manifest size.'); + const disabled = decodeSparseIndexes(bytes, coverage.offset, coverage.value); + return slot => slot < coverage.value && !disabled.has(slot); + } + throw new Error('Unknown adaptive tree-state mode.'); +} + +function asManifest(manifest) { + return manifest?.pathToSlot instanceof Map ? manifest : loadManifest(manifest); +} + +export function validateManifest(manifest, tree) { + const normalized = asManifest(manifest); + const treePaths = new Set(tree.nodesByPath.keys()); + return { + valid: Array.from(treePaths).every(path => normalized.pathToSlot.has(path)), + unregisteredPaths: Array.from(treePaths).filter(path => !normalized.pathToSlot.has(path)), + missingPaths: normalized.slots + .filter(slot => !slot.deleted && !treePaths.has(slot.path)) + .map(slot => slot.path) + }; +} + +export function validateManifestEvolution(previous, next) { + const oldManifest = asManifest(previous); + const newManifest = asManifest(next); + const errors = []; + + if (newManifest.slots.length < oldManifest.slots.length) { + errors.push('Manifest slots cannot be removed; use tombstones.'); + } + oldManifest.slots.forEach((oldSlot, index) => { + const newSlot = newManifest.slots[index]; + if (!newSlot) return; + if (oldSlot.deleted && !newSlot.deleted) { + errors.push(`Tombstoned slot ${index} cannot be reused.`); + } + if (!oldSlot.deleted && !newSlot.deleted && oldSlot.path !== newSlot.path) { + errors.push(`Slot ${index} changed path; confirm this is a logical rename or move.`); + } + }); + + return { valid: errors.length === 0, errors }; +} + +export function encodeTreeState(tree, manifest) { + const normalized = asManifest(manifest); + const checkedValues = new Map(); + const expandedValues = new Map(); + + for (const [path, node] of tree.nodesByPath) { + const slot = normalized.pathToSlot.get(path); + if (slot === undefined) continue; + const item = tree.itemForNode(node.id); + const checkbox = item && tree.directCheckbox(item); + const toggle = item?.querySelector(':scope > .checkbox-tree__row > .checkbox-tree__toggle'); + checkedValues.set(slot, Boolean(checkbox?.checked)); + expandedValues.set(slot, toggle?.getAttribute('aria-expanded') === 'true'); + } + + const checkedValue = bytesToBase64Url(encodeAdaptiveBitset(checkedValues, normalized.slots.length)); + const expandedValue = bytesToBase64Url(encodeAdaptiveBitset(expandedValues, normalized.slots.length)); + return { version: SERIALIZATION_VERSION, checked: checkedValue || null, expanded: expandedValue || null }; +} + +export function decodeTreeState(fragment, manifest) { + const normalized = asManifest(manifest); + const value = fragment.startsWith('#') ? fragment.slice(1) : fragment; + const params = new URLSearchParams(value); + if (params.get('v') !== String(SERIALIZATION_VERSION)) { + return { applied: false, version: params.get('v'), warnings: ['Missing or unsupported serialization version.'] }; + } + const maximumLength = Math.max( + Math.ceil(normalized.slots.length / 8) + 1, + normalized.slots.length * 2 + 10 + ); + try { + const checked = base64UrlToBytes(params.get('c') ?? '', maximumLength); + const expanded = base64UrlToBytes(params.get('x') ?? '', maximumLength); + decodeAdaptiveBitset(checked, normalized.slots.length); + decodeAdaptiveBitset(expanded, normalized.slots.length); + return { + applied: true, + version: SERIALIZATION_VERSION, + checked, + expanded, + warnings: [] + }; + } catch (error) { + return { applied: false, version: SERIALIZATION_VERSION, warnings: [error.message] }; + } +} + +export function applyTreeState(tree, manifest, state) { + if (!state?.applied) return false; + const normalized = asManifest(manifest); + let isChecked; + let isExpanded; + try { + isChecked = decodeAdaptiveBitset(state.checked, normalized.slots.length); + isExpanded = decodeAdaptiveBitset(state.expanded, normalized.slots.length); + } catch { + return false; + } + for (const [path, node] of tree.nodesByPath) { + const slot = normalized.pathToSlot.get(path); + if (slot === undefined) continue; + const item = tree.itemForNode(node.id); + const checkbox = item && tree.directCheckbox(item); + if (checkbox && !checkbox.disabled) checkbox.checked = isChecked(slot); + tree.setExpanded(item, isExpanded(slot)); + } + tree.updateAllParentCheckboxes(); + return true; +} + +export function serializeStateToFragment(tree, manifest) { + const state = encodeTreeState(tree, manifest); + const params = new URLSearchParams({ v: String(state.version) }); + if (state.checked) params.set('c', state.checked); + if (state.expanded) params.set('x', state.expanded); + return `#${params}`; +} + +export function restoreStateFromLocation(tree, manifest, location = window.location) { + const state = decodeTreeState(location.hash ?? '', manifest); + if (state.applied) applyTreeState(tree, manifest, state); + return state; +} + export class CheckboxTree { constructor(container, options = {}) { if (!(container instanceof Element)) { @@ -14,6 +315,7 @@ export class CheckboxTree { this.container = container; this.options = { ...DEFAULT_OPTIONS, ...options }; this.nodesById = new Map(); + this.nodesByPath = new Map(); this.rootNodes = []; this.handleChange = this.handleChange.bind(this); this.handleClick = this.handleClick.bind(this); @@ -29,11 +331,13 @@ export class CheckboxTree { } this.nodesById.clear(); + this.nodesByPath.clear(); this.rootNodes = nodes; this.indexNodes(nodes); this.render(); this.updateAllParentCheckboxes(); this.restoreState(); + if (this.options.manifest) this.restoreStateFromLocation(); } getSelectedIds() { @@ -66,9 +370,10 @@ export class CheckboxTree { this.container.classList.remove('checkbox-tree'); this.container.replaceChildren(); this.nodesById.clear(); + this.nodesByPath.clear(); } - indexNodes(nodes) { + indexNodes(nodes, parentPath = '') { nodes.forEach(node => { if (!node || typeof node.id !== 'string' || !node.id || typeof node.label !== 'string') { throw new TypeError('Each CheckboxTree node requires non-empty string id and label properties.'); @@ -77,12 +382,18 @@ export class CheckboxTree { throw new Error(`Duplicate CheckboxTree node id: ${node.id}`); } + if (node.id.includes(PATH_DELIMITER)) { + throw new Error(`CheckboxTree node id cannot contain "${PATH_DELIMITER}": ${node.id}`); + } + this.nodesById.set(node.id, node); + const path = parentPath ? `${parentPath}${PATH_DELIMITER}${node.id}` : node.id; + this.nodesByPath.set(path, node); if (node.children !== undefined) { if (!Array.isArray(node.children)) { throw new TypeError(`CheckboxTree node children must be an array: ${node.id}`); } - this.indexNodes(node.children); + this.indexNodes(node.children, path); } }); } @@ -191,6 +502,7 @@ export class CheckboxTree { } setExpanded(item, expanded) { + if (!item) return; const toggle = item.querySelector(':scope > .checkbox-tree__row > .checkbox-tree__toggle'); const children = this.directChildrenContainer(item); if (!toggle || !children) { @@ -211,6 +523,11 @@ export class CheckboxTree { return item.querySelector(':scope > .checkbox-tree__row .checkbox-tree__checkbox'); } + itemForNode(id) { + return Array.from(this.container.querySelectorAll('.checkbox-tree__item')) + .find(item => item.dataset.nodeId === id) ?? null; + } + updateAncestorCheckboxes(item) { let parentItem = item.parentElement?.closest('.checkbox-tree__item'); while (parentItem && this.container.contains(parentItem)) { @@ -286,4 +603,16 @@ export class CheckboxTree { }); } } + + validateManifest(manifest = this.options.manifest) { + return validateManifest(manifest, this); + } + + serializeStateToFragment(manifest = this.options.manifest) { + return serializeStateToFragment(this, manifest); + } + + restoreStateFromLocation(manifest = this.options.manifest, location = window.location) { + return restoreStateFromLocation(this, manifest, location); + } } From 9e7fbe08658b5b20f6b15c8e88d4c7b45338122d Mon Sep 17 00:00:00 2001 From: aaronaxvig Date: Sun, 12 Jul 2026 21:45:55 -0500 Subject: [PATCH 5/5] Revised checkbox-tree functions and related --- README.md | 3 +- package.json | 3 + scripts/generate-rendered-fixtures.mjs | 57 +-- scripts/refresh-generated-data.mjs | 7 + scripts/update-products-from-issues.mjs | 202 +++++++++++ scripts/update_products_from_issues.py | 217 ----------- .../expected-parsed.json | 30 ++ test/update-products-from-issues.test.mjs | 70 ++++ web/js/product-tree-model.js | 144 ++++++++ web/js/script.js | 149 +------- web/js/tree.js | 27 +- web/vendor/checkbox-tree/README.md | 16 +- web/vendor/checkbox-tree/checkbox-tree.js | 343 ++---------------- web/vendor/checkbox-tree/state-codec.js | 206 +++++++++++ web/vendor/checkbox-tree/tree-state.js | 310 ++++++++++++++++ 15 files changed, 1063 insertions(+), 721 deletions(-) create mode 100644 scripts/refresh-generated-data.mjs create mode 100644 scripts/update-products-from-issues.mjs delete mode 100644 scripts/update_products_from_issues.py create mode 100644 test/fixtures/PAN-OS-12.1.2-addressed/expected-parsed.json create mode 100644 test/update-products-from-issues.test.mjs create mode 100644 web/js/product-tree-model.js create mode 100644 web/vendor/checkbox-tree/state-codec.js create mode 100644 web/vendor/checkbox-tree/tree-state.js diff --git a/README.md b/README.md index 66b92dd..7170d8d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,8 @@ Known and addressed issues for newly released versions are easily added using th - Copy the issue table's HTML from the webpage using devtools. - Fill out the process.html page's fields and paste in the table HTML. - Download the Markdown file and put it into the correct folder. -- Fix up the products.json file manually or run `update_products_from_issues.py`. +- Run `npm run update:generated` to update `products.json`, append new stable + product-tree manifest slots, and regenerate rendered test fixtures. - Submit a pull request. There is intentionally no automated scaping of Palo Alto's website, to avoid abuse of server resources. Also releases are not that frequent. A crawler to grab some data from the Common Crawl dataset was started but never really finished. diff --git a/package.json b/package.json index daa860a..4008ba1 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,9 @@ "type": "module", "scripts": { "test": "node --test", + "update:products": "node scripts/update-products-from-issues.mjs", + "update:fixtures": "node scripts/generate-rendered-fixtures.mjs", + "update:generated": "node scripts/refresh-generated-data.mjs", "vendor:marked": "mkdir -p web/vendor && cp node_modules/marked/lib/marked.esm.js web/vendor/marked.esm.js" }, "devDependencies": { diff --git a/scripts/generate-rendered-fixtures.mjs b/scripts/generate-rendered-fixtures.mjs index 82d9cda..75b2243 100644 --- a/scripts/generate-rendered-fixtures.mjs +++ b/scripts/generate-rendered-fixtures.mjs @@ -1,11 +1,11 @@ /** * Generates expected-parsed.json for each fixture directory that contains expected.md. - * Run manually: node test/generate-rendered-fixtures.mjs + * Run manually: node scripts/generate-rendered-fixtures.mjs */ import { readFileSync, writeFileSync, readdirSync } from 'node:fs'; -import { join } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { join, resolve } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; import { JSDOM } from 'jsdom'; const { window } = new JSDOM(''); @@ -20,31 +20,38 @@ globalThis.fetch = () => Promise.resolve({ const { parseMarkdownIssues, markdownSummaryToHtml } = await import('../web/js/issues.js'); const __dirname = fileURLToPath(new URL('.', import.meta.url)); -const fixturesDir = join(__dirname, '../test/fixtures'); +const defaultFixturesDir = join(__dirname, '../test/fixtures'); -const fixtures = readdirSync(fixturesDir).filter(name => { - const contents = readdirSync(join(fixturesDir, name)).map(d => d); - return contents.includes('expected.md'); -}); +export function generateRenderedFixtures(fixturesDir = defaultFixturesDir) { + const fixtures = readdirSync(fixturesDir).filter(name => { + const contents = readdirSync(join(fixturesDir, name)).map(d => d); + return contents.includes('expected.md'); + }); -for (const fixtureId of fixtures) { - const fixturePath = join(fixturesDir, fixtureId); - const markdownText = readFileSync(join(fixturePath, 'expected.md'), 'utf-8'); - const parsed = parseMarkdownIssues(markdownText); + for (const fixtureId of fixtures) { + const fixturePath = join(fixturesDir, fixtureId); + const markdownText = readFileSync(join(fixturePath, 'expected.md'), 'utf-8'); + const parsed = parseMarkdownIssues(markdownText); - const result = parsed.map(issue => ({ - id: issue.id, - summary: issue.summary, - resolved: issue.resolved, - caveat: issue.caveat, - renderedHtml: markdownSummaryToHtml(issue.summary, issue.resolved, issue.caveat) - })); + const result = parsed.map(issue => ({ + id: issue.id, + summary: issue.summary, + resolved: issue.resolved, + caveat: issue.caveat, + renderedHtml: markdownSummaryToHtml(issue.summary, issue.resolved, issue.caveat) + })); - writeFileSync( - join(fixturePath, 'expected-parsed.json'), - JSON.stringify(result, null, 2) + '\n', - 'utf-8' - ); + writeFileSync( + join(fixturePath, 'expected-parsed.json'), + JSON.stringify(result, null, 2) + '\n', + 'utf-8' + ); - console.log(`Written ${fixtureId}/expected-parsed.json (${result.length} issues)`); + console.log(`Written ${fixtureId}/expected-parsed.json (${result.length} issues)`); + } + return { fixtures: fixtures.length }; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + generateRenderedFixtures(); } diff --git a/scripts/refresh-generated-data.mjs b/scripts/refresh-generated-data.mjs new file mode 100644 index 0000000..eebe1e1 --- /dev/null +++ b/scripts/refresh-generated-data.mjs @@ -0,0 +1,7 @@ +#!/usr/bin/env node +import { runUpdateProductsCli } from './update-products-from-issues.mjs'; +import { generateRenderedFixtures } from './generate-rendered-fixtures.mjs'; + +await runUpdateProductsCli(); +const fixtureResult = generateRenderedFixtures(); +console.log(`Regenerated ${fixtureResult.fixtures} rendered fixture set(s).`); diff --git a/scripts/update-products-from-issues.mjs b/scripts/update-products-from-issues.mjs new file mode 100644 index 0000000..87e0aff --- /dev/null +++ b/scripts/update-products-from-issues.mjs @@ -0,0 +1,202 @@ +#!/usr/bin/env node +import { readdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname, join, relative, resolve } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import { buildProductTree, enumerateTreePaths } from '../web/js/product-tree-model.js'; + +const ISSUE_TYPES = new Set(['addressed', 'known']); +const VERSION_FILENAME_RE = /^(?[^.]+(?:\.[^.]+)*)\.md$/; +const HOTFIX_RE = /^(?\d+(?:\.\d+)*)(?:-h(?\d+))?$/; +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..'); + +async function walkFiles(root) { + const files = []; + async function visit(directory) { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = join(directory, entry.name); + if (entry.isDirectory()) await visit(path); + else if (entry.isFile()) files.push(path); + } + } + await visit(root); + return files.sort(); +} + +export async function collectIssueFiles(issuesDir) { + const records = []; + for (const filePath of await walkFiles(issuesDir)) { + const parts = relative(issuesDir, filePath).split(/[\\/]/); + if (parts.length !== 3) continue; + const [product, issueType, filename] = parts; + if (!ISSUE_TYPES.has(issueType)) continue; + const match = VERSION_FILENAME_RE.exec(filename); + if (!match) continue; + records.push({ product, issueType, version: match.groups.version, filename }); + } + return records; +} + +function clearLeafIssueArrays(node) { + if (!node || typeof node !== 'object' || Array.isArray(node)) return; + if ('addressed' in node || 'known' in node) { + node.addressed = []; + node.known = []; + return; + } + Object.values(node).forEach(clearLeafIssueArrays); +} + +function getPath(data, path) { + let current = data; + for (const key of path) { + if (!current || typeof current !== 'object' || !(key in current)) return null; + current = current[key]; + } + return current; +} + +function isIssueLeaf(node) { + return Boolean(node) && typeof node === 'object' && ('addressed' in node || 'known' in node); +} + +function pickTargetLeafPath(products, product, version) { + const parts = version.split('.'); + const major = parts[0]; + const minor = parts.length > 1 ? `${parts[0]}.${parts[1]}` : major; + for (const path of [[product, major, minor], [product, major], [product]]) { + if (isIssueLeaf(getPath(products, path))) return path; + } + return null; +} + +function parseReleasePrefix(prefix) { + const match = HOTFIX_RE.exec(prefix); + if (!match) return { base: [], hotfix: -1 }; + return { + base: match.groups.base.split('.').map(Number), + hotfix: match.groups.hotfix === undefined ? -1 : Number(match.groups.hotfix) + }; +} + +function compareNumberArrays(left, right) { + const length = Math.max(left.length, right.length); + for (let index = 0; index < length; index++) { + const difference = (left[index] ?? -1) - (right[index] ?? -1); + if (difference) return difference; + } + return 0; +} + +function compareIssueFilenames(left, right) { + const leftPrefix = left.replace(/\.[^.]+$/, ''); + const rightPrefix = right.replace(/\.[^.]+$/, ''); + const leftVersion = parseReleasePrefix(leftPrefix); + const rightVersion = parseReleasePrefix(rightPrefix); + return compareNumberArrays(leftVersion.base, rightVersion.base) || + Number(leftVersion.hotfix >= 0) - Number(rightVersion.hotfix >= 0) || + leftVersion.hotfix - rightVersion.hotfix || + left.localeCompare(right); +} + +export function rebuildProducts(products, records) { + clearLeafIssueArrays(products); + const unmatched = []; + records.sort((left, right) => + left.product.localeCompare(right.product) || + left.issueType.localeCompare(right.issueType) || + left.version.localeCompare(right.version) + ); + + for (const record of records) { + const path = pickTargetLeafPath(products, record.product, record.version); + if (!path) { + unmatched.push(record); + continue; + } + const leaf = getPath(products, path); + const files = Array.isArray(leaf[record.issueType]) ? leaf[record.issueType] : []; + leaf[record.issueType] = Array.from(new Set([...files, record.filename])).sort(compareIssueFilenames); + } + return { products, unmatched }; +} + +export function updateManifest(manifest, currentPaths) { + if (!manifest || manifest.schema !== 1 || !Array.isArray(manifest.nodes)) { + throw new Error('Product-tree manifest must use schema 1 with a nodes array.'); + } + const active = new Set(); + manifest.nodes.forEach((path, slot) => { + if (path === null) return; + if (typeof path !== 'string' || !path) throw new Error(`Invalid manifest entry at slot ${slot}.`); + if (active.has(path)) throw new Error(`Duplicate manifest path: ${path}`); + active.add(path); + }); + + const current = new Set(currentPaths); + const appended = currentPaths.filter(path => !active.has(path)); + manifest.nodes.push(...appended); + const missing = Array.from(active).filter(path => !current.has(path)); + return { manifest, appended, missing }; +} + +export async function updateProductsFromIssues({ issuesDir, productsPath, manifestPath }) { + const [productsText, manifestText, records] = await Promise.all([ + readFile(productsPath, 'utf8'), + readFile(manifestPath, 'utf8'), + collectIssueFiles(issuesDir) + ]); + if (records.length === 0) { + throw new Error("No '.md' issue files found; refusing to rewrite products.json"); + } + const products = JSON.parse(productsText); + if (!products || typeof products !== 'object' || Array.isArray(products)) { + throw new Error('products.json must contain a top-level object.'); + } + + const rebuilt = rebuildProducts(products, records); + const paths = enumerateTreePaths(buildProductTree(rebuilt.products)); + const manifestUpdate = updateManifest(JSON.parse(manifestText), paths); + await writeFile(productsPath, `${JSON.stringify(rebuilt.products, null, 2)}\n`, 'utf8'); + if (manifestUpdate.appended.length > 0) { + await writeFile(manifestPath, `${JSON.stringify(manifestUpdate.manifest, null, 2)}\n`, 'utf8'); + } + return { + records: records.length, + unmatched: rebuilt.unmatched, + appended: manifestUpdate.appended, + missing: manifestUpdate.missing + }; +} + +function parseArgs(argv) { + const options = { + issuesDir: join(REPO_ROOT, 'web/data/issues'), + productsPath: join(REPO_ROOT, 'web/data/products.json'), + manifestPath: join(REPO_ROOT, 'web/data/product-tree-manifest.json') + }; + const flags = new Map([ + ['--issues-dir', 'issuesDir'], + ['--products-json', 'productsPath'], + ['--manifest-json', 'manifestPath'] + ]); + for (let index = 0; index < argv.length; index += 2) { + const property = flags.get(argv[index]); + if (!property || argv[index + 1] === undefined) throw new Error(`Invalid argument: ${argv[index] ?? ''}`); + options[property] = resolve(argv[index + 1]); + } + return options; +} + +export async function runUpdateProductsCli(argv = process.argv.slice(2)) { + const options = parseArgs(argv); + const result = await updateProductsFromIssues(options); + console.log(`Updated ${options.productsPath} from ${result.records} issue files.`); + console.log(`Appended ${result.appended.length} manifest path(s).`); + if (result.missing.length) console.warn(`${result.missing.length} manifest path(s) are no longer rendered; slots were preserved.`); + if (result.unmatched.length) console.warn(`${result.unmatched.length} issue file(s) did not match a products.json leaf.`); + return result; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + await runUpdateProductsCli(); +} diff --git a/scripts/update_products_from_issues.py b/scripts/update_products_from_issues.py deleted file mode 100644 index 754c286..0000000 --- a/scripts/update_products_from_issues.py +++ /dev/null @@ -1,217 +0,0 @@ -#!/usr/bin/env python3 -"""Update products.json entries from discovered issue data files.""" - -from __future__ import annotations - -import argparse -import json -import re -from pathlib import Path -from typing import Any - - -ISSUE_TYPES = {"addressed", "known"} -VERSION_FILENAME_RE = re.compile(r"^(?P[^.]+(?:\.[^.]+)*)\.md$") -HOTFIX_RE = re.compile(r"^(?P\d+(?:\.\d+)*)(?:-h(?P\d+))?$") - - -def parse_args() -> argparse.Namespace: - """Parse command line arguments for input and output paths.""" - - parser = argparse.ArgumentParser( - description="Update products.json from issue files." - ) - parser.add_argument( - "--issues-dir", - default="web/data/issues", - help="Directory containing product issue files.", - ) - parser.add_argument( - "--products-json", - default="web/data/products.json", - help="Path to products.json.", - ) - return parser.parse_args() - - -def collect_issue_files(issues_dir: Path) -> list[tuple[str, str, str, str]]: - """Collect all issue files with '.md' naming.""" - - records: list[tuple[str, str, str, str]] = [] - - for file_path in sorted(p for p in issues_dir.rglob("*") if p.is_file()): - relative = file_path.relative_to(issues_dir) - if len(relative.parts) != 3: - continue - - product, issue_type, filename = relative.parts - if issue_type not in ISSUE_TYPES: - continue - - version = parse_issue_filename(filename) - if version is None: - continue - - records.append((product, issue_type, version, filename)) - - return records - - -def parse_issue_filename(filename: str) -> str | None: - """Parse '.md' issue filenames and return version.""" - - match = VERSION_FILENAME_RE.match(filename) - if not match: - return None - - return match.group("version") - - -def clear_leaf_issue_arrays(node: Any) -> None: - """Reset addressed/known arrays on every issue leaf in products.json.""" - - if isinstance(node, dict): - if "addressed" in node or "known" in node: - node["addressed"] = [] - node["known"] = [] - return - - for value in node.values(): - clear_leaf_issue_arrays(value) - - -def is_issue_leaf(node: Any) -> bool: - """Return whether a node is a leaf containing issue arrays.""" - - return isinstance(node, dict) and ("addressed" in node or "known" in node) - - -def pick_target_leaf_path(products: dict[str, Any], product: str, version: str) -> list[str] | None: - """Choose the best matching product tree path for a version.""" - - parts = version.split(".") - if not parts: - return None - - major = parts[0] - minor = f"{parts[0]}.{parts[1]}" if len(parts) > 1 else parts[0] - - candidates = [ - [product, major, minor], - [product, major], - [product], - ] - - for path in candidates: - node = get_path(products, path) - if is_issue_leaf(node): - return path - - return None - - -def get_path(data: Any, path: list[str]) -> Any: - """Traverse nested dict keys and return the value at path, if present.""" - - current = data - for key in path: - if not isinstance(current, dict) or key not in current: - return None - current = current[key] - return current - - -def parse_release_prefix(prefix: str) -> tuple[tuple[int, ...], int]: - """Extract numeric base version parts and optional hotfix number.""" - - match = HOTFIX_RE.match(prefix) - if not match: - return tuple(), -1 - - base_parts = tuple(int(part) for part in match.group("base").split(".")) - hotfix_group = match.group("hotfix") - hotfix = int(hotfix_group) if hotfix_group is not None else -1 - return base_parts, hotfix - - -def filename_sort_key(filename: str) -> tuple[tuple[int, ...], int, int, str]: - """Build a sort key that orders base versions before hotfixes.""" - - prefix = filename.rsplit(".", 1)[0] - base_parts, hotfix = parse_release_prefix(prefix) - is_hotfix = 1 if hotfix >= 0 else 0 - return base_parts, is_hotfix, hotfix, filename - - -def add_file_to_leaf( - products: dict[str, Any], - path: list[str], - issue_type: str, - filename: str, -) -> None: - """Insert an issue filename into the target leaf with stable sorting.""" - - leaf = get_path(products, path) - if not isinstance(leaf, dict): - return - - files = leaf.get(issue_type) - if not isinstance(files, list): - files = [] - - files.append(filename) - leaf[issue_type] = sorted(set(files), key=filename_sort_key) - - -def update_products(issues_dir: Path, products_path: Path) -> None: - """Rebuild product issue file lists from crawled issue files.""" - - if not issues_dir.is_dir(): - raise FileNotFoundError(f"Issues directory not found: {issues_dir}") - if not products_path.is_file(): - raise FileNotFoundError(f"products.json not found: {products_path}") - - with products_path.open("r", encoding="utf-8") as f: - products = json.load(f) - - if not isinstance(products, dict): - raise ValueError("products.json must contain a top-level object") - - records = collect_issue_files(issues_dir) - if not records: - raise ValueError( - "No '.md' issue files found; refusing to rewrite products.json" - ) - - clear_leaf_issue_arrays(products) - - for product, issue_type, version, filename in sorted( - records, - key=lambda r: (r[0], r[1], r[2]), - ): - path = pick_target_leaf_path(products, product, version) - if path is None: - continue - add_file_to_leaf(products, path, issue_type, filename) - - with products_path.open("w", encoding="utf-8") as f: - json.dump(products, f, indent=2) - f.write("\n") - - -def main() -> int: - """Entrypoint for command-line usage.""" - - args = parse_args() - script_dir = Path(__file__).resolve().parent - repo_root = script_dir.parent - issues_dir = (repo_root / args.issues_dir).resolve() - products_path = (repo_root / args.products_json).resolve() - - update_products(issues_dir=issues_dir, products_path=products_path) - print(f"Updated {products_path} from {issues_dir}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/test/fixtures/PAN-OS-12.1.2-addressed/expected-parsed.json b/test/fixtures/PAN-OS-12.1.2-addressed/expected-parsed.json new file mode 100644 index 0000000..40d5a17 --- /dev/null +++ b/test/fixtures/PAN-OS-12.1.2-addressed/expected-parsed.json @@ -0,0 +1,30 @@ +[ + { + "id": "PAN-242777", + "summary": "Fixed an issue where users previously reported limitations due to session count caps when utilizing **Web Proxy** features on PA-5400 Series Firewalls. To address these performance complaints and support higher traffic volumes, we have increased the maximum session capacity on specific **PA-5400F** series platforms, leveraging available system memory. This update ensures greater capacity and stability for high-volume environments.The supported session limits are:| Platform | Max Sessions |\n| -------- | ------------ |\n| PA-5410 | 95K |\n| PA-5420 | 95K |\n| PA-5430 | 95K |\n| PA-5440 | 225K |\n| PA-5445 | 250K |\n| PA-5450 | 1.28M |", + "resolved": "", + "caveat": "", + "renderedHtml": "

Fixed an issue where users previously reported limitations due to session count caps when utilizing Web Proxy features on PA-5400 Series Firewalls. To address these performance complaints and support higher traffic volumes, we have increased the maximum session capacity on specific PA-5400F series platforms, leveraging available system memory. This update ensures greater capacity and stability for high-volume environments.The supported session limits are:| Platform | Max Sessions |\n| -------- | ------------ |\n| PA-5410 | 95K |\n| PA-5420 | 95K |\n| PA-5430 | 95K |\n| PA-5440 | 225K |\n| PA-5445 | 250K |\n| PA-5450 | 1.28M |

\n" + }, + { + "id": "PAN-291499", + "summary": "Fixed an issue where newly deployed firewalls were unable to connect to the Strata Logging Service (SLS) until after a reboot, license fetch, or management server restart.", + "resolved": "", + "caveat": "VM-Series firewalls on Amazon Web Services (AWS) environments only", + "renderedHtml": "
Caveat: VM-Series firewalls on Amazon Web Services (AWS) environments only

Fixed an issue where newly deployed firewalls were unable to connect to the Strata Logging Service (SLS) until after a reboot, license fetch, or management server restart.

\n" + }, + { + "id": "PAN-288726", + "summary": "Fixed an issue where the useridd process stopped responding due to a Security policy rule ID being set to 0, which caused the last configuration retrieval to fail.", + "resolved": "", + "caveat": "", + "renderedHtml": "

Fixed an issue where the useridd process stopped responding due to a Security policy rule ID being set to 0, which caused the last configuration retrieval to fail.

\n" + }, + { + "id": "PAN-287133", + "summary": "Fixed an issue on the Panorama web interface where assigning a policy rule to a group at the top or bottom of the list changed the order of other policy rules.", + "resolved": "", + "caveat": "", + "renderedHtml": "

Fixed an issue on the Panorama web interface where assigning a policy rule to a group at the top or bottom of the list changed the order of other policy rules.

\n" + } +] diff --git a/test/update-products-from-issues.test.mjs b/test/update-products-from-issues.test.mjs new file mode 100644 index 0000000..02650bc --- /dev/null +++ b/test/update-products-from-issues.test.mjs @@ -0,0 +1,70 @@ +import assert from 'node:assert/strict'; +import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import test from 'node:test'; +import { buildProductTree, enumerateTreePaths } from '../web/js/product-tree-model.js'; +import { updateProductsFromIssues } from '../scripts/update-products-from-issues.mjs'; + +test('product updater rebuilds products and append-only manifest idempotently', async t => { + const root = await mkdtemp(join(tmpdir(), 'firewallissues-update-')); + t.after(() => rm(root, { recursive: true, force: true })); + const issuesDir = join(root, 'issues'); + const productsPath = join(root, 'products.json'); + const manifestPath = join(root, 'manifest.json'); + await mkdir(join(issuesDir, 'Product', 'addressed'), { recursive: true }); + await mkdir(join(issuesDir, 'Product', 'known'), { recursive: true }); + await writeFile(join(issuesDir, 'Product', 'addressed', '1.2.3-h2.md'), 'addressed'); + await writeFile(join(issuesDir, 'Product', 'addressed', '1.2.3.md'), 'addressed'); + await writeFile(join(issuesDir, 'Product', 'known', '1.2.4.md'), 'known'); + + const initialProducts = { + Product: { + 1: { + '1.2': { + addressed: ['obsolete.md'], + known: [] + } + } + } + }; + const rootPath = JSON.stringify(['Product']); + await writeFile(productsPath, `${JSON.stringify(initialProducts, null, 2)}\n`); + await writeFile(manifestPath, `${JSON.stringify({ schema: 1, nodes: [rootPath, 'legacy-path', null] }, null, 2)}\n`); + + const first = await updateProductsFromIssues({ issuesDir, productsPath, manifestPath }); + assert.equal(first.records, 3); + assert.equal(first.unmatched.length, 0); + assert.ok(first.appended.length > 0); + assert.deepEqual(first.missing, ['legacy-path']); + + const products = JSON.parse(await readFile(productsPath, 'utf8')); + assert.deepEqual(products.Product[1]['1.2'].addressed, ['1.2.3.md', '1.2.3-h2.md']); + assert.deepEqual(products.Product[1]['1.2'].known, ['1.2.4.md']); + + const manifest = JSON.parse(await readFile(manifestPath, 'utf8')); + assert.deepEqual(manifest.nodes.slice(0, 3), [rootPath, 'legacy-path', null]); + const renderedPaths = enumerateTreePaths(buildProductTree(products)); + assert.ok(renderedPaths.every(path => manifest.nodes.includes(path))); + + const second = await updateProductsFromIssues({ issuesDir, productsPath, manifestPath }); + assert.deepEqual(second.appended, []); + assert.deepEqual(JSON.parse(await readFile(manifestPath, 'utf8')), manifest); +}); + +test('product updater refuses to rewrite when no issue files are found', async t => { + const root = await mkdtemp(join(tmpdir(), 'firewallissues-empty-update-')); + t.after(() => rm(root, { recursive: true, force: true })); + const issuesDir = join(root, 'issues'); + const productsPath = join(root, 'products.json'); + const manifestPath = join(root, 'manifest.json'); + await mkdir(issuesDir); + await writeFile(productsPath, '{}\n'); + await writeFile(manifestPath, '{"schema":1,"nodes":[]}\n'); + + await assert.rejects( + updateProductsFromIssues({ issuesDir, productsPath, manifestPath }), + /No '\.md' issue files found/ + ); + assert.equal(await readFile(productsPath, 'utf8'), '{}\n'); +}); diff --git a/web/js/product-tree-model.js b/web/js/product-tree-model.js new file mode 100644 index 0000000..e3d42ad --- /dev/null +++ b/web/js/product-tree-model.js @@ -0,0 +1,144 @@ +import { getSortedKeys } from './sort.js'; + +export function expandFilePrefixLevel(root) { + if (!root || typeof root !== 'object') { + return root; + } + return transformNode(root); +} + +function transformNode(node) { + if (!node || typeof node !== 'object' || Array.isArray(node)) { + return node; + } + + const hasIssueArrays = Object.prototype.hasOwnProperty.call(node, 'addressed') || + Object.prototype.hasOwnProperty.call(node, 'known'); + const childKeys = Object.keys(node).filter(key => key !== 'addressed' && key !== 'known'); + const transformedChildren = Object.fromEntries( + childKeys.map(key => [key, transformNode(node[key])]) + ); + + if (!hasIssueArrays) { + return transformedChildren; + } + + const addressed = Array.isArray(node.addressed) ? node.addressed : []; + const known = Array.isArray(node.known) ? node.known : []; + if (addressed.length + known.length === 0) { + return childKeys.length > 0 ? transformedChildren : { addressed: [], known: [] }; + } + + const groupedByPrefix = {}; + for (const [issueType, files] of [['addressed', addressed], ['known', known]]) { + for (const fileName of files) { + const prefix = getFilePrefix(fileName); + groupedByPrefix[prefix] ??= { addressed: [], known: [] }; + groupedByPrefix[prefix][issueType].push(fileName); + } + } + + const transformedIssueFiles = groupHotfixPrefixes(groupedByPrefix); + return childKeys.length === 0 + ? transformedIssueFiles + : mergeTreeNodes(transformedChildren, transformedIssueFiles); +} + +function groupHotfixPrefixes(groupedByPrefix) { + const byFamily = {}; + for (const prefix of Object.keys(groupedByPrefix)) { + const family = getReleaseFamily(prefix); + byFamily[family] ??= []; + byFamily[family].push(prefix); + } + + const result = {}; + for (const [family, members] of Object.entries(byFamily)) { + const hasHotfixes = members.length > 1 || members.some(member => member !== family); + if (!hasHotfixes && members[0] === family) { + result[family] = groupedByPrefix[family]; + } else { + result[family] = Object.fromEntries( + members.map(member => [member, groupedByPrefix[member]]) + ); + } + } + return result; +} + +function getReleaseFamily(prefix) { + return String(prefix || '').replace(/-h\d+$/i, ''); +} + +function getFilePrefix(fileName) { + const baseName = String(fileName || ''); + const underscoreIndex = baseName.indexOf('_'); + if (underscoreIndex <= 0) { + return baseName.replace(/\.json$/i, '') || 'unknown'; + } + return baseName.slice(0, underscoreIndex); +} + +function mergeTreeNodes(left, right) { + if (!isObjectNode(left)) return right; + if (!isObjectNode(right)) return left; + const merged = { ...left }; + + for (const [key, rightValue] of Object.entries(right)) { + const leftValue = merged[key]; + if (key === 'addressed' || key === 'known') { + const leftFiles = Array.isArray(leftValue) ? leftValue : []; + const rightFiles = Array.isArray(rightValue) ? rightValue : []; + merged[key] = Array.from(new Set([...leftFiles, ...rightFiles])); + } else if (isObjectNode(leftValue) && isObjectNode(rightValue)) { + merged[key] = mergeTreeNodes(leftValue, rightValue); + } else { + merged[key] = rightValue; + } + } + return merged; +} + +function isObjectNode(value) { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + +export function toTreeNodes(value, path = []) { + return getSortedKeys(value) + .filter(key => key !== 'addressed' && key !== 'known') + .map(key => { + const childValue = value[key]; + const childPath = [...path, key]; + const isObject = childValue && typeof childValue === 'object'; + const hasFiles = isObject && ('addressed' in childValue || 'known' in childValue); + const children = isObject ? toTreeNodes(childValue, childPath) : []; + return { + id: JSON.stringify(childPath), + label: key, + selectable: hasFiles || children.length > 0, + metadata: { + path: childPath, + addressed: hasFiles && Array.isArray(childValue.addressed) ? childValue.addressed : [], + known: hasFiles && Array.isArray(childValue.known) ? childValue.known : [] + }, + ...(children.length > 0 ? { children } : {}) + }; + }); +} + +export function enumerateTreePaths(nodes) { + const paths = []; + function visit(children, parentPath = '') { + for (const node of children) { + const path = parentPath ? `${parentPath}>${node.id}` : node.id; + paths.push(path); + visit(node.children ?? [], path); + } + } + visit(nodes); + return paths; +} + +export function buildProductTree(products) { + return toTreeNodes(expandFilePrefixLevel(products)); +} diff --git a/web/js/script.js b/web/js/script.js index 63fafab..a22180e 100644 --- a/web/js/script.js +++ b/web/js/script.js @@ -1,6 +1,7 @@ import { loadIssuesForCheckedPaths } from './issues.js'; import { getCheckedFileRefs, initializeTree, renderProductTree, restoreTreeState } from './tree.js'; import { applyIssueSearchFilter, getIssueTypeFilters, initializeUI } from './ui.js'; +import { expandFilePrefixLevel } from './product-tree-model.js'; document.addEventListener('DOMContentLoaded', () => { initializeUI({ onSelectionChange: refreshIssuesForCurrentSelection }); @@ -29,151 +30,3 @@ function loadProductTree() { }) .catch(error => console.error('Error loading products:', error)); } - -function expandFilePrefixLevel(root) { - if (!root || typeof root !== 'object') { - return root; - } - - return transformNode(root); -} - -function transformNode(node) { - if (!node || typeof node !== 'object' || Array.isArray(node)) { - return node; - } - - const hasIssueArrays = Object.prototype.hasOwnProperty.call(node, 'addressed') || - Object.prototype.hasOwnProperty.call(node, 'known'); - const childKeys = Object.keys(node).filter(key => key !== 'addressed' && key !== 'known'); - - const transformedChildren = {}; - childKeys.forEach(key => { - transformedChildren[key] = transformNode(node[key]); - }); - - if (!hasIssueArrays) { - return transformedChildren; - } - - const addressed = Array.isArray(node.addressed) ? node.addressed : []; - const known = Array.isArray(node.known) ? node.known : []; - const allFiles = [...addressed, ...known]; - - if (allFiles.length === 0) { - if (childKeys.length > 0) { - return transformedChildren; - } - - return { - addressed: [], - known: [] - }; - } - - const groupedByPrefix = {}; - - addressed.forEach(fileName => { - const prefix = getFilePrefix(fileName); - if (!groupedByPrefix[prefix]) { - groupedByPrefix[prefix] = { addressed: [], known: [] }; - } - groupedByPrefix[prefix].addressed.push(fileName); - }); - - known.forEach(fileName => { - const prefix = getFilePrefix(fileName); - if (!groupedByPrefix[prefix]) { - groupedByPrefix[prefix] = { addressed: [], known: [] }; - } - groupedByPrefix[prefix].known.push(fileName); - }); - - const transformedIssueFiles = groupHotfixPrefixes(groupedByPrefix); - if (childKeys.length === 0) { - return transformedIssueFiles; - } - - return mergeTreeNodes(transformedChildren, transformedIssueFiles); -} - -function groupHotfixPrefixes(groupedByPrefix) { - const byFamily = {}; - - Object.keys(groupedByPrefix).forEach(prefix => { - const family = getReleaseFamily(prefix); - if (!byFamily[family]) { - byFamily[family] = []; - } - byFamily[family].push(prefix); - }); - - const result = {}; - - Object.keys(byFamily).forEach(family => { - const members = byFamily[family]; - const hasHotfixes = members.length > 1 || members.some(member => member !== family); - - if (!hasHotfixes && members[0] === family) { - result[family] = groupedByPrefix[family]; - return; - } - - const branch = {}; - members.forEach(member => { - branch[member] = groupedByPrefix[member]; - }); - result[family] = branch; - }); - - return result; -} - -function getReleaseFamily(prefix) { - return String(prefix || '').replace(/-h\d+$/i, ''); -} - -function getFilePrefix(fileName) { - const baseName = String(fileName || ''); - const underscoreIndex = baseName.indexOf('_'); - if (underscoreIndex <= 0) { - return baseName.replace(/\.json$/i, '') || 'unknown'; - } - return baseName.slice(0, underscoreIndex); -} - -function mergeTreeNodes(left, right) { - if (!isObjectNode(left)) { - return right; - } - if (!isObjectNode(right)) { - return left; - } - - const merged = { ...left }; - - Object.keys(right).forEach(key => { - const leftValue = merged[key]; - const rightValue = right[key]; - - if (key === 'addressed' || key === 'known') { - const leftFiles = Array.isArray(leftValue) ? leftValue : []; - const rightFiles = Array.isArray(rightValue) ? rightValue : []; - merged[key] = Array.from(new Set([...leftFiles, ...rightFiles])); - return; - } - - if (isObjectNode(leftValue) && isObjectNode(rightValue)) { - merged[key] = mergeTreeNodes(leftValue, rightValue); - return; - } - - merged[key] = rightValue; - }); - - return merged; -} - -function isObjectNode(value) { - return Boolean(value) && typeof value === 'object' && !Array.isArray(value); -} diff --git a/web/js/tree.js b/web/js/tree.js index d40f361..9b8d0b8 100644 --- a/web/js/tree.js +++ b/web/js/tree.js @@ -1,5 +1,5 @@ import { CheckboxTree, loadManifest } from '../vendor/checkbox-tree/checkbox-tree.js'; -import { getSortedKeys } from './sort.js'; +import { toTreeNodes } from './product-tree-model.js'; const STORAGE_KEY = 'tree-state-v2'; let productTree = null; @@ -14,6 +14,7 @@ export function initializeTree(containerId, onSelectionChange) { productTree = new CheckboxTree(container, { storageKey: STORAGE_KEY, initiallyCollapsed: true, + stateEncoding: 'tiered', onSelectionChange: event => { onSelectionChange(event); pushTreeState(); @@ -82,30 +83,6 @@ function pushTreeState() { } } -function toTreeNodes(value, path = []) { - return getSortedKeys(value) - .filter(key => key !== 'addressed' && key !== 'known') - .map(key => { - const childValue = value[key]; - const childPath = [...path, key]; - const isObject = childValue && typeof childValue === 'object'; - const hasFiles = isObject && ('addressed' in childValue || 'known' in childValue); - const children = isObject ? toTreeNodes(childValue, childPath) : []; - - return { - id: JSON.stringify(childPath), - label: key, - selectable: hasFiles || children.length > 0, - metadata: { - path: childPath, - addressed: hasFiles && Array.isArray(childValue.addressed) ? childValue.addressed : [], - known: hasFiles && Array.isArray(childValue.known) ? childValue.known : [] - }, - ...(children.length > 0 ? { children } : {}) - }; - }); -} - function dedupeFileRefs(fileRefs) { const seen = new Set(); return fileRefs.filter(({ productKey, fileName }) => { diff --git a/web/vendor/checkbox-tree/README.md b/web/vendor/checkbox-tree/README.md index a14d6f3..b8a8b67 100644 --- a/web/vendor/checkbox-tree/README.md +++ b/web/vendor/checkbox-tree/README.md @@ -43,8 +43,8 @@ tree.setData([ ]); ``` -For applications without a bundler, serve or copy the two files in `src/` and -use relative URLs: +For applications without a bundler, serve or copy the `src/` directory and use +relative URLs. The main module imports its state-codec modules alongside it: ```html @@ -78,6 +78,9 @@ Options: - `onSelectionChange` receives `{ selectedIds, selectedNodes }`. - `manifest` optionally supplies a parsed manifest and automatically restores state from `window.location.hash` after `setData()`. +- `stateEncoding` selects `"adaptive"` (the default), `"dense"`, or `"tiered"`. + The website's configured value is the URL-format contract and is not embedded + in the fragment. ## Shareable URL state @@ -105,6 +108,15 @@ Use `validateManifestEvolution(previous, next)` in a build check to reject removed slots and tombstone reuse; path changes are reported for deliberate rename/move review. +With `stateEncoding: "tiered"`, `c1` stores root checked state and each deeper +`cN` field stores nodes whose checked state differs from their parent. Expansion +does not inherit: every `xN` stores the actual expanded branches at that depth, +with collapsed as the baseline. Each tier field uses the adaptive codec. The +`n` field records manifest coverage so nodes appended later still default to +false. A non-selectable parent supplies a false checked baseline. With +`stateEncoding: "dense"`, `c` and `x` contain trimmed +least-significant-bit-first bitsets. + Styling is namespaced under `.checkbox-tree`. Override the custom properties `--checkbox-tree-indent`, `--checkbox-tree-toggle-size`, and `--checkbox-tree-hover-color` in the consuming application. diff --git a/web/vendor/checkbox-tree/checkbox-tree.js b/web/vendor/checkbox-tree/checkbox-tree.js index 4974fc6..0dbe2f9 100644 --- a/web/vendor/checkbox-tree/checkbox-tree.js +++ b/web/vendor/checkbox-tree/checkbox-tree.js @@ -1,311 +1,44 @@ +// Dependency-free checkbox tree widget. State serialization lives in focused sibling modules. +import { + PATH_DELIMITER, + assertStateEncoding +} from './state-codec.js'; +import { + restoreStateFromLocation, + serializeStateToFragment, + validateManifest +} from './tree-state.js'; + +export { + PATH_DELIMITER, + SERIALIZATION_VERSION, + base64UrlToBytes, + bytesToBase64Url, + decodeAdaptiveBitset, + encodeAdaptiveBitset, + getBit, + loadManifest, + setBit +} from './state-codec.js'; +export { + applyTreeState, + decodeTreeState, + encodeTreeState, + restoreStateFromLocation, + serializeStateToFragment, + validateManifest, + validateManifestEvolution +} from './tree-state.js'; + const DEFAULT_OPTIONS = { initiallyCollapsed: true, initiallySelected: false, storageKey: null, onSelectionChange: null, - manifest: null + manifest: null, + stateEncoding: 'adaptive' }; -const SERIALIZATION_VERSION = 1; -const PATH_DELIMITER = '>'; - -export function loadManifest(source) { - let records; - let schema = 1; - - if (typeof source === 'string') { - records = source.split(/\r?\n/) - .map(line => line.trim()) - .filter(line => line && !line.startsWith('#')) - .map(line => line === '!deleted' ? null : line); - } else if (source && typeof source === 'object' && Array.isArray(source.nodes)) { - schema = source.schema; - records = source.nodes; - } else if (source && Array.isArray(source.slots)) { - return normalizeManifest(source.schema, source.slots.map(slot => slot?.deleted ? null : slot?.path)); - } else { - throw new TypeError('CheckboxTree manifest must be text or an object with a nodes array.'); - } - - return normalizeManifest(schema, records); -} - -function normalizeManifest(schema, records) { - if (schema !== 1) { - throw new Error(`Unsupported CheckboxTree manifest schema: ${schema}`); - } - - const pathToSlot = new Map(); - const slots = records.map((record, index) => { - if (record === null || record === '!deleted') { - return { path: null, deleted: true }; - } - if (typeof record !== 'string' || !record || record.trim() !== record) { - throw new TypeError(`Invalid CheckboxTree manifest entry at slot ${index}.`); - } - if (pathToSlot.has(record)) { - throw new Error(`Duplicate CheckboxTree manifest path: ${record}`); - } - pathToSlot.set(record, index); - return { path: record, deleted: false }; - }); - - return { schema: 1, slots, pathToSlot }; -} - -export function setBit(bytes, bitIndex, value) { - const byteIndex = Math.floor(bitIndex / 8); - const mask = 1 << (bitIndex % 8); - if (value) bytes[byteIndex] |= mask; - else bytes[byteIndex] &= ~mask; -} - -export function getBit(bytes, bitIndex) { - const byteIndex = Math.floor(bitIndex / 8); - return byteIndex < bytes.length && (bytes[byteIndex] & (1 << (bitIndex % 8))) !== 0; -} - -export function bytesToBase64Url(bytes) { - let binary = ''; - for (const byte of bytes) binary += String.fromCharCode(byte); - return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); -} - -export function base64UrlToBytes(value, maximumLength = Infinity) { - if (typeof value !== 'string' || !/^[A-Za-z0-9_-]*$/.test(value) || value.length % 4 === 1) { - throw new Error('Invalid Base64URL value.'); - } - if (Number.isFinite(maximumLength) && value.length > Math.ceil(maximumLength * 4 / 3) + 2) { - throw new Error('Encoded tree state exceeds the manifest size.'); - } - const base64 = value.replace(/-/g, '+').replace(/_/g, '/'); - const binary = atob(base64 + '='.repeat((4 - base64.length % 4) % 4)); - if (binary.length > maximumLength) throw new Error('Encoded tree state exceeds the manifest size.'); - return Uint8Array.from(binary, character => character.charCodeAt(0)); -} - -function trimTrailingZeroBytes(bytes) { - let end = bytes.length; - while (end && bytes[end - 1] === 0) end--; - return bytes.slice(0, end); -} - -function encodeUnsignedVarint(value) { - const bytes = []; - do { - let byte = value & 0x7f; - value = Math.floor(value / 128); - if (value) byte |= 0x80; - bytes.push(byte); - } while (value); - return bytes; -} - -function decodeUnsignedVarint(bytes, offset) { - let value = 0; - let multiplier = 1; - for (let index = offset; index < bytes.length && index < offset + 8; index++) { - const byte = bytes[index]; - value += (byte & 0x7f) * multiplier; - if ((byte & 0x80) === 0) return { value, offset: index + 1 }; - multiplier *= 128; - if (!Number.isSafeInteger(value) || !Number.isSafeInteger(multiplier)) break; - } - throw new Error('Invalid adaptive tree-state integer.'); -} - -function encodeSparseIndexes(indexes) { - const bytes = []; - let previous = -1; - for (const index of indexes) { - bytes.push(...encodeUnsignedVarint(index - previous - 1)); - previous = index; - } - return bytes; -} - -function decodeSparseIndexes(bytes, offset, limit) { - const indexes = new Set(); - let previous = -1; - while (offset < bytes.length) { - const decoded = decodeUnsignedVarint(bytes, offset); - const index = previous + decoded.value + 1; - if (index >= limit) throw new Error('Sparse tree state contains an out-of-range slot.'); - indexes.add(index); - previous = index; - offset = decoded.offset; - } - return indexes; -} - -export function encodeAdaptiveBitset(values, slotCount) { - const enabled = []; - const disabled = []; - const denseBytes = new Uint8Array(Math.ceil(slotCount / 8)); - for (const [slot, value] of values) { - (value ? enabled : disabled).push(slot); - setBit(denseBytes, slot, value); - } - enabled.sort((left, right) => left - right); - disabled.sort((left, right) => left - right); - if (enabled.length === 0) return new Uint8Array(); - - const candidates = [ - Uint8Array.from([0, ...trimTrailingZeroBytes(denseBytes)]), - Uint8Array.from([1, ...encodeSparseIndexes(enabled)]), - Uint8Array.from([2, ...encodeUnsignedVarint(slotCount), ...encodeSparseIndexes(disabled)]) - ]; - return candidates.reduce((shortest, candidate) => - candidate.length < shortest.length ? candidate : shortest - ); -} - -export function decodeAdaptiveBitset(bytes, slotCount) { - if (bytes.length === 0) return () => false; - const mode = bytes[0]; - if (mode === 0) { - const dense = bytes.slice(1); - if (dense.length > Math.ceil(slotCount / 8)) { - throw new Error('Dense tree state exceeds the manifest size.'); - } - return slot => getBit(dense, slot); - } - if (mode === 1) { - const enabled = decodeSparseIndexes(bytes, 1, slotCount); - return slot => enabled.has(slot); - } - if (mode === 2) { - const coverage = decodeUnsignedVarint(bytes, 1); - if (coverage.value > slotCount) throw new Error('Tree-state coverage exceeds the manifest size.'); - const disabled = decodeSparseIndexes(bytes, coverage.offset, coverage.value); - return slot => slot < coverage.value && !disabled.has(slot); - } - throw new Error('Unknown adaptive tree-state mode.'); -} - -function asManifest(manifest) { - return manifest?.pathToSlot instanceof Map ? manifest : loadManifest(manifest); -} - -export function validateManifest(manifest, tree) { - const normalized = asManifest(manifest); - const treePaths = new Set(tree.nodesByPath.keys()); - return { - valid: Array.from(treePaths).every(path => normalized.pathToSlot.has(path)), - unregisteredPaths: Array.from(treePaths).filter(path => !normalized.pathToSlot.has(path)), - missingPaths: normalized.slots - .filter(slot => !slot.deleted && !treePaths.has(slot.path)) - .map(slot => slot.path) - }; -} - -export function validateManifestEvolution(previous, next) { - const oldManifest = asManifest(previous); - const newManifest = asManifest(next); - const errors = []; - - if (newManifest.slots.length < oldManifest.slots.length) { - errors.push('Manifest slots cannot be removed; use tombstones.'); - } - oldManifest.slots.forEach((oldSlot, index) => { - const newSlot = newManifest.slots[index]; - if (!newSlot) return; - if (oldSlot.deleted && !newSlot.deleted) { - errors.push(`Tombstoned slot ${index} cannot be reused.`); - } - if (!oldSlot.deleted && !newSlot.deleted && oldSlot.path !== newSlot.path) { - errors.push(`Slot ${index} changed path; confirm this is a logical rename or move.`); - } - }); - - return { valid: errors.length === 0, errors }; -} - -export function encodeTreeState(tree, manifest) { - const normalized = asManifest(manifest); - const checkedValues = new Map(); - const expandedValues = new Map(); - - for (const [path, node] of tree.nodesByPath) { - const slot = normalized.pathToSlot.get(path); - if (slot === undefined) continue; - const item = tree.itemForNode(node.id); - const checkbox = item && tree.directCheckbox(item); - const toggle = item?.querySelector(':scope > .checkbox-tree__row > .checkbox-tree__toggle'); - checkedValues.set(slot, Boolean(checkbox?.checked)); - expandedValues.set(slot, toggle?.getAttribute('aria-expanded') === 'true'); - } - - const checkedValue = bytesToBase64Url(encodeAdaptiveBitset(checkedValues, normalized.slots.length)); - const expandedValue = bytesToBase64Url(encodeAdaptiveBitset(expandedValues, normalized.slots.length)); - return { version: SERIALIZATION_VERSION, checked: checkedValue || null, expanded: expandedValue || null }; -} - -export function decodeTreeState(fragment, manifest) { - const normalized = asManifest(manifest); - const value = fragment.startsWith('#') ? fragment.slice(1) : fragment; - const params = new URLSearchParams(value); - if (params.get('v') !== String(SERIALIZATION_VERSION)) { - return { applied: false, version: params.get('v'), warnings: ['Missing or unsupported serialization version.'] }; - } - const maximumLength = Math.max( - Math.ceil(normalized.slots.length / 8) + 1, - normalized.slots.length * 2 + 10 - ); - try { - const checked = base64UrlToBytes(params.get('c') ?? '', maximumLength); - const expanded = base64UrlToBytes(params.get('x') ?? '', maximumLength); - decodeAdaptiveBitset(checked, normalized.slots.length); - decodeAdaptiveBitset(expanded, normalized.slots.length); - return { - applied: true, - version: SERIALIZATION_VERSION, - checked, - expanded, - warnings: [] - }; - } catch (error) { - return { applied: false, version: SERIALIZATION_VERSION, warnings: [error.message] }; - } -} - -export function applyTreeState(tree, manifest, state) { - if (!state?.applied) return false; - const normalized = asManifest(manifest); - let isChecked; - let isExpanded; - try { - isChecked = decodeAdaptiveBitset(state.checked, normalized.slots.length); - isExpanded = decodeAdaptiveBitset(state.expanded, normalized.slots.length); - } catch { - return false; - } - for (const [path, node] of tree.nodesByPath) { - const slot = normalized.pathToSlot.get(path); - if (slot === undefined) continue; - const item = tree.itemForNode(node.id); - const checkbox = item && tree.directCheckbox(item); - if (checkbox && !checkbox.disabled) checkbox.checked = isChecked(slot); - tree.setExpanded(item, isExpanded(slot)); - } - tree.updateAllParentCheckboxes(); - return true; -} - -export function serializeStateToFragment(tree, manifest) { - const state = encodeTreeState(tree, manifest); - const params = new URLSearchParams({ v: String(state.version) }); - if (state.checked) params.set('c', state.checked); - if (state.expanded) params.set('x', state.expanded); - return `#${params}`; -} - -export function restoreStateFromLocation(tree, manifest, location = window.location) { - const state = decodeTreeState(location.hash ?? '', manifest); - if (state.applied) applyTreeState(tree, manifest, state); - return state; -} - export class CheckboxTree { constructor(container, options = {}) { if (!(container instanceof Element)) { @@ -314,8 +47,10 @@ export class CheckboxTree { this.container = container; this.options = { ...DEFAULT_OPTIONS, ...options }; + assertStateEncoding(this.options.stateEncoding); this.nodesById = new Map(); this.nodesByPath = new Map(); + this.itemsById = new Map(); this.rootNodes = []; this.handleChange = this.handleChange.bind(this); this.handleClick = this.handleClick.bind(this); @@ -332,6 +67,7 @@ export class CheckboxTree { this.nodesById.clear(); this.nodesByPath.clear(); + this.itemsById.clear(); this.rootNodes = nodes; this.indexNodes(nodes); this.render(); @@ -371,6 +107,7 @@ export class CheckboxTree { this.container.replaceChildren(); this.nodesById.clear(); this.nodesByPath.clear(); + this.itemsById.clear(); } indexNodes(nodes, parentPath = '') { @@ -408,6 +145,7 @@ export class CheckboxTree { const item = document.createElement('div'); item.className = 'checkbox-tree__item'; item.dataset.nodeId = node.id; + this.itemsById.set(node.id, item); const row = document.createElement('div'); row.className = 'checkbox-tree__row'; @@ -524,8 +262,7 @@ export class CheckboxTree { } itemForNode(id) { - return Array.from(this.container.querySelectorAll('.checkbox-tree__item')) - .find(item => item.dataset.nodeId === id) ?? null; + return this.itemsById.get(id) ?? null; } updateAncestorCheckboxes(item) { diff --git a/web/vendor/checkbox-tree/state-codec.js b/web/vendor/checkbox-tree/state-codec.js new file mode 100644 index 0000000..8a982a8 --- /dev/null +++ b/web/vendor/checkbox-tree/state-codec.js @@ -0,0 +1,206 @@ +// Manifest parsing and binary codecs. This module has no DOM dependencies. +export const SERIALIZATION_VERSION = 1; +export const PATH_DELIMITER = '>'; +const STATE_ENCODINGS = new Set(['adaptive', 'dense', 'tiered']); + +export function loadManifest(source) { + let records; + let schema = 1; + + if (typeof source === 'string') { + records = source.split(/\r?\n/) + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')) + .map(line => line === '!deleted' ? null : line); + } else if (source && typeof source === 'object' && Array.isArray(source.nodes)) { + schema = source.schema; + records = source.nodes; + } else if (source && Array.isArray(source.slots)) { + return normalizeManifest(source.schema, source.slots.map(slot => slot?.deleted ? null : slot?.path)); + } else { + throw new TypeError('CheckboxTree manifest must be text or an object with a nodes array.'); + } + + return normalizeManifest(schema, records); +} + +function normalizeManifest(schema, records) { + if (schema !== 1) { + throw new Error(`Unsupported CheckboxTree manifest schema: ${schema}`); + } + + const pathToSlot = new Map(); + const slots = records.map((record, index) => { + if (record === null || record === '!deleted') { + return { path: null, deleted: true }; + } + if (typeof record !== 'string' || !record || record.trim() !== record) { + throw new TypeError(`Invalid CheckboxTree manifest entry at slot ${index}.`); + } + if (pathToSlot.has(record)) { + throw new Error(`Duplicate CheckboxTree manifest path: ${record}`); + } + pathToSlot.set(record, index); + return { path: record, deleted: false }; + }); + + return { schema: 1, slots, pathToSlot }; +} + +export function setBit(bytes, bitIndex, value) { + const byteIndex = Math.floor(bitIndex / 8); + const mask = 1 << (bitIndex % 8); + if (value) bytes[byteIndex] |= mask; + else bytes[byteIndex] &= ~mask; +} + +export function getBit(bytes, bitIndex) { + const byteIndex = Math.floor(bitIndex / 8); + return byteIndex < bytes.length && (bytes[byteIndex] & (1 << (bitIndex % 8))) !== 0; +} + +export function bytesToBase64Url(bytes) { + let binary = ''; + for (const byte of bytes) binary += String.fromCharCode(byte); + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); +} + +export function base64UrlToBytes(value, maximumLength = Infinity) { + if (typeof value !== 'string' || !/^[A-Za-z0-9_-]*$/.test(value) || value.length % 4 === 1) { + throw new Error('Invalid Base64URL value.'); + } + if (Number.isFinite(maximumLength) && value.length > Math.ceil(maximumLength * 4 / 3) + 2) { + throw new Error('Encoded tree state exceeds the manifest size.'); + } + const base64 = value.replace(/-/g, '+').replace(/_/g, '/'); + const binary = atob(base64 + '='.repeat((4 - base64.length % 4) % 4)); + if (binary.length > maximumLength) throw new Error('Encoded tree state exceeds the manifest size.'); + return Uint8Array.from(binary, character => character.charCodeAt(0)); +} + +function trimTrailingZeroBytes(bytes) { + let end = bytes.length; + while (end && bytes[end - 1] === 0) end--; + return bytes.slice(0, end); +} + +function encodeUnsignedVarint(value) { + const bytes = []; + do { + let byte = value & 0x7f; + value = Math.floor(value / 128); + if (value) byte |= 0x80; + bytes.push(byte); + } while (value); + return bytes; +} + +function decodeUnsignedVarint(bytes, offset) { + let value = 0; + let multiplier = 1; + for (let index = offset; index < bytes.length && index < offset + 8; index++) { + const byte = bytes[index]; + value += (byte & 0x7f) * multiplier; + if ((byte & 0x80) === 0) return { value, offset: index + 1 }; + multiplier *= 128; + if (!Number.isSafeInteger(value) || !Number.isSafeInteger(multiplier)) break; + } + throw new Error('Invalid adaptive tree-state integer.'); +} + +function encodeSparseIndexes(indexes) { + const bytes = []; + let previous = -1; + for (const index of indexes) { + bytes.push(...encodeUnsignedVarint(index - previous - 1)); + previous = index; + } + return bytes; +} + +function decodeSparseIndexes(bytes, offset, limit) { + const indexes = new Set(); + let previous = -1; + while (offset < bytes.length) { + const decoded = decodeUnsignedVarint(bytes, offset); + const index = previous + decoded.value + 1; + if (index >= limit) throw new Error('Sparse tree state contains an out-of-range slot.'); + indexes.add(index); + previous = index; + offset = decoded.offset; + } + return indexes; +} + +export function encodeAdaptiveBitset(values, slotCount) { + const enabled = []; + const disabled = []; + const denseBytes = new Uint8Array(Math.ceil(slotCount / 8)); + for (const [slot, value] of values) { + (value ? enabled : disabled).push(slot); + setBit(denseBytes, slot, value); + } + enabled.sort((left, right) => left - right); + disabled.sort((left, right) => left - right); + if (enabled.length === 0) return new Uint8Array(); + + const candidates = [ + Uint8Array.from([0, ...trimTrailingZeroBytes(denseBytes)]), + Uint8Array.from([1, ...encodeSparseIndexes(enabled)]), + Uint8Array.from([2, ...encodeUnsignedVarint(slotCount), ...encodeSparseIndexes(disabled)]) + ]; + return candidates.reduce((shortest, candidate) => + candidate.length < shortest.length ? candidate : shortest + ); +} + +export function decodeAdaptiveBitset(bytes, slotCount) { + if (bytes.length === 0) return () => false; + const mode = bytes[0]; + if (mode === 0) { + const dense = bytes.slice(1); + if (dense.length > Math.ceil(slotCount / 8)) { + throw new Error('Dense tree state exceeds the manifest size.'); + } + return slot => getBit(dense, slot); + } + if (mode === 1) { + const enabled = decodeSparseIndexes(bytes, 1, slotCount); + return slot => enabled.has(slot); + } + if (mode === 2) { + const coverage = decodeUnsignedVarint(bytes, 1); + if (coverage.value > slotCount) throw new Error('Tree-state coverage exceeds the manifest size.'); + const disabled = decodeSparseIndexes(bytes, coverage.offset, coverage.value); + return slot => slot < coverage.value && !disabled.has(slot); + } + throw new Error('Unknown adaptive tree-state mode.'); +} + + +export function encodeDenseBitset(values, slotCount) { + const bytes = new Uint8Array(Math.ceil(slotCount / 8)); + for (const [slot, value] of values) setBit(bytes, slot, value); + return trimTrailingZeroBytes(bytes); +} + +export function decodeDenseBitset(bytes, slotCount) { + if (bytes.length > Math.ceil(slotCount / 8)) { + throw new Error('Dense tree state exceeds the manifest size.'); + } + return slot => getBit(bytes, slot); +} + +export function maximumEncodedLength(slotCount) { + return Math.max(Math.ceil(slotCount / 8) + 1, slotCount * 2 + 10); +} + +export function assertStateEncoding(encoding) { + if (!STATE_ENCODINGS.has(encoding)) { + throw new Error(`Unsupported CheckboxTree state encoding: ${encoding}`); + } +} + +export function asManifestSource(manifest) { + return manifest?.pathToSlot instanceof Map ? manifest : loadManifest(manifest); +} diff --git a/web/vendor/checkbox-tree/tree-state.js b/web/vendor/checkbox-tree/tree-state.js new file mode 100644 index 0000000..ac6ed6a --- /dev/null +++ b/web/vendor/checkbox-tree/tree-state.js @@ -0,0 +1,310 @@ +// Adapts pure state codecs to CheckboxTree's rendered node model and URL format. +import { + PATH_DELIMITER, + SERIALIZATION_VERSION, + asManifestSource as asManifest, + assertStateEncoding, + base64UrlToBytes, + bytesToBase64Url, + decodeAdaptiveBitset, + decodeDenseBitset, + encodeAdaptiveBitset, + encodeDenseBitset, + maximumEncodedLength +} from './state-codec.js'; + +export function validateManifest(manifest, tree) { + const normalized = asManifest(manifest); + const treePaths = new Set(tree.nodesByPath.keys()); + return { + valid: Array.from(treePaths).every(path => normalized.pathToSlot.has(path)), + unregisteredPaths: Array.from(treePaths).filter(path => !normalized.pathToSlot.has(path)), + missingPaths: normalized.slots + .filter(slot => !slot.deleted && !treePaths.has(slot.path)) + .map(slot => slot.path) + }; +} + +export function validateManifestEvolution(previous, next) { + const oldManifest = asManifest(previous); + const newManifest = asManifest(next); + const errors = []; + + if (newManifest.slots.length < oldManifest.slots.length) { + errors.push('Manifest slots cannot be removed; use tombstones.'); + } + oldManifest.slots.forEach((oldSlot, index) => { + const newSlot = newManifest.slots[index]; + if (!newSlot) return; + if (oldSlot.deleted && !newSlot.deleted) { + errors.push(`Tombstoned slot ${index} cannot be reused.`); + } + if (!oldSlot.deleted && !newSlot.deleted && oldSlot.path !== newSlot.path) { + errors.push(`Slot ${index} changed path; confirm this is a logical rename or move.`); + } + }); + + return { valid: errors.length === 0, errors }; +} + +function collectTreeValues(tree, manifest) { + const normalized = asManifest(manifest); + const checkedValues = new Map(); + const expandedValues = new Map(); + + for (const [path, node] of tree.nodesByPath) { + const slot = normalized.pathToSlot.get(path); + if (slot === undefined) continue; + const item = tree.itemForNode(node.id); + const checkbox = item && tree.directCheckbox(item); + const toggle = item?.querySelector(':scope > .checkbox-tree__row > .checkbox-tree__toggle'); + checkedValues.set(slot, Boolean(checkbox?.checked)); + expandedValues.set(slot, toggle?.getAttribute('aria-expanded') === 'true'); + } + + return { normalized, checkedValues, expandedValues }; +} + +function nodeState(tree, node) { + const item = tree.itemForNode(node.id); + const checkbox = item && tree.directCheckbox(item); + const toggle = item?.querySelector(':scope > .checkbox-tree__row > .checkbox-tree__toggle'); + return { + checked: Boolean(checkbox?.checked), + hasChecked: Boolean(checkbox), + expanded: toggle?.getAttribute('aria-expanded') === 'true', + hasExpanded: Boolean(toggle) + }; +} + +function encodeTieredState(tree, normalized) { + const checkedByDepth = new Map(); + const expandedByDepth = new Map(); + const statesByPath = new Map(); + + for (const [path, node] of tree.nodesByPath) { + const slot = normalized.pathToSlot.get(path); + if (slot === undefined) continue; + const depth = path.split(PATH_DELIMITER).length; + const parentPath = path.includes(PATH_DELIMITER) + ? path.slice(0, path.lastIndexOf(PATH_DELIMITER)) + : null; + const parent = statesByPath.get(parentPath) ?? { + checked: false, + hasChecked: false, + expanded: false, + hasExpanded: false + }; + const current = nodeState(tree, node); + if (current.hasChecked) { + checkedByDepth.set(depth, checkedByDepth.get(depth) ?? new Map()); + checkedByDepth.get(depth).set(slot, current.checked !== (parent.hasChecked ? parent.checked : false)); + } + if (current.hasExpanded) { + expandedByDepth.set(depth, expandedByDepth.get(depth) ?? new Map()); + expandedByDepth.get(depth).set(slot, current.expanded); + } + statesByPath.set(path, current); + } + + const fields = new Map([['n', String(normalized.slots.length)]]); + for (const [depth, values] of checkedByDepth) { + const encoded = bytesToBase64Url(encodeAdaptiveBitset(values, normalized.slots.length)); + if (encoded) fields.set(`c${depth}`, encoded); + } + for (const [depth, values] of expandedByDepth) { + const encoded = bytesToBase64Url(encodeAdaptiveBitset(values, normalized.slots.length)); + if (encoded) fields.set(`x${depth}`, encoded); + } + return fields; +} + +export function encodeTreeState(tree, manifest, encoding = tree.options?.stateEncoding ?? 'adaptive') { + assertStateEncoding(encoding); + const { normalized, checkedValues, expandedValues } = collectTreeValues(tree, manifest); + + if (encoding === 'tiered') { + return { version: SERIALIZATION_VERSION, encoding, fields: encodeTieredState(tree, normalized) }; + } + + const encode = encoding === 'dense' ? encodeDenseBitset : encodeAdaptiveBitset; + const checkedValue = bytesToBase64Url(encode(checkedValues, normalized.slots.length)); + const expandedValue = bytesToBase64Url(encode(expandedValues, normalized.slots.length)); + return { + version: SERIALIZATION_VERSION, + encoding, + checked: checkedValue || null, + expanded: expandedValue || null + }; +} + +function decodeField(params, name, normalized, encoding) { + const maximumLength = encoding === 'dense' + ? Math.ceil(normalized.slots.length / 8) + : maximumEncodedLength(normalized.slots.length); + const bytes = base64UrlToBytes(params.get(name) ?? '', maximumLength); + const decode = encoding === 'dense' ? decodeDenseBitset : decodeAdaptiveBitset; + decode(bytes, normalized.slots.length); + return bytes; +} + +export function decodeTreeState(fragment, manifest, encoding = 'adaptive') { + assertStateEncoding(encoding); + const normalized = asManifest(manifest); + const value = fragment.startsWith('#') ? fragment.slice(1) : fragment; + const params = new URLSearchParams(value); + if (params.get('v') !== String(SERIALIZATION_VERSION)) { + return { applied: false, version: params.get('v'), encoding, warnings: ['Missing or unsupported serialization version.'] }; + } + try { + if (encoding === 'tiered') { + if (params.has('c') || params.has('x')) { + throw new Error('Tiered tree state contains non-tiered fields.'); + } + const maximumDepth = normalized.slots.reduce((depth, slot) => + slot.deleted ? depth : Math.max(depth, slot.path.split(PATH_DELIMITER).length) + , 0); + for (const key of params.keys()) { + const match = /^([cx])(\d+)$/.exec(key); + if (match && (Number(match[2]) < 1 || Number(match[2]) > maximumDepth)) { + throw new Error(`Tree state contains an invalid tier: ${key}`); + } + } + const coverage = Number(params.get('n')); + if (!Number.isSafeInteger(coverage) || coverage < 0 || coverage > normalized.slots.length) { + throw new Error('Tiered tree state has invalid manifest coverage.'); + } + const checkedTiers = new Map(); + const expandedTiers = new Map(); + for (let depth = 1; depth <= maximumDepth; depth++) { + checkedTiers.set(depth, decodeField(params, `c${depth}`, normalized, 'adaptive')); + expandedTiers.set(depth, decodeField(params, `x${depth}`, normalized, 'adaptive')); + } + return { + applied: true, + version: SERIALIZATION_VERSION, + encoding, + coverage, + checkedTiers, + expandedTiers, + warnings: [] + }; + } + + for (const key of params.keys()) { + if (key === 'n' || /^[cx]\d+$/.test(key)) { + throw new Error('Tree state contains tiered fields for a non-tiered encoding.'); + } + } + + const checked = decodeField(params, 'c', normalized, encoding); + const expanded = decodeField(params, 'x', normalized, encoding); + return { + applied: true, + version: SERIALIZATION_VERSION, + encoding, + checked, + expanded, + warnings: [] + }; + } catch (error) { + return { applied: false, version: SERIALIZATION_VERSION, encoding, warnings: [error.message] }; + } +} + +export function applyTreeState(tree, manifest, state) { + if (!state?.applied) return false; + const normalized = asManifest(manifest); + if (state.encoding === 'tiered') return applyTieredTreeState(tree, normalized, state); + let isChecked; + let isExpanded; + try { + const decode = state.encoding === 'dense' ? decodeDenseBitset : decodeAdaptiveBitset; + isChecked = decode(state.checked, normalized.slots.length); + isExpanded = decode(state.expanded, normalized.slots.length); + } catch { + return false; + } + for (const [path, node] of tree.nodesByPath) { + const slot = normalized.pathToSlot.get(path); + if (slot === undefined) continue; + const item = tree.itemForNode(node.id); + const checkbox = item && tree.directCheckbox(item); + if (checkbox && !checkbox.disabled) checkbox.checked = isChecked(slot); + tree.setExpanded(item, isExpanded(slot)); + } + tree.updateAllParentCheckboxes(); + return true; +} + +function applyTieredTreeState(tree, normalized, state) { + const resolvedByPath = new Map(); + const checkedDecoders = new Map(); + const expandedDecoders = new Map(); + try { + for (const [depth, bytes] of state.checkedTiers) { + checkedDecoders.set(depth, decodeAdaptiveBitset(bytes, normalized.slots.length)); + } + for (const [depth, bytes] of state.expandedTiers) { + expandedDecoders.set(depth, decodeAdaptiveBitset(bytes, normalized.slots.length)); + } + } catch { + return false; + } + + for (const [path, node] of tree.nodesByPath) { + const slot = normalized.pathToSlot.get(path); + if (slot === undefined) continue; + const depth = path.split(PATH_DELIMITER).length; + const parentPath = path.includes(PATH_DELIMITER) + ? path.slice(0, path.lastIndexOf(PATH_DELIMITER)) + : null; + const parent = resolvedByPath.get(parentPath) ?? { + checked: false, + hasChecked: false, + expanded: false, + hasExpanded: false + }; + const item = tree.itemForNode(node.id); + const checkbox = item && tree.directCheckbox(item); + const toggle = item?.querySelector(':scope > .checkbox-tree__row > .checkbox-tree__toggle'); + const isCovered = slot < state.coverage; + const checked = checkbox && isCovered + ? (parent.hasChecked ? parent.checked : false) !== checkedDecoders.get(depth)(slot) + : false; + const expanded = Boolean(toggle && isCovered && expandedDecoders.get(depth)(slot)); + if (checkbox && !checkbox.disabled) checkbox.checked = checked; + tree.setExpanded(item, expanded); + resolvedByPath.set(path, { + checked, + hasChecked: Boolean(checkbox), + expanded, + hasExpanded: Boolean(toggle) + }); + } + tree.updateAllParentCheckboxes(); + return true; +} + +export function serializeStateToFragment(tree, manifest, encoding = tree.options?.stateEncoding ?? 'adaptive') { + const state = encodeTreeState(tree, manifest, encoding); + const params = new URLSearchParams({ v: String(state.version) }); + if (state.encoding === 'tiered') { + for (const [name, value] of state.fields) params.set(name, value); + } else { + if (state.checked) params.set('c', state.checked); + if (state.expanded) params.set('x', state.expanded); + } + return `#${params}`; +} + +export function restoreStateFromLocation( + tree, + manifest, + location = window.location, + encoding = tree.options?.stateEncoding ?? 'adaptive' +) { + const state = decodeTreeState(location.hash ?? '', manifest, encoding); + if (state.applied) applyTreeState(tree, manifest, state); + return state; +}