全部线索{total}等待人工判断
严重{counts['critical']}优先复核
高危{counts['high']}需要关注
中危{counts['medium']}结合上下文
基线新增{len(baseline.get('new', []))}本轮变化
已修复{len(baseline.get('fixed', []))}相对基线
@@ -235,13 +238,17 @@ def _render_html(payload: dict[str, Any]) -> str:
function matchesQuery(c,q){{if(!q)return true;return q.split(/\\s+/).every(token=>{{const parts=token.split(':',2);if(parts.length<2)return c.dataset.search.includes(token);const [key,value]=parts;if(key==='cwe')return c.dataset.cwe.includes(value);if(key==='path')return c.dataset.path.includes(value);if(key==='scanner')return c.dataset.scanner.includes(value);if(key==='priority')return Number(c.dataset.priority)>=Number(value||0);return c.dataset.search.includes(token)}})}}
function groupBy(){{const mode=document.querySelector('#view-mode').value,list=document.querySelector('#finding-list');cards.forEach(c=>c.style.display='');if(mode==='list'){{const sorted=cards.slice().sort((a,b)=>Number(b.dataset.priority)-Number(a.dataset.priority)||a.dataset.path.localeCompare(b.dataset.path));list.innerHTML='';sorted.forEach(c=>list.appendChild(c));return}}const groups={{}},order={{'critical':0,'high':1,'medium':2,'low':3,'info':4}};cards.forEach(c=>{{if(c.classList.contains('hidden'))return;let key;if(mode==='by-severity')key=c.dataset.severity;else if(mode==='by-rule')key=c.dataset.cwe;else if(mode==='by-file'){{const p=c.dataset.path,i=p.lastIndexOf('/');key=i<0?p:p.substring(i+1)}}else if(mode==='by-path'){{const p=c.dataset.path,i=p.indexOf('/');key=i<0?'(root)':p.substring(0,i)}}else if(mode==='by-scanner'){{key=c.dataset.scanner.split(' + ')[0]||'unknown'}};(groups[key]=groups[key]||[]).push(c)}});list.innerHTML='';const keys=Object.keys(groups);if(mode==='by-severity')keys.sort((a,b)=>(order[a]??9)-(order[b]??9));else keys.sort();keys.forEach(key=>{{const h=document.createElement('div'),label=document.createElement('span'),meta=document.createElement('span'),button=document.createElement('button');h.className='group-header';label.textContent=key;meta.className='group-meta';meta.textContent=groups[key].length+' 条';button.className='group-btn';button.textContent='折叠';button.onclick=()=>{{const collapsed=h.dataset.collapsed==='true';h.dataset.collapsed=String(!collapsed);groups[key].forEach(c=>c.style.display=collapsed?'':'none');button.textContent=collapsed?'折叠':'展开'}};h.append(label,meta,button);list.appendChild(h);groups[key].forEach(c=>list.appendChild(c))}})}}
function applyFilters(){{const q=document.querySelector('#search').value.trim().toLowerCase(),sev=document.querySelector('#severity').value,status=document.querySelector('#review-filter').value;let visible=0;cards.forEach(c=>{{const verdict=state[c.dataset.fp]?.verdict||'unreviewed',show=matchesQuery(c,q)&&(sev==='all'||c.dataset.severity===sev)&&(status==='all'||verdict===status);c.classList.toggle('hidden',!show);if(show)visible++}});document.querySelector('#visible-count').textContent=visible;groupBy()}}
-cards.forEach(card=>{{const fp=card.dataset.fp;state[fp]=state[fp]||{{}};state[fp].answers=state[fp].answers||{{}};const apply=()=>{{const verdict=state[fp].verdict||'unreviewed';card.dataset.status=verdict;card.querySelectorAll('[data-verdict]').forEach(b=>b.classList.toggle('active',verdict===b.dataset.verdict));card.querySelectorAll('[data-question]').forEach(b=>b.classList.toggle('active',state[fp].answers[b.dataset.question]===b.dataset.answer));card.querySelector('textarea').value=state[fp].notes||'';card.querySelector('[data-confidence]').value=state[fp].confidence||50;card.querySelector('[data-confidence-value]').textContent=(state[fp].confidence||50)+'%';updateProgress();applyFilters()}};apply();card.querySelectorAll('[data-question]').forEach(b=>b.onclick=()=>{{state[fp].answers[b.dataset.question]=b.dataset.answer;state[fp].updatedAt=new Date().toISOString();save();apply()}});card.querySelectorAll('[data-verdict]').forEach(b=>b.onclick=()=>{{state[fp].verdict=b.dataset.verdict;state[fp].updatedAt=new Date().toISOString();save();apply()}});card.querySelector('textarea').oninput=e=>{{state[fp].notes=e.target.value;save()}};card.querySelector('[data-confidence]').oninput=e=>{{state[fp].confidence=e.target.value;state[fp].updatedAt=new Date().toISOString();save();apply()}}}});
+cards.forEach(card=>{{const fp=card.dataset.fp;state[fp]=state[fp]||{{}};state[fp].answers=state[fp].answers||{{}};const apply=()=>{{const verdict=state[fp].verdict||'unreviewed',labels={{unreviewed:'尚未复核',confirmed:'已确认漏洞','false-positive':'已标记误报',uncertain:'仍不确定'}};card.dataset.status=verdict;card.querySelectorAll('[data-verdict]').forEach(b=>b.classList.toggle('active',verdict===b.dataset.verdict));card.querySelectorAll('[data-question]').forEach(b=>b.classList.toggle('active',state[fp].answers[b.dataset.question]===b.dataset.answer));card.querySelector('textarea').value=state[fp].notes||'';card.querySelector('[data-confidence]').value=state[fp].confidence||50;card.querySelector('[data-confidence-value]').textContent=(state[fp].confidence||50)+'%';const badge=card.querySelector('[data-review-state]');if(badge)badge.textContent=labels[verdict]||verdict;updateProgress();applyFilters()}};apply();card.querySelectorAll('[data-question]').forEach(b=>b.onclick=()=>{{state[fp].answers[b.dataset.question]=b.dataset.answer;state[fp].updatedAt=new Date().toISOString();save();apply()}});card.querySelectorAll('[data-verdict]').forEach(b=>b.onclick=()=>{{state[fp].verdict=b.dataset.verdict;state[fp].updatedAt=new Date().toISOString();save();apply()}});card.querySelector('textarea').oninput=e=>{{state[fp].notes=e.target.value;save()}};card.querySelector('[data-confidence]').oninput=e=>{{state[fp].confidence=e.target.value;state[fp].updatedAt=new Date().toISOString();save();apply()}}}});
['search','severity','review-filter','view-mode'].forEach(id=>document.querySelector('#'+id).addEventListener('input',applyFilters));
+const authzSearch=document.querySelector('#authz-search'),authzFilter=document.querySelector('#authz-filter'),authzRows=[...document.querySelectorAll('[data-authz-status]')];
+function applyAuthzFilters(){{if(!authzSearch||!authzFilter)return;const q=authzSearch.value.trim().toLowerCase(),filter=authzFilter.value;let visible=0;authzRows.forEach(row=>{{const statusMatch=filter==='all'||row.dataset.authzStatus===filter||(filter==='anonymous'&&row.dataset.authzRequirement==='anonymous'),show=statusMatch&&(!q||row.dataset.authzSearch.includes(q));row.hidden=!show;if(show)visible++}});const count=document.querySelector('#authz-visible');if(count)count.textContent=visible}}
+if(authzSearch&&authzFilter){{authzSearch.addEventListener('input',applyAuthzFilters);authzFilter.addEventListener('input',applyAuthzFilters)}}
document.querySelector('#export').onclick=()=>{{const blob=new Blob([JSON.stringify({{schema_version:'1.2',project:{project_json},exported_at:new Date().toISOString(),reviews:state}},null,2)],{{type:'application/json'}}),a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='review.json';a.click();URL.revokeObjectURL(a.href)}};
document.querySelector('#clear').onclick=()=>{{if(confirm('确定清空当前项目保存在浏览器中的复核记录?')){{localStorage.removeItem(storageKey);location.reload()}}}};
const themeKey='java-audit-theme',root=document.documentElement;root.dataset.theme=localStorage.getItem(themeKey)||'light';
-const toggleTheme=()=>{{root.dataset.theme=root.dataset.theme==='dark'?'light':'dark';localStorage.setItem(themeKey,root.dataset.theme)}};
-document.querySelector('#theme').onclick=toggleTheme;const tm=document.querySelector('#theme-m');if(tm)tm.onclick=toggleTheme;
+const syncThemeUi=()=>{{const dark=root.dataset.theme==='dark';document.querySelectorAll('[data-theme-label]').forEach(el=>el.textContent=dark?'浅色模式':'深色模式');document.querySelectorAll('.theme-icon').forEach(el=>el.textContent=dark?'☀':'☾');const mobile=document.querySelector('#theme-m');if(mobile)mobile.textContent=dark?'☀':'◐'}};
+const toggleTheme=()=>{{root.dataset.theme=root.dataset.theme==='dark'?'light':'dark';localStorage.setItem(themeKey,root.dataset.theme);syncThemeUi()}};
+document.querySelector('#theme').onclick=toggleTheme;const tm=document.querySelector('#theme-m');if(tm)tm.onclick=toggleTheme;syncThemeUi();
const navLinks=[...document.querySelectorAll('.side-link')];const spy=new IntersectionObserver(entries=>entries.forEach(e=>{{if(e.isIntersecting)navLinks.forEach(l=>l.classList.toggle('active',l.getAttribute('href')==='#'+e.target.id))}}),{{rootMargin:'-35% 0px -58% 0px'}});{spy_ids}.forEach(id=>{{const el=document.getElementById(id);if(el)spy.observe(el)}});
document.querySelectorAll('[data-copy]').forEach(b=>b.onclick=async()=>{{await navigator.clipboard.writeText(b.dataset.copy);const old=b.textContent;b.textContent='已复制';setTimeout(()=>b.textContent=old,900)}});
document.addEventListener('keydown',e=>{{if(['INPUT','TEXTAREA','SELECT'].includes(document.activeElement.tagName))return;if(!['j','k'].includes(e.key))return;const visible=cards.filter(c=>!c.classList.contains('hidden')),current=visible.findIndex(c=>c===document.activeElement||c.contains(document.activeElement)),next=e.key==='j'?Math.min(visible.length-1,current+1):Math.max(0,current<0?0:current-1);visible[next]?.focus();visible[next]?.scrollIntoView({{behavior:'smooth',block:'center'}})}});
@@ -509,22 +516,33 @@ def _authz_section(payload: dict[str, Any]) -> str:
if len(ops) > 4:
ops_text += f" +{len(ops) - 4}"
expression = str(endpoint.get("expression") or "")
- risk_mark = "" if requirement in {"role", "permission", "ownership"} else ""
+ search_text = " ".join([
+ str(endpoint.get("route", "")), str(endpoint.get("handler", "")),
+ str(endpoint.get("path", "")), str(endpoint.get("http_method", "")),
+ role_text, ops_text, requirement_labels.get(requirement, requirement),
+ status_labels.get(status, status),
+ ]).lower()
+ route = html.escape(str(endpoint.get("route", "")) or "/")
+ handler = html.escape(str(endpoint.get("handler", "")))
+ source_path = html.escape(str(endpoint.get("path", "")))
+ method = html.escape(str(endpoint.get("http_method", "")))
+ requirement_label = html.escape(requirement_labels.get(requirement, requirement))
+ status_label = html.escape(status_labels.get(status, status))
rows.append(
- f'
'
- f'{html.escape(str(endpoint.get("route", "")) or "/")}'
- f'{html.escape(str(endpoint.get("handler", "")))} · {html.escape(str(endpoint.get("path", "")))}:{endpoint.get("line", 0)} | '
- f'{html.escape(str(endpoint.get("http_method", "")))} | '
- f'{html.escape(requirement_labels.get(requirement, requirement))}{risk_mark} | '
- f'{html.escape(role_text)}'
- + (f' {html.escape(expression[:80])} ' if expression else "")
+ f' |
'
+ f'{route}'
+ f'{handler} · {source_path}:{endpoint.get("line", 0)} | '
+ f'{method} | '
+ f'{requirement_label} | '
+ f'{html.escape(role_text)}'
+ + (f'{html.escape(expression[:120])}' if expression else "")
+ ' | '
- f'{html.escape(ops_text)} | '
- f'{html.escape(status_labels.get(status, status))} | '
+ f'{html.escape(ops_text)} | '
+ f'{status_label} | '
f'
'
)
chips = "".join(
- f'
{label} {coverage.get(key, 0)}'
+ f'
{label} {coverage.get(key, 0)}'
for label, key in (("端点", "endpoints"), ("权限已证明", "proven"), ("权限缺失", "missing"), ("匿名可访问", "anonymous"), ("配置规则", "rules"))
)
high_risk = int(coverage.get("unprotected_high_risk", 0) or 0)
@@ -537,17 +555,15 @@ def _authz_section(payload: dict[str, Any]) -> str:
return (
'
端点权限矩阵
'
'
证据来源:方法级注解 → 安全配置匹配 → Shiro 过滤链;没有匹配到的端点标记为权限缺失,需人工确认
'
- f'
{chips}
'
- ''
- '
'
- ''
- '| 接口 | '
- '方法 | '
- '身份要求 | '
- '角色 / 权限 | '
- '危险操作 | '
- '证据状态 | '
- '
' + "".join(rows) + "
" + warning + ""
+ f'
{chips}
'
+ '