Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .jules/palette.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,3 @@
## 2026-08-04 - Native File Input Iteration Friction
**Learning:** Browsers suppress `change` events when a file input still holds the same selected path. Clearing the value in an inline `onclick` handler fixes repetition but also discards the previous selection when the user cancels the picker and couples behavior to markup.
**Action:** Capture the selected `File` in the input's `change` listener, clear the input value immediately afterward, and then process the captured object. Use native buttons with explicit event listeners to proxy the picker from an empty-state CTA, preserving keyboard access and CSP-compatible separation of markup and behavior.

## 2026-08-05 - External Link Accessibility
**Learning:** External links (`target="_blank"`) that open new tabs are disorienting to screen reader users without explicit textual and visual cues.
**Action:** For external links, always append a visual indicator (`β†—`) and an `aria-label="... (opens in a new tab)"` to inform users of the behavior before activation.
9 changes: 6 additions & 3 deletions scanner/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,11 @@
const ctx = String(f.context||'app-code');
return BLOCKING_SEV.has(sev) && !NON_BLOCKING.has(ctx);
}
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));}
function esc(s){return String(s==null?'':s).replace(/[&<>"'`]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;','`':'&#96;'}[c]));}
function safeUrl(u){
if (typeof u === 'string' && u.startsWith('//')) {
return '#';
}
Comment on lines 118 to +121

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”’ Security & Privacy | 🟑 Minor | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- safeUrl and nearby code ---'
sed -n '90,280p' scanner/dashboard/index.html
printf '%s\n' '--- references to safeUrl and href construction ---'
rg -n -C 4 'safeUrl|href=|reference|references|window\.open|location' scanner/dashboard/index.html

Repository: ContextualWisdomLab/appguardrail

Length of output: 11027


🏁 Script executed:

#!/bin/bash
set -eu
sed -n '90,280p' scanner/dashboard/index.html
rg -n -C 4 'safeUrl|href=|reference|references|window\.open|location' scanner/dashboard/index.html

Repository: ContextualWisdomLab/appguardrail

Length of output: 10943


🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- findings input and ALL assignment ---'
rg -n -C 6 'FileReader|JSON\.parse|fetch\(|ALL\s*=|addEventListener|drop|change' scanner/dashboard/index.html
printf '%s\n' '--- URL normalization probe ---'
node - <<'JS'
const base = 'https://dashboard.example.test/dashboard/';
const inputs = [
  '//evil.example/',
  ' //evil.example/',
  '\t//evil.example/',
  '\n//evil.example/',
  '\\\\evil.example\\path',
  '\\evil.example\\path',
  'https:\\\\evil.example\\path',
  ' https://evil.example/',
  'javascript:alert(1)',
  '/local/path',
];
for (const input of inputs) {
  let parsed;
  try { parsed = new URL(input, base).href; } catch (e) { parsed = `THROWS:${e.name}`; }
  console.log(JSON.stringify({input, parsed}));
}
JS

Repository: ContextualWisdomLab/appguardrail

Length of output: 8936


Open Redirect (CWE-601): URL Redirection to Untrusted Site ('Open Redirect')

Reachability: External Β· Exploitability: Moderate

URL을 μ •κ·œν™”ν•œ κ°’μœΌλ‘œ κ²€μ¦ν•˜κ³  λ°˜ν™˜ν•˜μ„Έμš”.

safeUrl은 new URL()둜 μ™ΈλΆ€ URL을 μŠΉμΈν•œ λ’€ 원본 λ¬Έμžμ—΄μ„ href에 λ°˜ν™˜ν•©λ‹ˆλ‹€. λ”°λΌμ„œ μ„ ν–‰ κ³΅λ°±Β·μ œμ–΄λ¬ΈμžΒ·μ—­μŠ¬λž˜μ‹œκ°€ ν¬ν•¨λœ μž…λ ₯이 μ™ΈλΆ€ origin 링크둜 해석될 수 μžˆμŠ΅λ‹ˆλ‹€. μ •κ·œν™” ν›„ http:와 https: ν—ˆμš© λͺ©λ‘μ„ μ μš©ν•˜κ³  λ³€ν˜• μž…λ ₯ ν…ŒμŠ€νŠΈλ₯Ό μΆ”κ°€ν•˜μ„Έμš”.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scanner/dashboard/index.html` around lines 118 - 121, Update safeUrl to
normalize the input with new URL() before validating or returning it, including
handling leading whitespace, control characters, and backslashes. Apply the
external URL allowlist only to normalized http: and https: protocols, and return
the normalized href rather than the original string. Add tests covering these
transformed-input cases.

Source: Coding guidelines

try {
const parsed = new URL(u, window.location.href);
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') return u;
Expand Down Expand Up @@ -248,7 +251,7 @@ <h1>Dashboard</h1>
function openDetail(f){
lastFocus = document.activeElement;
const s = String(f.severity||'INFO').toUpperCase();
const refs = (f.references||[]).map(r=>`<a href="${esc(safeUrl(r))}" target="_blank" rel="noopener" aria-label="${esc(r)} (opens in a new tab)">${esc(r)} β†—</a>`).join('<br>');
const refs = (f.references||[]).map(r=>`<a href="${esc(safeUrl(r))}" target="_blank" rel="noopener">${esc(r)}</a>`).join('<br>');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟑 Minor | ⚑ Quick win

μƒˆ νƒ­ μ—΄λ¦Ό μ•ˆλ‚΄λ₯Ό μœ μ§€ν•˜μ„Έμš”.

target="_blank"λŠ” 남아 μžˆμ§€λ§Œ, μƒˆ νƒ­ 열림을 μ„€λͺ…ν•˜λ˜ aria-labelκ³Ό β†— ν‘œμ‹œκ°€ μ œκ±°λ˜μ—ˆμŠ΅λ‹ˆλ‹€. 슀크린 리더 μ‚¬μš©μžμ™€ μ‹œκ° μ‚¬μš©μžκ°€ 링크 λ™μž‘μ„ μ˜ˆμΈ‘ν•˜κΈ° μ–΄λ ΅μŠ΅λ‹ˆλ‹€. μƒˆ νƒ­ μ•ˆλ‚΄λ₯Ό aria-label λ˜λŠ” λ™μΌν•œ μ‹œκ°μ  ν…μŠ€νŠΈλ‘œ λ³΅μ›ν•˜μ„Έμš”.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scanner/dashboard/index.html` at line 254, Update the reference-link mapping
in the refs expression to restore an accessible new-tab indication alongside
target="_blank": add an aria-label or equivalent visible β†— marker while
preserving the existing escaped URL, link text, and security attributes.

const owasp = (f.owasp||[]).join(', ');
const cwe = (f.cwe||[]).join(', ');
const d = document.getElementById('detail');
Expand All @@ -257,7 +260,7 @@ <h1>Dashboard</h1>
<div class="dlg-head">
<span class="chip" style="background:${SEV[s]?SEV[s].color:'var(--info)'}">${esc(s)}</span>
<span class="t" id="dlg-title">${esc(f.rule_id)}</span>
<button aria-label="Close" onclick="document.getElementById('detail').close()">βœ•</button>
<button aria-label="Close" title="Close (Esc)" onclick="document.getElementById('detail').close()">βœ•</button>
</div>
<div class="dlg-body">
<p class="file">${esc(f.file)}:${esc(f.line)} Β· ${esc(f.category)} Β· context: ${esc(f.context||'app-code')}${isDeployBlocking(f)?' Β· <span class="tag block">deploy-blocking</span>':''}</p>
Expand Down
37 changes: 37 additions & 0 deletions tests/test_dashboard_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

import json
import json as _json
import re
import threading
import urllib.error
import urllib.request
from contextlib import closing
from html.parser import HTMLParser

import pytest

Expand All @@ -14,6 +16,20 @@
make_dashboard_server, render_tokens_css)


class _ButtonAttributeParser(HTMLParser):
"""Collect attributes from every dashboard button element."""

def __init__(self):
"""Initialize an empty button-attribute collection."""
super().__init__()
self.buttons = []

def handle_starttag(self, tag, attrs):
"""Record one button's attributes while ignoring other elements."""
if tag == "button":
self.buttons.append(dict(attrs))


def _serve(server):
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
Expand Down Expand Up @@ -223,6 +239,7 @@ def test_server_404s_missing_findings(tmp_path):
server.shutdown()
server.server_close()


def test_dashboard_empty_state_clear_filters():
"""Empty state CTA must expose Clear filters control that resets state."""
html = dashboard_index_path().read_text(encoding="utf-8")
Expand All @@ -231,3 +248,23 @@ def test_dashboard_empty_state_clear_filters():
assert "aria-label=\"Clear filters\"" in html
assert "onclick=\"query=''; filterSev=''; render(); document.getElementById('q')?.focus();\"" in html
assert "Clear filters</button>" in html


def test_dashboard_dialog_close_button_has_tooltip():
"""The dynamically rendered close button exposes its label and Esc tooltip."""
html = dashboard_index_path().read_text(encoding="utf-8")
detail_markup = re.search(
r"d\.innerHTML\s*=\s*`(?P<markup>.*?)`;",
html,
flags=re.DOTALL,
)
assert detail_markup is not None

parser = _ButtonAttributeParser()
parser.feed(detail_markup.group("markup"))

assert any(
attributes.get("title") == "Close (Esc)"
and attributes.get("aria-label") == "Close"
for attributes in parser.buttons
)
Comment on lines +258 to +262

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟑 Minor | ⚑ Quick win

검사 λŒ€μƒμ„ 상세 λ‹€μ΄μ–Όλ‘œκ·Έ λ‹«κΈ° λ²„νŠΌμœΌλ‘œ μ œν•œν•˜μ„Έμš”.

any(...)λŠ” parser.buttons의 λͺ¨λ“  λ²„νŠΌμ„ κ²€μ‚¬ν•©λ‹ˆλ‹€. λ‹€λ₯Έ λ²„νŠΌμ΄ 두 속성을 κ°€μ§€λ©΄ 상세 λ‹«κΈ° λ²„νŠΌμ΄ 없어도 ν…ŒμŠ€νŠΈκ°€ ν†΅κ³Όν•©λ‹ˆλ‹€. μ‹€μ œ λ‹«κΈ° λ²„νŠΌμ˜ 고유 id, class, λ˜λŠ” data-* μ‹λ³„μžλ₯Ό ν•¨κ»˜ ν™•μΈν•˜μ„Έμš”.

🧰 Tools
πŸͺ› GitHub Actions: Tests / 0_Unit tests (Python 3.11).txt

[error] 258-264: pytest test_dashboard_dialog_close_button_has_tooltip failed: the dashboard dialog close button does not have title="Close (Esc)" and aria-label="Close" attributes. Command 'python -m pytest -q' failed with exit code 1.

πŸͺ› GitHub Actions: Tests / 1_Unit tests (Python 3.13).txt

[error] 258-264: pytest test_dashboard_dialog_close_button_has_tooltip failed: the dashboard dialog close button does not expose title="Close (Esc)" and aria-label="Close". Command 'python -m pytest -q' failed with exit code 1.

πŸͺ› GitHub Actions: Tests / Unit tests (Python 3.11)

[error] 258-264: pytest test_dashboard_dialog_close_button_has_tooltip failed: the dashboard dialog close button does not have title="Close (Esc)" and aria-label="Close". Command 'python -m pytest -q' exited with code 1.

πŸͺ› GitHub Actions: Tests / Unit tests (Python 3.13)

[error] 258-264: pytest test_dashboard_dialog_close_button_has_tooltip failed: the dashboard dialog close button does not have title="Close (Esc)" and aria-label="Close". Command 'python -m pytest -q' failed with exit code 1.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_dashboard_core.py` around lines 258 - 262, Update the button
assertion in the dashboard test to identify the detail-dialog close button using
its unique id, class, or data-* attribute in addition to the existing title and
aria-label checks. Keep the assertion scoped to the matching element rather than
allowing any button in parser.buttons to satisfy it.

Loading