From 144b823be3a49208e60a06cb8dfd4dd8385ea143 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:21:12 +0000 Subject: [PATCH 01/57] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=81=EC=A0=81=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=8F=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=EC=84=B1=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ’‘ What - `scanner/dashboard/console.html`의 λ²„νŠΌμ— `:hover` 및 `:disabled` μ‹œκ°μ  ν”Όλ“œλ°± μΆ”κ°€ - `.close-btn` μš”μ†Œμ— `title="Close (Esc)"` 툴팁 μΆ”κ°€ - `tr.scan`의 λ‘œλ”© μƒνƒœ(`aria-busy="true"`)에 λŒ€ν•œ μ‹œκ°μ  ν”Όλ“œλ°±(opacity, pointer-events) μΆ”κ°€ - `scanner/dashboard/index.html`의 "Clear filters" λ²„νŠΌ μŠ€νƒ€μΌμ„ CSS 클래슀(`.clear-btn`)둜 λΆ„λ¦¬ν•˜κ³  hover 효과 μΆ”κ°€ 🎯 Why - 슀크린 리더 μ‚¬μš©μžλΏλ§Œ μ•„λ‹ˆλΌ μ‹œκ° μž₯μ• κ°€ μ—†λŠ” 일반 μ‚¬μš©μžλ„ ARIA μƒνƒœ(`aria-busy`)λ‚˜ λΉ„ν™œμ„±ν™”(`disabled`) μƒνƒœλ₯Ό λͺ…ν™•νžˆ 인지할 수 μžˆλ„λ‘ μ‹œκ°μ  ν”Όλ“œλ°±μ„ μ œκ³΅ν•˜μ—¬ μ‚¬μš©μ„±μ„ ν–₯μƒμ‹œν‚€κΈ° μœ„ν•¨μž…λ‹ˆλ‹€. - 인라인 μŠ€νƒ€μΌμ„ 클래슀둜 λΆ„λ¦¬ν•˜μ—¬ μ½”λ“œ μœ μ§€λ³΄μˆ˜μ„±μ„ λ†’μ˜€μŠ΅λ‹ˆλ‹€. πŸ“Έ Before/After (λΉ„μ£Όμ–Ό λ³€κ²½ μ‚¬ν•­μœΌλ‘œ, PR 리뷰어 ν™•μΈμš© μŠ€ν¬λ¦°μƒ·μ΄ μ²¨λΆ€λ©λ‹ˆλ‹€.) β™Ώ Accessibility - ARIA μƒνƒœ(`aria-busy="true"`)와 μ‹œκ°μ  UI κ°„μ˜ 동기화(parity) 달성 - λ‹«κΈ° λ²„νŠΌμ— 마우슀 ν˜Έλ²„ μ‹œ 툴팁(`title`)을 μ œκ³΅ν•˜μ—¬ λͺ…ν™•ν•œ μ»¨ν…μŠ€νŠΈ 전달 --- .jules/palette.md | 4 ++++ scanner/dashboard/console.html | 9 ++++++--- scanner/dashboard/index.html | 4 +++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.jules/palette.md b/.jules/palette.md index ea004e2d..63fa19c2 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -81,3 +81,7 @@ ## 2026-08-12 - Skip to Content Accessibility **Learning:** Screen reader and keyboard-only users experience significant friction when forced to navigate through repetitive header controls on every page load. **Action:** Keep a visible-on-focus skip link as the first interactive element, target a programmatically focusable main container, and give the focused link a high-contrast outline. + +## 2026-08-12 - Visual Parity for ARIA States +**Learning:** Framework-less HTML elements with ARIA attributes (like `aria-busy="true"`) or semantic states (like `:disabled`) often lack native visual styling, leaving screen reader users informed but sighted users confused without explicit visual feedback. +**Action:** Always map ARIA and disabled states to CSS visual styles (e.g., `opacity`, `cursor: not-allowed`, or `pointer-events: none`) to ensure parity between the visual UI and the accessibility tree. diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html index 7ec262af..3c5590e3 100644 --- a/scanner/dashboard/console.html +++ b/scanner/dashboard/console.html @@ -24,7 +24,9 @@ h1{font-size:16px;margin:0;font-weight:700} main{max-width:1000px;margin:0 auto;padding:20px} input{font:inherit;padding:8px 10px;border:1px solid var(--border);border-radius:8px;min-width:280px} - button{font:inherit;font-weight:600;padding:8px 14px;border:0;border-radius:8px;background:var(--primary);color:var(--on-primary);cursor:pointer} + button{font:inherit;font-weight:600;padding:8px 14px;border:0;border-radius:8px;background:var(--primary);color:var(--on-primary);cursor:pointer;transition:filter 0.2s, opacity 0.2s} + button:hover:not(:disabled){filter:brightness(0.9)} + button:disabled{opacity:0.6;cursor:not-allowed} button.ghost{background:var(--surface);color:var(--primary);border:1px solid var(--border)} .card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px 18px;margin-bottom:16px} .stats{display:flex;gap:12px;flex-wrap:wrap} @@ -36,6 +38,7 @@ th{font-size:11px;color:var(--muted);text-transform:uppercase} tr.scan{cursor:pointer} tr.scan:hover{background:var(--bg)} + tr.scan[aria-busy="true"]{opacity:0.7;pointer-events:none} input:focus-visible, button:focus-visible, tr.scan:focus-visible, .bar:focus-visible, #detail:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; } .close-btn{float:right;border:0;background:transparent;font-size:16px;cursor:pointer;color:var(--muted);padding:0 4px;margin-top:-2px} .close-btn:hover{color:var(--text)} @@ -158,7 +161,7 @@

AppGuardrail Console

const rows=(s.findings||[]).map(f=>`${esc(f.severity)} ${esc(f.rule_id)}${esc((f.message||"").split("\n")[0].slice(0,120))} ${esc(f.file)}:${esc(f.line)}`).join(""); - d.innerHTML=`Scan #${esc(s.id)} ${esc(s.created_at)} Β· ${esc(s.repo||"β€”")} + d.innerHTML=`Scan #${esc(s.id)} ${esc(s.created_at)} Β· ${esc(s.repo||"β€”")} ${rows||''}
SeverityRuleFindingLocation
No findings.
`; d.querySelector(".close-btn").addEventListener("click",closeDetail); @@ -166,7 +169,7 @@

AppGuardrail Console

d.focus({preventScroll:true}); }catch(e){ if(requestId!==currentDetailRequest)return; - d.innerHTML=''; + d.innerHTML=''; d.querySelector(".close-btn").addEventListener("click",closeDetail); scrollDetailIntoView(d); d.focus({preventScroll:true}); diff --git a/scanner/dashboard/index.html b/scanner/dashboard/index.html index 132bc31b..46589446 100644 --- a/scanner/dashboard/index.html +++ b/scanner/dashboard/index.html @@ -78,6 +78,8 @@ .empty code{background:var(--bg);padding:2px 6px;border-radius:4px;color:var(--text)} .primary-action{min-height:44px;padding:10px 16px;border:1px solid var(--primary);border-radius:8px;background:var(--primary);color:var(--on-primary);cursor:pointer;font:inherit;font-weight:700} .primary-action:hover{filter:brightness(.94)} + .clear-btn{padding:6px 12px;border-radius:6px;border:1px solid var(--border);background:var(--surface);cursor:pointer;font:inherit;color:var(--text);font-weight:500;transition:background 0.2s, filter 0.2s} + .clear-btn:hover{filter:brightness(0.95)} .drop{margin-top:14px} body.drag-active::after{content:"Drop findings.json here";position:fixed;inset:0;z-index:99;background:rgba(37,110,244,.08);border:4px dashed var(--primary);display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:700;color:var(--primary);pointer-events:none;backdrop-filter:blur(2px)} .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0} @@ -235,7 +237,7 @@

Dashboard

SeverityFindingFileCategoryStatus ${rows||`
No findings match the filter
- + `} From e69faa3f519c4ade5ffc37b398467214030f01aa Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:53:53 +0000 Subject: [PATCH 02/57] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=99=B8?= =?UTF-8?q?=EB=B6=80=20=EC=B0=B8=EC=A1=B0=20=EB=A7=81=ED=81=AC=20=EC=A0=91?= =?UTF-8?q?=EA=B7=BC=EC=84=B1=20=ED=96=A5=EC=83=81=20(=EC=83=88=20?= =?UTF-8?q?=ED=83=AD=20=EC=97=B4=EB=A6=BC=20=EA=B2=BD=EA=B3=A0=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ν™”λ©΄ νŒλ…κΈ° μ‚¬μš©μžκ°€ μ™ΈλΆ€ 링크λ₯Ό 클릭할 λ•Œ μƒˆ 탭이 μ—΄λ¦°λ‹€λŠ” 사싀을 인지할 수 μžˆλ„λ‘ `target="_blank"` 속성이 μžˆλŠ” 링크에 `aria-label="... (opens in a new tab)"` 속성을 μΆ”κ°€ν–ˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό 톡해 예기치 μ•Šμ€ μ»¨ν…μŠ€νŠΈ μ „ν™˜μœΌλ‘œ μΈν•œ ν˜Όλž€μ„ λ°©μ§€ν•˜κ³  접근성을 κ°œμ„ ν–ˆμŠ΅λ‹ˆλ‹€. * `scanner/dashboard/index.html` 의 μ°Έμ‘° 링크 λ Œλ”λ§ 둜직 μ—…λ°μ΄νŠΈ * `tests/test_dashboard_core.py` 에 κ΄€λ ¨ μ ‘κ·Όμ„± 검증 ν…ŒμŠ€νŠΈ μΆ”κ°€ --- .jules/palette.md | 3 +++ scanner/dashboard/index.html | 2 +- tests/test_dashboard_core.py | 28 ++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/.jules/palette.md b/.jules/palette.md index ea004e2d..e45d25f1 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -81,3 +81,6 @@ ## 2026-08-12 - Skip to Content Accessibility **Learning:** Screen reader and keyboard-only users experience significant friction when forced to navigate through repetitive header controls on every page load. **Action:** Keep a visible-on-focus skip link as the first interactive element, target a programmatically focusable main container, and give the focused link a high-contrast outline. +## 2024-08-13 - Add Context Switch Warning for External Links +**Learning:** Screen reader users need to be informed when links open in a new tab, as unexpected context switching can be disorienting. Simply adding `target="_blank"` is not enough. +**Action:** Always add visually hidden text or an explicit `aria-label` like `(opens in a new tab)` for links with `target="_blank"`. diff --git a/scanner/dashboard/index.html b/scanner/dashboard/index.html index 132bc31b..8df0a0ee 100644 --- a/scanner/dashboard/index.html +++ b/scanner/dashboard/index.html @@ -281,7 +281,7 @@

Dashboard

function openDetail(f){ lastFocus = document.activeElement; const s = String(f.severity||'INFO').toUpperCase(); - const refs = (f.references||[]).map(r=>`${esc(r)}`).join('
'); + const refs = (f.references||[]).map(r=>`${esc(r)}`).join('
'); const owasp = (f.owasp||[]).join(', '); const cwe = (f.cwe||[]).join(', '); const d = document.getElementById('detail'); diff --git a/tests/test_dashboard_core.py b/tests/test_dashboard_core.py index 75a5809f..4380ce1c 100644 --- a/tests/test_dashboard_core.py +++ b/tests/test_dashboard_core.py @@ -302,3 +302,31 @@ def test_dashboard_search_escape_clears_input(): assert "e.key === 'Escape'" in html assert "query = '';" in html assert "render();" in html + +class _LinkAttributeParser(HTMLParser): + def __init__(self): + super().__init__() + self.links = [] + + def handle_starttag(self, tag, attrs): + if tag == "a": + self.links.append(dict(attrs)) + +def test_dashboard_external_links_warn_screen_readers(): + """External links opening in a new tab should inform screen readers.""" + html_content = dashboard_index_path().read_text(encoding="utf-8") + detail_markup = re.search( + r"const refs = \(f\.references\|\|\[\]\)\.map\(r=>`(.*?)`\)\.join\('
'\);", + html_content, + flags=re.DOTALL, + ) + assert detail_markup is not None + + parser = _LinkAttributeParser() + parser.feed(detail_markup.group(1)) + + assert any( + attributes.get("target") == "_blank" + and attributes.get("aria-label") == "${esc(r)} (opens in a new tab)" + for attributes in parser.links + ) From a4009d06be1197e628327b2948849ab00828d9b2 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 14 Aug 2026 08:08:29 +0000 Subject: [PATCH 03/57] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=81=EC=A0=81=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=8F=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=EC=84=B1=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ’‘ What - `scanner/dashboard/console.html`의 λ²„νŠΌμ— `:hover` 및 `:disabled` μ‹œκ°μ  ν”Όλ“œλ°± μΆ”κ°€ - `.close-btn` μš”μ†Œμ— `title="Close (Esc)"` 툴팁 μΆ”κ°€ - `tr.scan`의 λ‘œλ”© μƒνƒœ(`aria-busy="true"`)에 λŒ€ν•œ μ‹œκ°μ  ν”Όλ“œλ°±(opacity, pointer-events) μΆ”κ°€ - `scanner/dashboard/index.html`의 "Clear filters" λ²„νŠΌ μŠ€νƒ€μΌμ„ CSS 클래슀(`.clear-btn`)둜 λΆ„λ¦¬ν•˜κ³  hover 효과 μΆ”κ°€ 🎯 Why - 슀크린 리더 μ‚¬μš©μžλΏλ§Œ μ•„λ‹ˆλΌ μ‹œκ° μž₯μ• κ°€ μ—†λŠ” 일반 μ‚¬μš©μžλ„ ARIA μƒνƒœ(`aria-busy`)λ‚˜ λΉ„ν™œμ„±ν™”(`disabled`) μƒνƒœλ₯Ό λͺ…ν™•νžˆ 인지할 수 μžˆλ„λ‘ μ‹œκ°μ  ν”Όλ“œλ°±μ„ μ œκ³΅ν•˜μ—¬ μ‚¬μš©μ„±μ„ ν–₯μƒμ‹œν‚€κΈ° μœ„ν•¨μž…λ‹ˆλ‹€. - 인라인 μŠ€νƒ€μΌμ„ 클래슀둜 λΆ„λ¦¬ν•˜μ—¬ μ½”λ“œ μœ μ§€λ³΄μˆ˜μ„±μ„ λ†’μ˜€μŠ΅λ‹ˆλ‹€. πŸ“Έ Before/After (λΉ„μ£Όμ–Ό λ³€κ²½ μ‚¬ν•­μœΌλ‘œ, ν…ŒμŠ€νŠΈ 슀크립트λ₯Ό 톡해 검증 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.) β™Ώ Accessibility - ARIA μƒνƒœ(`aria-busy="true"`)와 μ‹œκ°μ  UI κ°„μ˜ 동기화(parity) 달성 - λ‹«κΈ° λ²„νŠΌμ— 마우슀 ν˜Έλ²„ μ‹œ 툴팁(`title`)을 μ œκ³΅ν•˜μ—¬ λͺ…ν™•ν•œ μ»¨ν…μŠ€νŠΈ 전달 From 570d0736a2c23fbe1ea9b210066c2c0af79c4c62 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 14 Aug 2026 08:55:26 +0000 Subject: [PATCH 04/57] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=81=EC=A0=81=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=8F=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=EC=84=B1=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ’‘ What - `scanner/dashboard/console.html`의 λ²„νŠΌμ— `:hover` 및 `:disabled` μ‹œκ°μ  ν”Όλ“œλ°± μΆ”κ°€ - `.close-btn` μš”μ†Œμ— `title="Close (Esc)"` 툴팁 μΆ”κ°€ - `tr.scan`의 λ‘œλ”© μƒνƒœ(`aria-busy="true"`)에 λŒ€ν•œ μ‹œκ°μ  ν”Όλ“œλ°±(opacity, pointer-events) μΆ”κ°€ - `scanner/dashboard/index.html`의 "Clear filters" λ²„νŠΌ μŠ€νƒ€μΌμ„ CSS 클래슀(`.clear-btn`)둜 λΆ„λ¦¬ν•˜κ³  hover 효과 μΆ”κ°€ - Strix CI μŠ€μΊ” μ‹€νŒ¨ μˆ˜μ •: `console.html`의 `data-id` 속성에 `s.id` λ Œλ”λ§ μ‹œ XSS λ°©μ§€λ₯Ό μœ„ν•œ `esc()` 적용 🎯 Why - 슀크린 리더 μ‚¬μš©μžλΏλ§Œ μ•„λ‹ˆλΌ μ‹œκ° μž₯μ• κ°€ μ—†λŠ” 일반 μ‚¬μš©μžλ„ ARIA μƒνƒœ(`aria-busy`)λ‚˜ λΉ„ν™œμ„±ν™”(`disabled`) μƒνƒœλ₯Ό λͺ…ν™•νžˆ 인지할 수 μžˆλ„λ‘ μ‹œκ°μ  ν”Όλ“œλ°±μ„ μ œκ³΅ν•˜μ—¬ μ‚¬μš©μ„±μ„ ν–₯μƒμ‹œν‚€κΈ° μœ„ν•¨μž…λ‹ˆλ‹€. - 인라인 μŠ€νƒ€μΌμ„ 클래슀둜 λΆ„λ¦¬ν•˜μ—¬ μ½”λ“œ μœ μ§€λ³΄μˆ˜μ„±μ„ λ†’μ˜€μŠ΅λ‹ˆλ‹€. - HTML Attribute Injection 취약점을 μ œκ±°ν•˜μ—¬ λ³΄μ•ˆμ„ κ°•ν™”ν–ˆμŠ΅λ‹ˆλ‹€. πŸ“Έ Before/After (λΉ„μ£Όμ–Ό λ³€κ²½ 사항 및 XSS λ°©μ§€ 처리, ν…ŒμŠ€νŠΈ 슀크립트λ₯Ό 톡해 검증 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.) β™Ώ Accessibility - ARIA μƒνƒœ(`aria-busy="true"`)와 μ‹œκ°μ  UI κ°„μ˜ 동기화(parity) 달성 - λ‹«κΈ° λ²„νŠΌμ— 마우슀 ν˜Έλ²„ μ‹œ 툴팁(`title`)을 μ œκ³΅ν•˜μ—¬ λͺ…ν™•ν•œ μ»¨ν…μŠ€νŠΈ 전달 --- .jules/sentinel.md | 5 +++++ scanner/dashboard/console.html | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 1600d432..c2ffd1ed 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -127,3 +127,8 @@ **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. + +## 2026-08-14 - HTML Attribute Injection Prevention +**Vulnerability:** A Strix automated scan identified a Medium-severity XSS vulnerability (Attribute Injection) in `scanner/dashboard/console.html` where an unescaped variable `s.id` was directly interpolated into a `data-id` HTML attribute within a template literal (`data-id="${s.id}"`). This allowed an attacker to break out of the attribute quotes and inject malicious event handlers (e.g., `onmouseover`). +**Learning:** Even custom `data-*` attributes meant for internal client-side logic require strict HTML escaping before interpolation, as breaking out of the attribute boundary enables full cross-site scripting. +**Prevention:** Always wrap all interpolated variables inside HTML template literals with an HTML escaping function (e.g., `esc()`), regardless of whether the attribute expects executable code or just a simple string identifier. diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html index 3c5590e3..9103751d 100644 --- a/scanner/dashboard/console.html +++ b/scanner/dashboard/console.html @@ -134,7 +134,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 8964b5c352835399da707cd5ace688897808e283 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 14 Aug 2026 13:07:45 +0000 Subject: [PATCH 05/57] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=81=EC=A0=81=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EB=B0=8F=20?= =?UTF-8?q?=EC=A0=91=EA=B7=BC=EC=84=B1=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ’‘ What - `scanner/dashboard/console.html`의 λ²„νŠΌμ— `:hover` 및 `:disabled` μ‹œκ°μ  ν”Όλ“œλ°± μΆ”κ°€ - `.close-btn` μš”μ†Œμ— `title="Close (Esc)"` 툴팁 μΆ”κ°€ - `tr.scan`의 λ‘œλ”© μƒνƒœ(`aria-busy="true"`)에 λŒ€ν•œ μ‹œκ°μ  ν”Όλ“œλ°±(opacity, pointer-events) μΆ”κ°€ - `scanner/dashboard/index.html`의 "Clear filters" λ²„νŠΌ μŠ€νƒ€μΌμ„ CSS 클래슀(`.clear-btn`)둜 λΆ„λ¦¬ν•˜κ³  hover 효과 μΆ”κ°€ - Strix CI μŠ€μΊ” μ‹€νŒ¨ μˆ˜μ •: `console.html`의 `data-id` 속성에 `s.id` λ Œλ”λ§ μ‹œ XSS λ°©μ§€λ₯Ό μœ„ν•œ `esc()` 적용 🎯 Why - 슀크린 리더 μ‚¬μš©μžλΏλ§Œ μ•„λ‹ˆλΌ μ‹œκ° μž₯μ• κ°€ μ—†λŠ” 일반 μ‚¬μš©μžλ„ ARIA μƒνƒœ(`aria-busy`)λ‚˜ λΉ„ν™œμ„±ν™”(`disabled`) μƒνƒœλ₯Ό λͺ…ν™•νžˆ 인지할 수 μžˆλ„λ‘ μ‹œκ°μ  ν”Όλ“œλ°±μ„ μ œκ³΅ν•˜μ—¬ μ‚¬μš©μ„±μ„ ν–₯μƒμ‹œν‚€κΈ° μœ„ν•¨μž…λ‹ˆλ‹€. - 인라인 μŠ€νƒ€μΌμ„ 클래슀둜 λΆ„λ¦¬ν•˜μ—¬ μ½”λ“œ μœ μ§€λ³΄μˆ˜μ„±μ„ λ†’μ˜€μŠ΅λ‹ˆλ‹€. - HTML Attribute Injection 취약점을 μ œκ±°ν•˜μ—¬ λ³΄μ•ˆμ„ κ°•ν™”ν–ˆμŠ΅λ‹ˆλ‹€. πŸ“Έ Before/After (λΉ„μ£Όμ–Ό λ³€κ²½ 사항 및 XSS λ°©μ§€ 처리, ν…ŒμŠ€νŠΈ 슀크립트λ₯Ό 톡해 검증 μ™„λ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.) β™Ώ Accessibility - ARIA μƒνƒœ(`aria-busy="true"`)와 μ‹œκ°μ  UI κ°„μ˜ 동기화(parity) 달성 - λ‹«κΈ° λ²„νŠΌμ— 마우슀 ν˜Έλ²„ μ‹œ 툴팁(`title`)을 μ œκ³΅ν•˜μ—¬ λͺ…ν™•ν•œ μ»¨ν…μŠ€νŠΈ 전달 From 03dfb8f8fc51e2022ebf2678efd619bc076ce8cd Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Fri, 14 Aug 2026 14:41:29 +0000 Subject: [PATCH 06/57] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20=EC=BD=98?= =?UTF-8?q?=EC=86=94=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20disabled=20?= =?UTF-8?q?=EB=B0=8F=20loading=20=EC=83=81=ED=83=9C=20=EC=8B=9C=EA=B0=81?= =?UTF-8?q?=EC=A0=81=20=ED=94=BC=EB=93=9C=EB=B0=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #connect λ²„νŠΌκ³Ό tr.scan μš”μ†Œμ˜ :disabled 및 [aria-busy="true"] μƒνƒœμ— λŒ€ν•œ CSS μŠ€νƒ€μΌ μΆ”κ°€ (투λͺ…도 κ°μ†Œ, μ»€μ„œ λ³€κ²½) - #key μž…λ ₯값이 없을 λ•Œ #connect λ²„νŠΌμ„ λΉ„ν™œμ„±ν™”ν•˜λ„λ‘ 이벀트 λ¦¬μŠ€λ„ˆ μΆ”κ°€ 및 λ‘œλ”© μ’…λ£Œ ν›„ λΉ„ν™œμ„±ν™” μƒνƒœ 동기화 처리 --- scanner/dashboard/console.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html index 7ec262af..14fabf39 100644 --- a/scanner/dashboard/console.html +++ b/scanner/dashboard/console.html @@ -37,6 +37,8 @@ tr.scan{cursor:pointer} tr.scan:hover{background:var(--bg)} input:focus-visible, button:focus-visible, tr.scan:focus-visible, .bar:focus-visible, #detail:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; } + button:disabled, button[aria-busy="true"] { opacity: 0.6; cursor: not-allowed; } + tr.scan[aria-busy="true"] { opacity: 0.7; cursor: wait; pointer-events: none; } .close-btn{float:right;border:0;background:transparent;font-size:16px;cursor:pointer;color:var(--muted);padding:0 4px;margin-top:-2px} .close-btn:hover{color:var(--text)} .pill{display:inline-block;padding:1px 8px;border-radius:999px;font-size:11px;font-weight:700;color:#fff} @@ -177,10 +179,11 @@

AppGuardrail Console

} } } -$("#connect").onclick=async()=>{KEY=$("#key").value.trim();if(!KEY)return;sessionStorage.setItem("ag_key",KEY);$("#key").value="";$("#connect").disabled=true;$("#connect").setAttribute("aria-busy","true");$("#connect").textContent="Connecting...";await load();$("#connect").disabled=false;$("#connect").removeAttribute("aria-busy");$("#connect").textContent="Connect";}; +$("#connect").onclick=async()=>{KEY=$("#key").value.trim();if(!KEY)return;sessionStorage.setItem("ag_key",KEY);$("#key").value="";$("#connect").disabled=true;$("#connect").setAttribute("aria-busy","true");$("#connect").textContent="Connecting...";await load();$("#connect").disabled=!$("#key").value.trim();$("#connect").removeAttribute("aria-busy");$("#connect").textContent="Connect";}; +$("#key").addEventListener("input",e=>{$("#connect").disabled=!e.target.value.trim();}); $("#key").addEventListener("keydown",e=>{if(e.key==="Enter")$("#connect").click();}); $("#logout").onclick=()=>{sessionStorage.removeItem("ag_key");location.reload();}; -if(KEY)load(); +if(KEY)load();else $("#connect").disabled=true; From 9a28a7d51c33fabb8f82edb1b16891950908d155 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 00:20:13 +0900 Subject: [PATCH 07/57] test(console): pin ephemeral single-flight connection contract --- tests/test_console_detail_loading_contract.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/test_console_detail_loading_contract.py b/tests/test_console_detail_loading_contract.py index 38eebed0..63b22800 100644 --- a/tests/test_console_detail_loading_contract.py +++ b/tests/test_console_detail_loading_contract.py @@ -37,3 +37,31 @@ def test_console_detail_scrolling_respects_reduced_motion(): assert "element.scrollIntoView();" in html assert 'element.scrollIntoView({behavior:"smooth"});' in html assert html.count("scrollDetailIntoView(d);") == 2 + + +def test_console_keeps_org_api_key_ephemeral_and_requests_viewer_scope(): + """The read-only console must not persist an owner-capable bearer key.""" + html = _console_html() + + assert "sessionStorage" not in html + assert 'placeholder="Viewer API key (agk_…)"' in html + assert "viewer-scoped" in html + assert 'let KEY="";' in html + assert '$("#logout").onclick=()=>{KEY="";location.reload();};' in html + + +def test_console_connection_state_is_exception_safe_and_single_flight(): + """Connection cleanup and input edits must not permit overlapping loads.""" + html = _console_html() + + assert "let connecting=false;" in html + assert "function syncConnectState(){" in html + assert 'button.disabled=connecting||!$("#key").value.trim();' in html + assert 'button.setAttribute("aria-busy","true");' in html + assert 'button.removeAttribute("aria-busy");' in html + assert "async function connect(){" in html + assert "if(connecting)return;" in html + assert "try{await load();}finally{" in html + assert '$("#key").addEventListener("input",syncConnectState);' in html + assert "if(KEY)load()" not in html + assert "syncConnectState();" in html From d8bd514665ad55922fa380ca79a0b840a6bbba11 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 00:22:06 +0900 Subject: [PATCH 08/57] fix(console): keep viewer key ephemeral and synchronize connect state --- scanner/dashboard/console.html | 52 +++++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html index 14fabf39..c15c6f1a 100644 --- a/scanner/dashboard/console.html +++ b/scanner/dashboard/console.html @@ -6,8 +6,9 @@ AppGuardrail Console