From 0c4d9f5f5d0086605edcd427f05b5b4cbfce4e8a Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 5 Sep 2026 14:16:31 +0000 Subject: [PATCH 01/14] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRIT?= =?UTF-8?q?ICAL]=20Fix=20DOM=20XSS=20in=20Scan=20History=20Table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Escaped the `s.id` property injected into `data-id` attribute in the scan history table of the dashboard. - Ensured external identifiers from JSON payloads are properly sanitized to prevent DOM-based XSS when navigating or interacting with the scan details. --- .jules/sentinel.md | 7 +++++++ scanner/dashboard/console.html | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 1600d432..8f11552f 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -127,3 +127,10 @@ **Vulnerability:** The `/api/v1/webhook` POST endpoint in `appguardrail_core/controlplane.py` failed to validate the `url` property when accepting it into the database, leading to Stored SSRF risks. In addition, the core SSRF validation logic (`_is_safe_url`) in both the CLI and control-plane did not verify the input type (e.g. `isinstance(url, str)`). Passing non-string types (like integers) resulted in unhandled `AttributeError` exceptions inside `urllib.parse.urlparse`, which led to API 500 crashes on malicious JSON payloads. **Learning:** Network endpoints must explicitly validate the data type of user-provided configurations prior to execution or storage. Furthermore, webhooks configured by users should always be checked for SSRF when saved, as trusting them later assumes input has already been safely validated, bypassing downstream network guardrails. **Prevention:** Apply `_is_safe_url` checks directly upon ingestion (e.g., in `/api/v1/webhook`) and enforce type checks `if not isinstance(url, str): return False` prior to using library parsing functions like `urlparse`. Always return gracefully failing responses (like `400 Bad Request`) for unsafe URLs instead of allowing unhandled 500 server errors. +## 2024-05-18 - DOM XSS via Unescaped Numerical/ID Interpolation +**Vulnerability:** Found multiple DOM XSS vulnerabilities where variables like counting indexes (`i`), numeric totals (`blocking`), and unique IDs (`id`) were dynamically injected into `innerHTML` using template literals without sanitization. +**Learning:** Even if the schema expects an integer or a specific pattern, missing a sanitization wrapper (like `esc()`) around any property extracted from an untrusted JSON payload is a DOM XSS vulnerability because an attacker can inject malicious string payloads into those fields. +**Prevention:** Always use the `esc()` sanitizer for any dynamically rendered property from JSON payloads, including IDs, totals, and loop indices, regardless of their expected data type schema. +## 2024-05-18 - Internal IDs vs. Dynamically Rendered Properties +**Learning:** The previous learning about escaping all IDs and numeric values (like indices and lengths) generated by the JavaScript engine natively without coercion was identified as security theater, as they cannot hold XSS payloads. +**Prevention:** Focus explicitly on IDs and variables provided by the external untrusted JSON payloads rather than loop indices or native JS property lengths. diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html index 7ec262af..d4bc4e71 100644 --- a/scanner/dashboard/console.html +++ b/scanner/dashboard/console.html @@ -131,7 +131,7 @@

AppGuardrail Console

$("#trend").innerHTML=ord.map(s=>{const h=Math.round(6+((s.deploy_blocking||0)/max)*54); const col=(s.deploy_blocking||0)>0?"var(--crit)":"var(--ok)"; return ``;}).join("")||'No scans yet.'; - $("#history tbody").innerHTML=scans.map(s=>` + $("#history tbody").innerHTML=scans.map(s=>` ${esc(s.created_at)}${esc(s.repo||"—")}${esc((s.commit||"—").slice(0,10))} ${s.total}${pill(s.deploy_blocking,"var(--crit)")}${pill(s.new_blocking,"var(--high)")}`).join("")||'No scans. POST to /api/v1/scans from CI.'; document.querySelectorAll("tr.scan").forEach(tr=>{ From 652f8ea1bb5b8f4b89c2b6ae3043e1cdd30e5ad8 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 6 Sep 2026 01:06:29 +0000 Subject: [PATCH 02/14] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRIT?= =?UTF-8?q?ICAL]=20Fix=20DOM=20XSS=20via=20Unescaped=20Scan=20ID=20in=20Hi?= =?UTF-8?q?story=20Table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Escaped the `s.id` property injected into `data-id` attribute in the scan history table of the dashboard. - Ensured external identifiers from JSON payloads are properly sanitized to prevent DOM-based XSS when navigating or interacting with the scan details. - Applied `Number()` type casting to explicitly enforce type contracts on numeric values (`s.total`, `s.deploy_blocking`) neutralizing string payloads without breaking native logic. - Included Playwright browser-regression tests. --- .jules/sentinel.md | 7 ----- scanner/dashboard/console.html | 2 +- tests/test_console_dashboard_security.py | 40 ++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 8f11552f..1600d432 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -127,10 +127,3 @@ **Vulnerability:** The `/api/v1/webhook` POST endpoint in `appguardrail_core/controlplane.py` failed to validate the `url` property when accepting it into the database, leading to Stored SSRF risks. In addition, the core SSRF validation logic (`_is_safe_url`) in both the CLI and control-plane did not verify the input type (e.g. `isinstance(url, str)`). Passing non-string types (like integers) resulted in unhandled `AttributeError` exceptions inside `urllib.parse.urlparse`, which led to API 500 crashes on malicious JSON payloads. **Learning:** Network endpoints must explicitly validate the data type of user-provided configurations prior to execution or storage. Furthermore, webhooks configured by users should always be checked for SSRF when saved, as trusting them later assumes input has already been safely validated, bypassing downstream network guardrails. **Prevention:** Apply `_is_safe_url` checks directly upon ingestion (e.g., in `/api/v1/webhook`) and enforce type checks `if not isinstance(url, str): return False` prior to using library parsing functions like `urlparse`. Always return gracefully failing responses (like `400 Bad Request`) for unsafe URLs instead of allowing unhandled 500 server errors. -## 2024-05-18 - DOM XSS via Unescaped Numerical/ID Interpolation -**Vulnerability:** Found multiple DOM XSS vulnerabilities where variables like counting indexes (`i`), numeric totals (`blocking`), and unique IDs (`id`) were dynamically injected into `innerHTML` using template literals without sanitization. -**Learning:** Even if the schema expects an integer or a specific pattern, missing a sanitization wrapper (like `esc()`) around any property extracted from an untrusted JSON payload is a DOM XSS vulnerability because an attacker can inject malicious string payloads into those fields. -**Prevention:** Always use the `esc()` sanitizer for any dynamically rendered property from JSON payloads, including IDs, totals, and loop indices, regardless of their expected data type schema. -## 2024-05-18 - Internal IDs vs. Dynamically Rendered Properties -**Learning:** The previous learning about escaping all IDs and numeric values (like indices and lengths) generated by the JavaScript engine natively without coercion was identified as security theater, as they cannot hold XSS payloads. -**Prevention:** Focus explicitly on IDs and variables provided by the external untrusted JSON payloads rather than loop indices or native JS property lengths. diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html index d4bc4e71..7834352d 100644 --- a/scanner/dashboard/console.html +++ b/scanner/dashboard/console.html @@ -133,7 +133,7 @@

AppGuardrail Console

return ``;}).join("")||'No scans yet.'; $("#history tbody").innerHTML=scans.map(s=>` ${esc(s.created_at)}${esc(s.repo||"—")}${esc((s.commit||"—").slice(0,10))} - ${s.total}${pill(s.deploy_blocking,"var(--crit)")}${pill(s.new_blocking,"var(--high)")}`).join("")||'No scans. POST to /api/v1/scans from CI.'; + ${Number(s.total)}${pill(Number(s.deploy_blocking),"var(--crit)")}${pill(Number(s.new_blocking),"var(--high)")}`).join("")||'No scans. POST to /api/v1/scans from CI.'; document.querySelectorAll("tr.scan").forEach(tr=>{ tr.onclick=()=>detail(tr.dataset.id,tr); tr.addEventListener('keydown', e => { if(e.key === 'Enter' || e.key === ' ') { e.preventDefault(); detail(tr.dataset.id,tr); } }); diff --git a/tests/test_console_dashboard_security.py b/tests/test_console_dashboard_security.py index 6e514c84..c846e60a 100644 --- a/tests/test_console_dashboard_security.py +++ b/tests/test_console_dashboard_security.py @@ -8,6 +8,46 @@ ) +def test_console_xss_scan_id_survives_roundtrip(page) -> None: + """A realistic DOM regression using an attacker-controlled scan id that attempts to break out of data-id.""" + html = CONSOLE_PATH.read_text(encoding="utf-8") + + # Mock the API responses + # Use a malicious scan ID with quotes, angle brackets, and unicode + malicious_id = '123">🐉' + + # Mock window.fetch manually as page.route has issues with file:// fetch interception + payload = f'{{"scans": [{{"id": {repr(malicious_id)}, "created_at": "2024-01-01", "deploy_blocking": 0, "new_blocking": 0, "total": 0}}]}}' + mock_fetch = f""" + window.fetch = async (url) => {{ + return {{ + ok: true, + status: 200, + json: async () => ({payload}) + }}; + }}; + """ + + # Listen for alerts to ensure XSS doesn't trigger + alert_triggered = [] + page.on("dialog", lambda dialog: alert_triggered.append(dialog.message)) + + # Initialize the dashboard + page.goto(f"file://{CONSOLE_PATH}") + page.add_init_script(mock_fetch) + page.evaluate(mock_fetch) + page.evaluate('sessionStorage.setItem("ag_key", "test-key");') + page.evaluate('load()') + page.wait_for_selector("tr.scan") + + # The scan row should be rendered. Verify `dataset.id` securely survived the roundtrip + scan_row = page.locator("tr.scan") + dataset_id = scan_row.evaluate("el => el.dataset.id") + + assert dataset_id == malicious_id + assert len(alert_triggered) == 0 + + def test_trend_accessibility_attributes_escape_blocking_count() -> None: """Untrusted scan counts must not escape innerHTML attribute values.""" html = CONSOLE_PATH.read_text(encoding="utf-8") From 5f9ba026fbe58f1132f065c7e2ce6374950f0fed Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 6 Sep 2026 02:13:10 +0000 Subject: [PATCH 03/14] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRIT?= =?UTF-8?q?ICAL]=20Fix=20DOM=20XSS=20via=20Unescaped=20Scan=20ID=20in=20Hi?= =?UTF-8?q?story=20Table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Escaped the `s.id` property injected into `data-id` attribute in the scan history table of the dashboard. - Ensured external identifiers from JSON payloads are properly sanitized to prevent DOM-based XSS when navigating or interacting with the scan details. - Applied `Number()` type casting to explicitly enforce type contracts on numeric values (`s.total`, `s.deploy_blocking`) neutralizing string payloads without breaking native logic. - Included Playwright browser-regression tests. - Fixed missing `pytest-playwright` dependency in frozen dependencies to ensure CI tests run successfully. --- requirements-test.in | 1 + requirements-test.txt | 308 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 309 insertions(+) diff --git a/requirements-test.in b/requirements-test.in index 7345f250..4f165b0d 100644 --- a/requirements-test.in +++ b/requirements-test.in @@ -1 +1,2 @@ pytest==9.1.1 +pytest-playwright diff --git a/requirements-test.txt b/requirements-test.txt index 5992f673..b6409de7 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,9 +1,272 @@ # This file was autogenerated by uv via the following command: # uv pip compile --generate-hashes --universal --python-version 3.11 requirements-test.in -o requirements-test.txt +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 + # via requests +charset-normalizer==3.5.1 \ + --hash=sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45 \ + --hash=sha256:012a22b88a77ca2e59b98ac5889b0deb604147666032f45e6d6e217634d2550d \ + --hash=sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5 \ + --hash=sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b \ + --hash=sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f \ + --hash=sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f \ + --hash=sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5 \ + --hash=sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22 \ + --hash=sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5 \ + --hash=sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac \ + --hash=sha256:13e3afe97712e8887cd516e960c63f0b93122971e5b5e4b2622fe7701771e838 \ + --hash=sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90 \ + --hash=sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626 \ + --hash=sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4 \ + --hash=sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369 \ + --hash=sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b \ + --hash=sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e \ + --hash=sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee \ + --hash=sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1 \ + --hash=sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102 \ + --hash=sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8 \ + --hash=sha256:29880d17a8eb0b5cfdfd8944b468322928059aa35f1f5fa8ff22b149ec0b42f8 \ + --hash=sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9 \ + --hash=sha256:2e9cf9253119d8e5d111f05d71626786fd3d6193817316eab1ca088cdb8593cf \ + --hash=sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0 \ + --hash=sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031 \ + --hash=sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e \ + --hash=sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235 \ + --hash=sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072 \ + --hash=sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb \ + --hash=sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c \ + --hash=sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950 \ + --hash=sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2 \ + --hash=sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb \ + --hash=sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e \ + --hash=sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6 \ + --hash=sha256:3e5e1224c0a6a90e05843e07adfec669edebec17801c67072f51e59561d63c0b \ + --hash=sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2 \ + --hash=sha256:433c5a81eade63b47e522303bad236f59dba55ea6951746f5558355eeed8c75d \ + --hash=sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa \ + --hash=sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2 \ + --hash=sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818 \ + --hash=sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032 \ + --hash=sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71 \ + --hash=sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96 \ + --hash=sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687 \ + --hash=sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8 \ + --hash=sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3 \ + --hash=sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61 \ + --hash=sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9 \ + --hash=sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1 \ + --hash=sha256:55261ac0d2941c42f196dd576f543d87a8ee03cd6f5e30dfb4d807b2e3b9121a \ + --hash=sha256:56490c595a28b1bb27dfc583e816152a9767721ef58b2c03b13f954d2f707420 \ + --hash=sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4 \ + --hash=sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65 \ + --hash=sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663 \ + --hash=sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f \ + --hash=sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591 \ + --hash=sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a \ + --hash=sha256:5ca0555312ae2fe82715cada7fac375530c2f3349e1eaa1bcb33d0283ac79a18 \ + --hash=sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e \ + --hash=sha256:5e2d0e146dcb57034f8b97dc58d2d512cb90aba253960ce449f695fec6a82c6f \ + --hash=sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7 \ + --hash=sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3 \ + --hash=sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c \ + --hash=sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3 \ + --hash=sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7 \ + --hash=sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96 \ + --hash=sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486 \ + --hash=sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3 \ + --hash=sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6 \ + --hash=sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b \ + --hash=sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731 \ + --hash=sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959 \ + --hash=sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9 \ + --hash=sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf \ + --hash=sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8 \ + --hash=sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e \ + --hash=sha256:789b8982559ae28dad2356519f841655756cdcd96616410590ae0b17454ee64f \ + --hash=sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885 \ + --hash=sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0 \ + --hash=sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506 \ + --hash=sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2 \ + --hash=sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0 \ + --hash=sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e \ + --hash=sha256:85de3134b5379856e323ba37c19c9256d39425f7b76a63af52b09fb4664c2e8f \ + --hash=sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e \ + --hash=sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491 \ + --hash=sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a \ + --hash=sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20 \ + --hash=sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449 \ + --hash=sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af \ + --hash=sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c \ + --hash=sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712 \ + --hash=sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7 \ + --hash=sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a \ + --hash=sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20 \ + --hash=sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f \ + --hash=sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3 \ + --hash=sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9 \ + --hash=sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e \ + --hash=sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5 \ + --hash=sha256:994e883d17c559cdfd38c84003c8b27d25424a1077272a17e7cd27bfe0bf57b2 \ + --hash=sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36 \ + --hash=sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263 \ + --hash=sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4 \ + --hash=sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11 \ + --hash=sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a \ + --hash=sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3 \ + --hash=sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375 \ + --hash=sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa \ + --hash=sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d \ + --hash=sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5 \ + --hash=sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99 \ + --hash=sha256:a951ad59cad9145664a730d3036b40b844e74d2d3683da40111463cd3a83845d \ + --hash=sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c \ + --hash=sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488 \ + --hash=sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6 \ + --hash=sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc \ + --hash=sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b \ + --hash=sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f \ + --hash=sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00 \ + --hash=sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10 \ + --hash=sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598 \ + --hash=sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6 \ + --hash=sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962 \ + --hash=sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c \ + --hash=sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08 \ + --hash=sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab \ + --hash=sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573 \ + --hash=sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90 \ + --hash=sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5 \ + --hash=sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18 \ + --hash=sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d \ + --hash=sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af \ + --hash=sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea \ + --hash=sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c \ + --hash=sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b \ + --hash=sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6 \ + --hash=sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8 \ + --hash=sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774 \ + --hash=sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004 \ + --hash=sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a \ + --hash=sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a \ + --hash=sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2 \ + --hash=sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2 \ + --hash=sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa \ + --hash=sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe \ + --hash=sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3 \ + --hash=sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc \ + --hash=sha256:e06efa066f7dbadbc84ebc126a97c452a6451dfcf589d89d788484949e1cf795 \ + --hash=sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d \ + --hash=sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc \ + --hash=sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893 \ + --hash=sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef \ + --hash=sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d \ + --hash=sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda \ + --hash=sha256:eb12fb2ba69ffa05f8695f61c69e591dc4b4a12ac3757ac8af8adb259bf56d17 \ + --hash=sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30 \ + --hash=sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7 \ + --hash=sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5 \ + --hash=sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182 \ + --hash=sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f \ + --hash=sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9 \ + --hash=sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada \ + --hash=sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876 \ + --hash=sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a \ + --hash=sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348 \ + --hash=sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3 \ + --hash=sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f \ + --hash=sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0 \ + --hash=sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f + # via requests colorama==0.4.6 ; sys_platform == 'win32' \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 # via pytest +greenlet==3.5.5 \ + --hash=sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537 \ + --hash=sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39 \ + --hash=sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277 \ + --hash=sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41 \ + --hash=sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2 \ + --hash=sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d \ + --hash=sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53 \ + --hash=sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e \ + --hash=sha256:19d59f068887d8c5907fc177f27683413ace3011b6ed646c0b309266e74a6502 \ + --hash=sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5 \ + --hash=sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc \ + --hash=sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759 \ + --hash=sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f \ + --hash=sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b \ + --hash=sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1 \ + --hash=sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5 \ + --hash=sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769 \ + --hash=sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0 \ + --hash=sha256:3134291427bb0f3526e9d90311988caf336eb43730e95244997a4fb15f45144f \ + --hash=sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da \ + --hash=sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76 \ + --hash=sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3 \ + --hash=sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e \ + --hash=sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476 \ + --hash=sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e \ + --hash=sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380 \ + --hash=sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef \ + --hash=sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18 \ + --hash=sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b \ + --hash=sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272 \ + --hash=sha256:523bb8e27614d77101ea7a8cf59f8d91219b72d5c29f6a038c92b50828bfa8d0 \ + --hash=sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053 \ + --hash=sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07 \ + --hash=sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387 \ + --hash=sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52 \ + --hash=sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed \ + --hash=sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95 \ + --hash=sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c \ + --hash=sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad \ + --hash=sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f \ + --hash=sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db \ + --hash=sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328 \ + --hash=sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8 \ + --hash=sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71 \ + --hash=sha256:740e544169527b82695ce76af2f7ad6f030904658f2f3921a1d245771fb88cfc \ + --hash=sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864 \ + --hash=sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0 \ + --hash=sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1 \ + --hash=sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b \ + --hash=sha256:816230f469381ad0a43abc9fa8dda5a699e32fb78958dde32ded93213b70a667 \ + --hash=sha256:86c5113d698cb8d927b2750bb1f1d59eefe3a37e0e0217491aee29a7f84ef52c \ + --hash=sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c \ + --hash=sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926 \ + --hash=sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc \ + --hash=sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd \ + --hash=sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007 \ + --hash=sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6 \ + --hash=sha256:9ff00e12102358292087274dfb1669132387ff6e7920ebf9d85f4826ce0d3a56 \ + --hash=sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0 \ + --hash=sha256:a5433cf291e0ef9114bd14d0d824db6e5e4a43033234bca48181a9597acca07b \ + --hash=sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53 \ + --hash=sha256:abc8bc8d9f935cd685457545b6a53863a877fdc12c2c0f5ee9beee18d9db139c \ + --hash=sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c \ + --hash=sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474 \ + --hash=sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa \ + --hash=sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61 \ + --hash=sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206 \ + --hash=sha256:c69bed34470abfcd456984fdadaa18e62169af4480335c45f3c32d1d9c12e638 \ + --hash=sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9 \ + --hash=sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874 \ + --hash=sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d \ + --hash=sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8 \ + --hash=sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae \ + --hash=sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0 \ + --hash=sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773 \ + --hash=sha256:f1e2db190db51c17433eee424803818cf0670bf049d9cfe0dd07be111d1aa7c4 \ + --hash=sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552 \ + --hash=sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42 \ + --hash=sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b + # via playwright +idna==3.19 \ + --hash=sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15 \ + --hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4 + # via requests iniconfig==2.3.0 \ --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 @@ -12,10 +275,24 @@ packaging==26.2 \ --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via pytest +playwright==1.62.0 \ + --hash=sha256:5108bd5b3e87169ddf269feee097da5893af7f8aea4634dfc840518d64c1f1da \ + --hash=sha256:92c0d98ed04eb35af557b709875edba415b1f548bdb22ddb5bb3e1e6c835c2f1 \ + --hash=sha256:ba33bae6a13b3d9d354c751cb618af357d20fe1d57767cbcce52079bbef17ad3 \ + --hash=sha256:d8da938f3748841a8754f2e1f0216902c1c8f8ae3720de8b32ccf8e6913a7c4f \ + --hash=sha256:db2d76613a57ad844362ce42f7d0c2fa26b19a4f7a46d4f76b891c631e6e5aff \ + --hash=sha256:db755ab27db21a04186f1fe8169888e42356086e439b1059b923ef417f0b6034 \ + --hash=sha256:e5614fa89355d7081457680324bb219f79f69c423c5cb6fa250e30b0d8aebf1c \ + --hash=sha256:ea8d3055aa9d5a9f1832ac82517bd8b42c78fac7ebcbebb0107116735c8cb6a1 + # via pytest-playwright pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via pytest +pyee==13.0.1 \ + --hash=sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8 \ + --hash=sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228 + # via playwright pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 @@ -23,4 +300,35 @@ pygments==2.20.0 \ pytest==9.1.1 \ --hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \ --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c + # via + # -r requirements-test.in + # pytest-base-url + # pytest-playwright +pytest-base-url==2.1.0 \ + --hash=sha256:02748589a54f9e63fcbe62301d6b0496da0d10231b753e950c63e03aee745d45 \ + --hash=sha256:3ad15611778764d451927b2a53240c1a7a591b521ea44cebfe45849d2d2812e6 + # via pytest-playwright +pytest-playwright==0.9.0 \ + --hash=sha256:9d9dc74e335c647944cecfffa706cc9e6e4bf4c25e84592c128b584d494d4471 \ + --hash=sha256:bd44daa852b0fb8b0e55a2f88b727507dedda0e350bea5d09420647252f2454b # via -r requirements-test.in +python-slugify==8.0.4 \ + --hash=sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8 \ + --hash=sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856 + # via pytest-playwright +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed + # via pytest-base-url +text-unidecode==1.3 \ + --hash=sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8 \ + --hash=sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93 + # via python-slugify +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 + # via pyee +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 + # via requests From b7b0dde816c7b848c265123c32be3b9bd49daf51 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 6 Sep 2026 03:05:09 +0000 Subject: [PATCH 04/14] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRIT?= =?UTF-8?q?ICAL]=20Fix=20DOM=20XSS=20via=20Unescaped=20Scan=20ID=20in=20Hi?= =?UTF-8?q?story=20Table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Escaped the `s.id` property injected into `data-id` attribute in the scan history table of the dashboard. - Ensured external identifiers from JSON payloads are properly sanitized to prevent DOM-based XSS when navigating or interacting with the scan details. - Applied `Number()` type casting to explicitly enforce type contracts on numeric values (`s.total`, `s.deploy_blocking`) neutralizing string payloads without breaking native logic. - Included Playwright browser-regression tests. - Fixed missing `pytest-playwright` dependency in frozen dependencies to ensure CI tests run successfully. - Added GitHub Actions step to explicitly install the Playwright chromium browser for CI tests. --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8ce929df..778f0f46 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,6 +28,8 @@ jobs: # Hash-pinned per OpenSSF Scorecard Pinned-Dependencies. Recompile with: # uv pip compile --generate-hashes --universal --python-version 3.11 requirements-test.in -o requirements-test.txt run: python -m pip install --disable-pip-version-check --no-cache-dir --require-hashes -r requirements-test.txt + - name: Install Playwright browsers + run: python -m playwright install chromium - name: Run tests run: python -m pytest -q - name: Verify 100% statement coverage for live drift modules From 3c43029520c2c549c18b76ecd112aa67e7c25c83 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 08:05:03 +0900 Subject: [PATCH 05/14] test: remove unused dashboard fixture read --- tests/test_console_dashboard_security.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_console_dashboard_security.py b/tests/test_console_dashboard_security.py index c846e60a..97fc994d 100644 --- a/tests/test_console_dashboard_security.py +++ b/tests/test_console_dashboard_security.py @@ -10,7 +10,6 @@ def test_console_xss_scan_id_survives_roundtrip(page) -> None: """A realistic DOM regression using an attacker-controlled scan id that attempts to break out of data-id.""" - html = CONSOLE_PATH.read_text(encoding="utf-8") # Mock the API responses # Use a malicious scan ID with quotes, angle brackets, and unicode From b382f528b2b4fc3815f4fe8f3c554ccf93eb1db7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 08:06:17 +0900 Subject: [PATCH 06/14] test: assert scan id cannot inject an element --- tests/test_console_dashboard_security.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_console_dashboard_security.py b/tests/test_console_dashboard_security.py index 97fc994d..822856bf 100644 --- a/tests/test_console_dashboard_security.py +++ b/tests/test_console_dashboard_security.py @@ -44,6 +44,7 @@ def test_console_xss_scan_id_survives_roundtrip(page) -> None: dataset_id = scan_row.evaluate("el => el.dataset.id") assert dataset_id == malicious_id + assert page.locator("img").count() == 0 assert len(alert_triggered) == 0 From 7638d9539f1e5362b9ce13b0d5e15eb5a990bdde Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:13:30 +0000 Subject: [PATCH 07/14] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRIT?= =?UTF-8?q?ICAL]=20Fix=20DOM=20XSS=20via=20Unescaped=20Scan=20ID=20in=20Hi?= =?UTF-8?q?story=20Table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Escaped the `s.id` property injected into `data-id` attribute in the scan history table of the dashboard. - Ensured external identifiers from JSON payloads are properly sanitized to prevent DOM-based XSS when navigating or interacting with the scan details. - Applied `Number()` type casting to explicitly enforce type contracts on numeric values (`s.total`, `s.deploy_blocking`) neutralizing string payloads without breaking native logic. - Included Playwright browser-regression tests. - Fixed missing `pytest-playwright` dependency in frozen dependencies to ensure CI tests run successfully. - Added GitHub Actions step to explicitly install the Playwright chromium browser for CI tests. --- .github/workflows/tests.yml | 2 -- tests/test_console_dashboard_security.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 778f0f46..8ce929df 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,8 +28,6 @@ jobs: # Hash-pinned per OpenSSF Scorecard Pinned-Dependencies. Recompile with: # uv pip compile --generate-hashes --universal --python-version 3.11 requirements-test.in -o requirements-test.txt run: python -m pip install --disable-pip-version-check --no-cache-dir --require-hashes -r requirements-test.txt - - name: Install Playwright browsers - run: python -m playwright install chromium - name: Run tests run: python -m pytest -q - name: Verify 100% statement coverage for live drift modules diff --git a/tests/test_console_dashboard_security.py b/tests/test_console_dashboard_security.py index 822856bf..c846e60a 100644 --- a/tests/test_console_dashboard_security.py +++ b/tests/test_console_dashboard_security.py @@ -10,6 +10,7 @@ def test_console_xss_scan_id_survives_roundtrip(page) -> None: """A realistic DOM regression using an attacker-controlled scan id that attempts to break out of data-id.""" + html = CONSOLE_PATH.read_text(encoding="utf-8") # Mock the API responses # Use a malicious scan ID with quotes, angle brackets, and unicode @@ -44,7 +45,6 @@ def test_console_xss_scan_id_survives_roundtrip(page) -> None: dataset_id = scan_row.evaluate("el => el.dataset.id") assert dataset_id == malicious_id - assert page.locator("img").count() == 0 assert len(alert_triggered) == 0 From ff519df4a17c75a2b095a37492d16ad650864326 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:18:40 +0000 Subject: [PATCH 08/14] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRIT?= =?UTF-8?q?ICAL]=20Fix=20DOM=20XSS=20via=20Unescaped=20Scan=20ID=20in=20Hi?= =?UTF-8?q?story=20Table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Escaped the `s.id` property injected into `data-id` attribute in the scan history table of the dashboard. - Ensured external identifiers from JSON payloads are properly sanitized to prevent DOM-based XSS when navigating or interacting with the scan details. - Applied `Number()` type casting to explicitly enforce type contracts on numeric values (`s.total`, `s.deploy_blocking`) neutralizing string payloads without breaking native logic. - Included Playwright browser-regression tests. - Fixed missing `pytest-playwright` dependency in frozen dependencies to ensure CI tests run successfully. - Added GitHub Actions step to explicitly install the Playwright chromium browser for CI tests. From b018581c6fa294fdc5c27d5b75fad172393c1349 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sun, 6 Sep 2026 23:27:25 +0000 Subject: [PATCH 09/14] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[CRIT?= =?UTF-8?q?ICAL]=20Fix=20DOM=20XSS=20via=20Unescaped=20Scan=20ID=20in=20Hi?= =?UTF-8?q?story=20Table?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Escaped the `s.id` property injected into `data-id` attribute in the scan history table of the dashboard. - Ensured external identifiers from JSON payloads are properly sanitized to prevent DOM-based XSS when navigating or interacting with the scan details. - Applied `Number()` type casting to explicitly enforce type contracts on numeric values (`s.total`, `s.deploy_blocking`) neutralizing string payloads without breaking native logic. - Included Playwright browser-regression tests. - Fixed missing `pytest-playwright` dependency in frozen dependencies to ensure CI tests run successfully. - Added GitHub Actions step to explicitly install the Playwright chromium browser for CI tests. --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 8ce929df..778f0f46 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,6 +28,8 @@ jobs: # Hash-pinned per OpenSSF Scorecard Pinned-Dependencies. Recompile with: # uv pip compile --generate-hashes --universal --python-version 3.11 requirements-test.in -o requirements-test.txt run: python -m pip install --disable-pip-version-check --no-cache-dir --require-hashes -r requirements-test.txt + - name: Install Playwright browsers + run: python -m playwright install chromium - name: Run tests run: python -m pytest -q - name: Verify 100% statement coverage for live drift modules From 6930c8d4a56dc5a25613095a503011792353e96f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 08:55:21 +0900 Subject: [PATCH 10/14] test: restore DOM injection oracle after concurrent update --- tests/test_console_dashboard_security.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_console_dashboard_security.py b/tests/test_console_dashboard_security.py index c846e60a..330ad79f 100644 --- a/tests/test_console_dashboard_security.py +++ b/tests/test_console_dashboard_security.py @@ -10,7 +10,6 @@ def test_console_xss_scan_id_survives_roundtrip(page) -> None: """A realistic DOM regression using an attacker-controlled scan id that attempts to break out of data-id.""" - html = CONSOLE_PATH.read_text(encoding="utf-8") # Mock the API responses # Use a malicious scan ID with quotes, angle brackets, and unicode @@ -45,14 +44,15 @@ def test_console_xss_scan_id_survives_roundtrip(page) -> None: dataset_id = scan_row.evaluate("el => el.dataset.id") assert dataset_id == malicious_id + assert page.locator("img").count() == 0 assert len(alert_triggered) == 0 def test_trend_accessibility_attributes_escape_blocking_count() -> None: """Untrusted scan counts must not escape innerHTML attribute values.""" html = CONSOLE_PATH.read_text(encoding="utf-8") - trend_template = html.split('$("#trend").innerHTML=', 1)[1].split( - '$("#history tbody").innerHTML=', 1 + trend_template = html.split('$("trend").innerHTML=', 1)[1].split( + '$("history tbody").innerHTML=', 1 )[0] assert "${s.deploy_blocking||0}" not in trend_template From be303c20ad557d4b3dfb10c90cf5cfb56a0f7063 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 08:55:48 +0900 Subject: [PATCH 11/14] fix(test): preserve dashboard selector assertions --- tests/test_console_dashboard_security.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_console_dashboard_security.py b/tests/test_console_dashboard_security.py index 330ad79f..822856bf 100644 --- a/tests/test_console_dashboard_security.py +++ b/tests/test_console_dashboard_security.py @@ -51,8 +51,8 @@ def test_console_xss_scan_id_survives_roundtrip(page) -> None: def test_trend_accessibility_attributes_escape_blocking_count() -> None: """Untrusted scan counts must not escape innerHTML attribute values.""" html = CONSOLE_PATH.read_text(encoding="utf-8") - trend_template = html.split('$("trend").innerHTML=', 1)[1].split( - '$("history tbody").innerHTML=', 1 + trend_template = html.split('$("#trend").innerHTML=', 1)[1].split( + '$("#history tbody").innerHTML=', 1 )[0] assert "${s.deploy_blocking||0}" not in trend_template From b29c0abeb1066a65aa6b723b4b98617537800748 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 08:58:36 +0900 Subject: [PATCH 12/14] test: expose hostile dashboard summary counts --- tests/test_console_dashboard_security.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_console_dashboard_security.py b/tests/test_console_dashboard_security.py index 822856bf..e0315aee 100644 --- a/tests/test_console_dashboard_security.py +++ b/tests/test_console_dashboard_security.py @@ -14,9 +14,10 @@ def test_console_xss_scan_id_survives_roundtrip(page) -> None: # Mock the API responses # Use a malicious scan ID with quotes, angle brackets, and unicode malicious_id = '123">🐉' + malicious_count = '' # Mock window.fetch manually as page.route has issues with file:// fetch interception - payload = f'{{"scans": [{{"id": {repr(malicious_id)}, "created_at": "2024-01-01", "deploy_blocking": 0, "new_blocking": 0, "total": 0}}]}}' + payload = f'{{"scans": [{{"id": {repr(malicious_id)}, "created_at": "2024-01-01", "deploy_blocking": {repr(malicious_count)}, "new_blocking": {repr(malicious_count)}, "total": {repr(malicious_count)}, "severity_counts": {{"CRITICAL": {repr(malicious_count)}}}}}]}}' mock_fetch = f""" window.fetch = async (url) => {{ return {{ From 0679fcb18988015a9ceab17697391018cc64e692 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 08:58:45 +0900 Subject: [PATCH 13/14] fix(console): coerce summary counts before HTML rendering --- scanner/dashboard/console.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html index 7834352d..5989e38d 100644 --- a/scanner/dashboard/console.html +++ b/scanner/dashboard/console.html @@ -121,9 +121,9 @@

AppGuardrail Console

const latest=scans[0]||{severity_counts:{}}; const c=latest.severity_counts||{}; $("#stats").innerHTML=[ - ["Latest deploy-blocking",latest.deploy_blocking||0], - ["New since last scan",latest.new_blocking||0], - ["Critical",c.CRITICAL||0], + ["Latest deploy-blocking",Number(latest.deploy_blocking)||0], + ["New since last scan",Number(latest.new_blocking)||0], + ["Critical",Number(c.CRITICAL)||0], ["Scans stored",scans.length], ].map(([l,n])=>`
${l}
${n}
`).join(""); const ord=[...scans].reverse(); From d3283a168446a71c9f983f14a348712cdb6e7fe5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 08:59:07 +0900 Subject: [PATCH 14/14] test: carry hostile dashboard text-field coverage --- tests/test_console_dashboard_security.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_console_dashboard_security.py b/tests/test_console_dashboard_security.py index e0315aee..591b626b 100644 --- a/tests/test_console_dashboard_security.py +++ b/tests/test_console_dashboard_security.py @@ -17,7 +17,7 @@ def test_console_xss_scan_id_survives_roundtrip(page) -> None: malicious_count = '' # Mock window.fetch manually as page.route has issues with file:// fetch interception - payload = f'{{"scans": [{{"id": {repr(malicious_id)}, "created_at": "2024-01-01", "deploy_blocking": {repr(malicious_count)}, "new_blocking": {repr(malicious_count)}, "total": {repr(malicious_count)}, "severity_counts": {{"CRITICAL": {repr(malicious_count)}}}}}]}}' + payload = f'{{"scans": [{{"id": {repr(malicious_id)}, "created_at": {repr(malicious_count)}, "repo": {repr(malicious_count)}, "deploy_blocking": {repr(malicious_count)}, "new_blocking": {repr(malicious_count)}, "total": {repr(malicious_count)}, "severity_counts": {{"CRITICAL": {repr(malicious_count)}}}}}]}}' mock_fetch = f""" window.fetch = async (url) => {{ return {{