+12
-9
@@ -216,12 +216,13 @@ async function renderSecurity(){
|
||||
try{
|
||||
const d=await api('/api/security/status'+qnode()),c=d.capabilities||{},os=d.os||{},managed=d.managed||{};
|
||||
window.__securityCaps=c;
|
||||
window.__firewallBackend=d.firewall_backend||{};
|
||||
const capClass=!c.enabled?'securityOff':c.allow_changes&&c.executor_available?'securityManage':'securityAudit';
|
||||
$('#content').innerHTML=`${pageHead('Host Security',`Linux host hardening, configuration and maintenance · ${nodeName()}`,'<button class="btn" id="securityRefresh">↻ Security audit</button>')}
|
||||
<div class="securityHero ${capClass}"><div><small>HOST SECURITY POSTURE</small><div class="securityScore">${Number(d.score||0)}<span>/100</span></div><p>${esc(os.pretty_name||os.name||'Host OS unknown')} · ${esc(d.package_manager||'package manager unknown')} · ${esc(d.init_system||'init unknown')}</p></div><div class="securityCaps">${securityCapabilityPills(c)}</div></div>
|
||||
${c.reason?`<div class="notice ${c.enabled?'':'warnNotice'}"><b>Capability:</b> ${esc(c.reason)}</div>`:''}
|
||||
<div class="securityGrid">
|
||||
${securityComponentCard('firewall','Firewall · nftables','Isolated host INPUT policy. Docker NAT/FORWARD chains remain untouched.',d.firewall,managed.firewall,d.conflicts)}
|
||||
${securityComponentCard('firewall',`Firewall · ${esc((d.firewall_backend||{}).selected||'auto')}`,'Auto-detected native firewall management through UFW, firewalld or isolated nftables.',d.firewall,managed.firewall,d.conflicts)}
|
||||
${securityComponentCard('fail2ban','Fail2Ban','Rate-limit and ban repeated authentication failures using managed jail.d overrides.',d.fail2ban,managed.fail2ban)}
|
||||
${securityComponentCard('auditd','Linux Audit · auditd','Track changes to identity, SSH, sudo, Docker and selected host paths.',d.auditd,managed.auditd)}
|
||||
</div>
|
||||
@@ -236,17 +237,19 @@ async function renderSecurity(){
|
||||
}catch(e){$('#content').innerHTML=`${pageHead('Host Security',nodeName())}<div class="empty red">${esc(e.message)}</div>`}
|
||||
}
|
||||
function securityCapabilityPills(c){return `<span class="tag ${c.host_root_available?'oktag':''}">host root ${c.host_root_available?'✓':'×'}</span><span class="tag ${c.target_verified?'oktag':''}">host namespace ${c.target_verified?'✓':'×'}</span><span class="tag ${c.allow_changes?'oktag':''}">${c.allow_changes?'manage':'audit only'}</span><span class="tag ${c.allow_package_management?'oktag':''}">packages ${c.allow_package_management?'enabled':'locked'}</span>`}
|
||||
function securityComponentCard(key,title,desc,st={},managed={},conflicts=[]){const installed=!!st.installed,active=!!st.active,drift=!!st.drift,c=window.__securityCaps||{},manage=!!(c.allow_changes&&c.executor_available),packages=!!(c.allow_package_management&&c.executor_available),manageDisabled=manage?'':'disabled title="Host security changes are disabled for this environment"',pkgDisabled=packages?'':'disabled title="Host package management is disabled for this environment"';return `<section class="securityCard"><div class="securityCardHead"><div><span class="securityIcon">${key==='firewall'?'⛨':key==='fail2ban'?'⊘':'≋'}</span><div><h2>${esc(title)}</h2><p>${esc(desc)}</p></div></div>${installed?(active?badge('up'):badge('paused')):badge('unknown')}</div><div class="securityFacts"><span><small>Installed</small><b>${installed?'Yes':'No'}</b></span><span><small>Runtime</small><b class="${active?'green':'muted'}">${active?'Active':'Inactive'}</b></span><span><small>Boot</small><b>${st.enabled?'Enabled':'—'}</b></span><span><small>Config</small><b class="${drift?'amber':''}">${managed?.configured?(drift?'Drift':'Managed'):'Not managed'}</b></span></div>${st.version?`<div class="securityVersion">${esc(st.version)}</div>`:''}${st.detail?`<pre class="securityDetail">${esc(st.detail)}</pre>`:''}${conflicts?.length?`<div class="notice warnNotice"><b>Conflict:</b> ${esc(conflicts.join(', '))} active. Dockwatch firewall apply is blocked.</div>`:''}<div class="securityActions">${!installed?`<button class="btn primary" data-secinstall="${key}" ${pkgDisabled}>Install</button>`:''}<button class="btn" id="configure-${key}" ${!installed||!manage?'disabled':''} ${!manage?'title="Host security changes are disabled for this environment"':''}>Configure</button>${installed?`<button class="btn tiny" data-seccomponent="${key}" data-secaction="enable" ${manageDisabled}>Enable</button><button class="btn tiny" data-seccomponent="${key}" data-secaction="disable" ${manageDisabled}>Disable</button><button class="btn tiny" data-seccomponent="${key}" data-secaction="restart" ${manageDisabled}>Restart</button>${key!=='firewall'?`<button class="btn tiny" data-seccomponent="${key}" data-secaction="reload" ${manageDisabled}>Reload</button>`:''}<button class="btn tiny" data-secinstall="${key}" ${pkgDisabled} title="Uses the host package manager to install the currently available package version">Upgrade</button>`:''}</div></section>`}
|
||||
function securityComponentCard(key,title,desc,st={},managed={},conflicts=[]){const installed=!!st.installed,active=!!st.active,drift=!!st.drift,c=window.__securityCaps||{},manage=!!(c.allow_changes&&c.executor_available),packages=!!(c.allow_package_management&&c.executor_available),manageDisabled=manage?'':'disabled title="Host security changes are disabled for this environment"',pkgDisabled=packages?'':'disabled title="Host package management is disabled for this environment"',canConfigure=manage&&(key==='firewall'||installed);return `<section class="securityCard"><div class="securityCardHead"><div><span class="securityIcon">${key==='firewall'?'⛨':key==='fail2ban'?'⊘':'≋'}</span><div><h2>${title}</h2><p>${esc(desc)}</p></div></div>${installed?(active?badge('up'):badge('paused')):badge('unknown')}</div><div class="securityFacts"><span><small>Installed</small><b>${installed?'Yes':'No'}</b></span><span><small>Runtime</small><b class="${active?'green':'muted'}">${active?'Active':'Inactive'}</b></span><span><small>Boot</small><b>${st.enabled?'Enabled':'—'}</b></span><span><small>Config</small><b class="${drift?'amber':''}">${managed?.configured?(drift?'Drift':'Managed'):'Not managed'}</b></span></div>${st.version?`<div class="securityVersion">${esc(st.version)}</div>`:''}${st.detail?`<pre class="securityDetail">${esc(st.detail)}</pre>`:''}${conflicts?.length?`<div class="notice dangerNotice"><b>Firewall conflict:</b> ${esc(conflicts.join(', '))}. Resolve competing active frontends before applying changes.</div>`:''}<div class="securityActions">${!installed?`<button class="btn primary" data-secinstall="${key}" ${pkgDisabled}>Install</button>`:''}<button class="btn" id="configure-${key}" ${!canConfigure?'disabled':''} ${!manage?'title="Host security changes are disabled for this environment"':''}>Configure</button>${installed?`<button class="btn tiny" data-seccomponent="${key}" data-secaction="enable" ${manageDisabled}>Enable</button><button class="btn tiny" data-seccomponent="${key}" data-secaction="disable" ${manageDisabled}>Disable</button><button class="btn tiny" data-seccomponent="${key}" data-secaction="restart" ${manageDisabled}>Restart</button>${key!=='firewall'?`<button class="btn tiny" data-seccomponent="${key}" data-secaction="reload" ${manageDisabled}>Reload</button>`:''}<button class="btn tiny" data-secinstall="${key}" ${pkgDisabled} title="Uses the host package manager to install the currently available package version">Upgrade</button>`:''}</div></section>`}
|
||||
function securityFindings(rows){if(!rows.length)return '<div class="empty">No findings.</div>';return `<div class="securityFindings">${rows.map(f=>`<div class="securityFinding sev-${esc(f.severity)}"><span>${f.severity==='high'?'!':f.severity==='medium'?'△':f.severity==='ok'?'✓':'i'}</span><div><b>${esc(f.title)}</b><p>${esc(f.detail)}</p>${f.action?`<small>${esc(f.action)}</small>`:''}</div></div>`).join('')}</div>`}
|
||||
async function securityInstall(component,btn){if(!confirm(`Install or upgrade ${component} on ${nodeName()} using the host package manager?`))return;setBusy(btn,true,'Working…');try{const out=await api(`/api/security/components/${encodeURIComponent(component)}/install${qnode()}`,{method:'POST',body:JSON.stringify({enable:component!=='firewall'})});toast(out.message||'Package operation complete');if(out.output)showOutput(`${component} package operation`,out.output);else renderSecurity()}catch(e){toast(e.message);setBusy(btn,false)}}
|
||||
async function securityInstall(component,btn,provider=''){if(component==='firewall'&&!provider)provider=(window.__firewallBackend||{}).selected||'nftables';if(!confirm(`Install or upgrade ${component==='firewall'?provider:component} on ${nodeName()} using the host package manager?`))return;setBusy(btn,true,'Working…');try{const out=await api(`/api/security/components/${encodeURIComponent(component)}/install${qnode()}`,{method:'POST',body:JSON.stringify({enable:component!=='firewall',provider})});toast(out.message||'Package operation complete');if(out.output)showOutput(`${component} package operation`,out.output);else renderSecurity()}catch(e){toast(e.message);setBusy(btn,false)}}
|
||||
async function securityComponentAction(component,action,btn){if(['disable','stop'].includes(action)&&!confirm(`${action} ${component} on ${nodeName()}?`))return;setBusy(btn,true,'Working…');try{const out=await api(`/api/security/components/${encodeURIComponent(component)}/actions/${encodeURIComponent(action)}${qnode()}`,{method:'POST',body:'{}'});toast(out.message||`${component} ${action} complete`);await renderSecurity()}catch(e){toast(e.message);setBusy(btn,false)}}
|
||||
async function securityFirewallModal(){try{const p=await api('/api/security/firewall'+qnode());firewallEditor(p)}catch(e){toast(e.message)}}
|
||||
function firewallEditor(p){const rules=asArray(p.rules);modal(`<div class="modalhead"><h2>Managed nftables firewall</h2><button class="closex" data-close>×</button></div><div class="modalbody"><div class="notice"><b>Scope:</b> Dockwatch manages only <code>table inet dockwatch</code> and its INPUT chain. It never flushes the global ruleset and does not alter Docker forwarding/NAT chains.</div><div class="fieldgrid" style="margin-top:12px"><label class="switch"><input id="fwEnabled" type="checkbox" ${p.enabled?'checked':''}> Enable Dockwatch firewall policy</label><div class="field"><label>Default inbound</label><select id="fwDefault"><option value="accept">ACCEPT</option><option value="drop">DROP</option></select></div><label class="switch"><input id="fwICMP" type="checkbox" ${p.allow_icmp!==false?'checked':''}> Allow ICMP / IPv6 ICMP</label><div class="field full"><label>Trusted CIDRs · one per line</label><textarea id="fwTrusted" placeholder="192.0.2.0/24\n2001:db8::/32">${esc(asArray(p.trusted_cidrs).join('\n'))}</textarea></div></div><div class="panel inset" style="margin-top:12px"><div class="panelhead"><h3>Port rules</h3><button class="btn tiny" id="fwAddRule">+ Rule</button></div><div id="fwRules">${rules.map(firewallRuleRow).join('')}</div></div><div class="notice dangerNotice" style="margin-top:12px"><b>Lockout protection:</b> Apply starts a 90-second rollback timer. You must explicitly keep the rules after confirming that this UI is still reachable. Default DROP requires that you add the management ports/CIDRs you need.</div></div><div class="modalfoot"><button class="btn" data-close>Cancel</button><button class="btn" id="fwPreview">Preview nftables</button><button class="btn danger" id="fwApply">Apply with rollback</button></div>`);$('#fwDefault').value=p.default_inbound||'accept';$('#fwAddRule').onclick=()=>{$('#fwRules').insertAdjacentHTML('beforeend',firewallRuleRow({action:'accept',protocol:'tcp',port:'',source:'',comment:''}));wireFirewallRows()};wireFirewallRows();$('#fwPreview').onclick=async()=>{try{const x=await api(`/api/security/firewall/preview${qnode()}`,{method:'POST',body:JSON.stringify(collectFirewallPolicy())});showSecurityPreview('nftables preview',x.rendered,x.warnings,x.conflicts)}catch(e){toast(e.message)}};$('#fwApply').onclick=async()=>{const policy=collectFirewallPolicy();if(policy.default_inbound==='drop'&&!confirm('Default inbound DROP can disconnect this host. Confirm that your SSH/Dockwatch management ports are explicitly allowed. Continue with timed rollback?'))return;const btn=$('#fwApply');setBusy(btn,true,'Applying…');try{const out=await api(`/api/security/firewall/apply${qnode()}`,{method:'POST',body:JSON.stringify({policy,rollback_seconds:90})});firewallCommitModal(out)}catch(e){toast(e.message);setBusy(btn,false)}}}
|
||||
function firewallRuleRow(r={}){return `<div class="fwRule"><select class="fwAction"><option value="accept" ${r.action!=='drop'?'selected':''}>ALLOW</option><option value="drop" ${r.action==='drop'?'selected':''}>DENY</option></select><select class="fwProto"><option value="tcp" ${r.protocol!=='udp'?'selected':''}>TCP</option><option value="udp" ${r.protocol==='udp'?'selected':''}>UDP</option></select><input class="fwPort" placeholder="22 or 8000-8100" value="${esc(r.port||'')}"><input class="fwSource" placeholder="Source CIDR · optional" value="${esc(r.source||'')}"><input class="fwComment" placeholder="Comment" value="${esc(r.comment||'')}"><button class="iconbtn fwRemove" title="Remove">×</button></div>`}
|
||||
function wireFirewallRows(){$$('.fwRemove').forEach(b=>b.onclick=()=>b.closest('.fwRule').remove())}
|
||||
function collectFirewallPolicy(){return {enabled:$('#fwEnabled').checked,default_inbound:$('#fwDefault').value,allow_icmp:$('#fwICMP').checked,trusted_cidrs:$('#fwTrusted').value.split(/[\n,]+/).map(x=>x.trim()).filter(Boolean),rules:$$('.fwRule').map(r=>({action:r.querySelector('.fwAction').value,protocol:r.querySelector('.fwProto').value,port:r.querySelector('.fwPort').value.trim(),source:r.querySelector('.fwSource').value.trim(),comment:r.querySelector('.fwComment').value.trim()})).filter(r=>r.port)}}
|
||||
async function securityFirewallModal(){try{const p=await api('/api/security/firewall'+qnode()),preview=await api(`/api/security/firewall/preview${qnode()}`,{method:'POST',body:JSON.stringify(p)});firewallEditor(preview.policy||p,preview)}catch(e){toast(e.message)}}
|
||||
function firewallProviderOptions(current='auto'){return ['auto','ufw','firewalld','nftables'].map(v=>`<option value="${v}" ${current===v?'selected':''}>${v==='auto'?'Auto detect':v}</option>`).join('')}
|
||||
function firewallRuntimeHTML(preview={}){const b=preview.backend||{},r=preview.runtime||{},available=asArray(b.available),active=asArray(b.active);return `<div class="panel inset fwRuntime"><div class="panelhead"><h3>Detected host firewall</h3><span class="tag">selected: ${esc(b.selected||'—')}</span></div><div class="securityFacts"><span><small>Available</small><b>${esc(available.join(', ')||'none')}</b></span><span><small>Active</small><b>${esc(active.join(', ')||'none')}</b></span><span><small>Default</small><b>${esc(r.default_inbound||'—')}</b></span><span><small>Zone</small><b>${esc(r.zone||b.default_zone||'—')}</b></span></div>${b.reason?`<div class="notice ${asArray(b.conflicts).length?'dangerNotice':'warnNotice'}">${esc(b.reason)}</div>`:''}<div class="field"><label>Existing native rules/state <span class="muted">· read-only · foreign rules are preserved</span></label><pre class="terminal fwExisting" style="max-height:230px">${esc(r.raw||'No runtime rules reported.')}</pre></div></div>`}
|
||||
function firewallEditor(p,preview={}){const rules=asArray(p.rules),backend=preview.backend||{},selected=backend.selected||p.provider||'auto',installed=asArray(backend.available).includes(selected),packages=!!((window.__securityCaps||{}).allow_package_management&&(window.__securityCaps||{}).executor_available);modal(`<div class="modalhead"><h2>Host firewall · ${esc(selected)}</h2><button class="closex" data-close>×</button></div><div class="modalbody"><div class="notice"><b>Provider model:</b> Auto uses active UFW or firewalld when present and falls back to native nftables. Dockwatch preserves rules it does not own and never runs <code>ufw reset</code> or <code>nft flush ruleset</code>.</div><div class="fieldgrid" style="margin-top:12px"><div class="field"><label>Firewall provider</label><select id="fwProvider">${firewallProviderOptions(p.provider||'auto')}</select></div><label class="switch"><input id="fwEnabled" type="checkbox" ${p.enabled?'checked':''}> Enable Dockwatch-managed rules</label><label class="switch"><input id="fwManageDefault" type="checkbox" ${p.manage_default?'checked':''} ${selected==='nftables'?'disabled':''}> Manage provider default inbound ${selected==='nftables'?'<span class="muted">(native policy is always local to Dockwatch table)</span>':''}</label><div class="field"><label>Default inbound</label><select id="fwDefault"><option value="accept">ACCEPT</option><option value="drop">DROP</option></select></div>${selected==='firewalld'?`<div class="field"><label>firewalld zone</label><input id="fwZone" value="${esc(p.zone||preview.runtime?.zone||backend.default_zone||'public')}" placeholder="public"></div>`:''}<label class="switch"><input id="fwICMP" type="checkbox" ${p.allow_icmp!==false?'checked':''} ${selected!=='nftables'?'disabled':''}> Allow ICMP / IPv6 ICMP ${selected!=='nftables'?'<span class="muted">(kept native by this provider)</span>':''}</label><div class="field full"><label>Trusted CIDRs · one per line</label><textarea id="fwTrusted" placeholder="192.0.2.0/24\n2001:db8::/32">${esc(asArray(p.trusted_cidrs).join('\n'))}</textarea></div></div>${firewallRuntimeHTML(preview)}<div class="panel inset" style="margin-top:12px"><div class="panelhead"><h3>Dockwatch-managed port rules</h3><button class="btn tiny" id="fwAddRule">+ Rule</button></div><div id="fwRules">${rules.map(firewallRuleRow).join('')}</div></div><div class="notice dangerNotice" style="margin-top:12px"><b>Lockout protection:</b> Apply starts a 90-second rollback timer. Foreign provider rules remain intact. If you opt into managing the provider's global default inbound, verify SSH/Dockwatch access before keeping the change.</div></div><div class="modalfoot">${!installed&&selected!=='auto'?`<button class="btn" id="fwInstall" ${packages?'':'disabled'}>Install ${esc(selected)}</button>`:''}<button class="btn" data-close>Cancel</button><button class="btn" id="fwPreview">Preview ${esc(selected)}</button><button class="btn danger" id="fwApply" ${preview.can_apply===false?'disabled':''}>Apply with rollback</button></div>`);$('#fwDefault').value=p.default_inbound||'accept';$('#fwProvider').onchange=async()=>{const policy=collectFirewallPolicy();policy.provider=$('#fwProvider').value;try{const x=await api(`/api/security/firewall/preview${qnode()}`,{method:'POST',body:JSON.stringify(policy)});firewallEditor(x.policy||policy,x)}catch(e){toast(e.message)}};$('#fwInstall')?.addEventListener('click',e=>securityInstall('firewall',e.currentTarget,selected));$('#fwAddRule').onclick=()=>{$('#fwRules').insertAdjacentHTML('beforeend',firewallRuleRow({action:'accept',protocol:'tcp',port:'',source:'',comment:''}));wireFirewallRows()};wireFirewallRows();$('#fwPreview').onclick=async()=>{try{const x=await api(`/api/security/firewall/preview${qnode()}`,{method:'POST',body:JSON.stringify(collectFirewallPolicy())});showSecurityPreview(`${x.backend?.selected||'firewall'} preview`,x.rendered,x.warnings,x.conflicts)}catch(e){toast(e.message)}};$('#fwApply').onclick=async()=>{const policy=collectFirewallPolicy(),provider=(preview.backend||{}).selected||policy.provider;if((provider==='nftables'||policy.manage_default)&&policy.default_inbound==='drop'&&!confirm('Default inbound DROP can disconnect this host. Confirm that your SSH/Dockwatch management ports are explicitly allowed. Continue with timed rollback?'))return;const btn=$('#fwApply');setBusy(btn,true,'Applying…');try{const out=await api(`/api/security/firewall/apply${qnode()}`,{method:'POST',body:JSON.stringify({policy,rollback_seconds:90})});firewallCommitModal(out)}catch(e){toast(e.message);setBusy(btn,false)}}}
|
||||
function firewallRuleRow(r={}){return `<div class="fwRule"><select class="fwAction"><option value="accept" ${r.action==='accept'||!r.action?'selected':''}>ALLOW</option><option value="drop" ${r.action==='drop'?'selected':''}>DENY</option><option value="reject" ${r.action==='reject'?'selected':''}>REJECT</option><option value="limit" ${r.action==='limit'?'selected':''}>LIMIT</option></select><select class="fwProto"><option value="tcp" ${r.protocol!=='udp'?'selected':''}>TCP</option><option value="udp" ${r.protocol==='udp'?'selected':''}>UDP</option></select><input class="fwPort" placeholder="22 or 8000-8100" value="${esc(r.port||'')}"><input class="fwSource" placeholder="Source CIDR · optional" value="${esc(r.source||'')}"><input class="fwComment" placeholder="Comment" value="${esc(r.comment||'')}"><button class="iconbtn fwUp" title="Move up">↑</button><button class="iconbtn fwDown" title="Move down">↓</button><button class="iconbtn fwRemove" title="Remove">×</button></div>`}
|
||||
function wireFirewallRows(){$$('.fwRemove').forEach(b=>b.onclick=()=>b.closest('.fwRule').remove());$$('.fwUp').forEach(b=>b.onclick=()=>{const r=b.closest('.fwRule');if(r.previousElementSibling)r.parentElement.insertBefore(r,r.previousElementSibling)});$$('.fwDown').forEach(b=>b.onclick=()=>{const r=b.closest('.fwRule'),n=r.nextElementSibling;if(n)r.parentElement.insertBefore(n,r)})}
|
||||
function collectFirewallPolicy(){return {provider:$('#fwProvider')?.value||'auto',enabled:!!$('#fwEnabled')?.checked,manage_default:!!$('#fwManageDefault')?.checked,default_inbound:$('#fwDefault')?.value||'accept',zone:$('#fwZone')?.value.trim()||'',allow_icmp:$('#fwICMP')?.checked!==false,trusted_cidrs:($('#fwTrusted')?.value||'').split(/[\n,]+/).map(x=>x.trim()).filter(Boolean),rules:$$('.fwRule').map(r=>({action:r.querySelector('.fwAction').value,protocol:r.querySelector('.fwProto').value,port:r.querySelector('.fwPort').value.trim(),source:r.querySelector('.fwSource').value.trim(),comment:r.querySelector('.fwComment').value.trim()})).filter(r=>r.port)}}
|
||||
function showSecurityPreview(title,text,warnings=[],conflicts=[]){modal(`<div class="modalhead"><h2>${esc(title)}</h2><button class="closex" data-close>×</button></div><div class="modalbody">${warnings.map(x=>`<div class="notice">${esc(x)}</div>`).join('')}${conflicts?.length?`<div class="notice dangerNotice">Conflicts: ${esc(conflicts.join(', '))}</div>`:''}<pre class="terminal" style="max-height:55vh">${esc(text||'')}</pre></div><div class="modalfoot"><button class="btn" data-close>Close</button></div>`)}
|
||||
function firewallCommitModal(out){const end=Number(out.expires_at||0)*1000;modal(`<div class="modalhead"><h2>Firewall applied · verification window</h2><button class="closex" data-close>×</button></div><div class="modalbody"><div class="notice dangerNotice"><b>Do not close this dialog yet.</b> If the new policy breaks access, Dockwatch will restore the previous managed firewall policy automatically.</div><div class="securityCountdown"><small>Automatic rollback in</small><strong id="fwCountdown">…</strong></div><p class="muted">Verify SSH and any other management path in a separate session. Then keep the change.</p></div><div class="modalfoot"><button class="btn danger" id="fwRollbackNow">Rollback now</button><button class="btn primary" id="fwCommitNow">Keep changes</button></div>`);const tick=()=>{const s=Math.max(0,Math.ceil((end-Date.now())/1000));const e=$('#fwCountdown');if(e)e.textContent=`${s}s`;if(s>0)setTimeout(tick,1000);else{closeModal();renderSecurity()}};tick();$('#fwCommitNow').onclick=async()=>{try{await api(`/api/security/firewall/commit${qnode()}`,{method:'POST',body:JSON.stringify({change_id:out.change_id})});toast('Firewall policy committed.');closeModal();renderSecurity()}catch(e){toast(e.message)}};$('#fwRollbackNow').onclick=async()=>{try{await api(`/api/security/firewall/rollback${qnode()}`,{method:'POST',body:JSON.stringify({change_id:out.change_id})});toast('Firewall rolled back.');closeModal();renderSecurity()}catch(e){toast(e.message)}}}
|
||||
function firewallCommitModal(out){const end=Number(out.expires_at||0)*1000;modal(`<div class="modalhead"><h2>Firewall applied · verification window</h2><button class="closex" data-close>×</button></div><div class="modalbody"><div class="notice dangerNotice"><b>Do not close this dialog yet.</b> If the new policy breaks access, Dockwatch will restore the previous Dockwatch-managed policy and provider default snapshot automatically.</div><div class="securityCountdown"><small>Automatic rollback in</small><strong id="fwCountdown">…</strong></div><p class="muted">Verify SSH and any other management path in a separate session. Then keep the change.</p></div><div class="modalfoot"><button class="btn danger" id="fwRollbackNow">Rollback now</button><button class="btn primary" id="fwCommitNow">Keep changes</button></div>`);const tick=()=>{const s=Math.max(0,Math.ceil((end-Date.now())/1000));const e=$('#fwCountdown');if(e)e.textContent=`${s}s`;if(s>0)setTimeout(tick,1000);else{closeModal();renderSecurity()}};tick();$('#fwCommitNow').onclick=async()=>{try{await api(`/api/security/firewall/commit${qnode()}`,{method:'POST',body:JSON.stringify({change_id:out.change_id})});toast('Firewall policy committed.');closeModal();renderSecurity()}catch(e){toast(e.message)}};$('#fwRollbackNow').onclick=async()=>{try{await api(`/api/security/firewall/rollback${qnode()}`,{method:'POST',body:JSON.stringify({change_id:out.change_id})});toast('Firewall rolled back.');closeModal();renderSecurity()}catch(e){toast(e.message)}}}
|
||||
async function securityFail2BanModal(){try{const p=await api('/api/security/fail2ban'+qnode());fail2banEditor(p)}catch(e){toast(e.message)}}
|
||||
function fail2banEditor(p){modal(`<div class="modalhead"><h2>Fail2Ban policy</h2><button class="closex" data-close>×</button></div><div class="modalbody"><div class="notice">Dockwatch writes only <code>/etc/fail2ban/jail.d/dockwatch.local</code>. Other distro/user jails are preserved and remain effective.</div><div class="fieldgrid" style="margin-top:12px"><div class="field"><label>Ban time</label><input id="f2bBan" value="${esc(p.bantime||'1h')}"></div><div class="field"><label>Find time</label><input id="f2bFind" value="${esc(p.findtime||'10m')}"></div><div class="field"><label>Max retry</label><input id="f2bRetry" type="number" min="1" max="1000" value="${esc(p.maxretry||5)}"></div><div class="field"><label>Backend</label><select id="f2bBackend"><option>auto</option><option>systemd</option><option>polling</option><option>pyinotify</option></select></div><div class="field full"><label>Ignore IP/CIDR · one per line</label><textarea id="f2bIgnore">${esc(asArray(p.ignore_ip).join('\n'))}</textarea></div></div><div class="panel inset" style="margin-top:12px"><div class="panelhead"><h3>Jails</h3><button class="btn tiny" id="f2bAdd">+ Jail</button></div><div id="f2bJails">${asArray(p.jails).map(fail2banJailRow).join('')}</div></div></div><div class="modalfoot"><button class="btn" data-close>Cancel</button><button class="btn primary" id="f2bSave">Validate & apply</button></div>`);$('#f2bBackend').value=p.backend||'auto';$('#f2bAdd').onclick=()=>{$('#f2bJails').insertAdjacentHTML('beforeend',fail2banJailRow({enabled:true,backend:'auto'}));wireF2BRows()};wireF2BRows();$('#f2bSave').onclick=async()=>{const body={bantime:$('#f2bBan').value.trim(),findtime:$('#f2bFind').value.trim(),maxretry:Number($('#f2bRetry').value),backend:$('#f2bBackend').value,ignore_ip:$('#f2bIgnore').value.split(/[\n,]+/).map(x=>x.trim()).filter(Boolean),jails:$$('.f2bJail').map(r=>({name:r.querySelector('.f2bName').value.trim(),enabled:r.querySelector('.f2bEnabled').checked,port:r.querySelector('.f2bPort').value.trim(),filter:r.querySelector('.f2bFilter').value.trim(),backend:r.querySelector('.f2bBackend').value,logpath:r.querySelector('.f2bLog').value.trim(),maxretry:Number(r.querySelector('.f2bMax').value)||0})).filter(j=>j.name)};const btn=$('#f2bSave');setBusy(btn,true,'Applying…');try{const out=await api(`/api/security/fail2ban${qnode()}`,{method:'PUT',body:JSON.stringify(body)});toast(out.message||'Fail2Ban applied');closeModal();renderSecurity()}catch(e){toast(e.message);setBusy(btn,false)}}}
|
||||
function fail2banJailRow(j={}){return `<div class="f2bJail securityFormRow"><label class="switch"><input class="f2bEnabled" type="checkbox" ${j.enabled!==false?'checked':''}> enabled</label><input class="f2bName" placeholder="jail name" value="${esc(j.name||'')}"><input class="f2bPort" placeholder="port · ssh" value="${esc(j.port||'')}"><input class="f2bFilter" placeholder="filter" value="${esc(j.filter||'')}"><select class="f2bBackend"><option ${j.backend==='auto'||!j.backend?'selected':''}>auto</option><option ${j.backend==='systemd'?'selected':''}>systemd</option><option ${j.backend==='polling'?'selected':''}>polling</option><option ${j.backend==='pyinotify'?'selected':''}>pyinotify</option></select><input class="f2bLog" placeholder="log path · optional" value="${esc(j.logpath||'')}"><input class="f2bMax" type="number" min="0" max="1000" placeholder="retries" value="${esc(j.maxretry||'')}"><button class="iconbtn f2bRemove">×</button></div>`}
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Dockwatch</title>
|
||||
<link rel="stylesheet" href="/styles.css?v=9.4.1">
|
||||
<link rel="stylesheet" href="/styles.css?v=9.5">
|
||||
</head>
|
||||
<body>
|
||||
<div id="shell">
|
||||
@@ -39,5 +39,5 @@
|
||||
</main>
|
||||
</div>
|
||||
<div id="modalRoot"></div><div id="toast"></div>
|
||||
<script src="/app.js?v=9.4.1"></script>
|
||||
<script src="/app.js?v=9.5"></script>
|
||||
</body></html>
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ body.sidebar-collapsed #shell{grid-template-columns:64px 1fr}body.sidebar-collap
|
||||
.mono{font:11px/1.45 "Cascadia Code","SFMono-Regular",Consolas,monospace}.warn{color:var(--amber)}.compactList{margin:8px 0 0;padding-left:18px;color:var(--muted)}.compactList li{margin:6px 0}.panel.inset{padding:12px;background:var(--panel2);overflow:visible}.panel.inset h3{margin:0;font-size:12px}.dangerNotice{border-color:#592733!important;background:#2b171e!important;color:#f1bcc5!important}.tablewrap{overflow:auto;border:1px solid var(--line);border-radius:7px}.tablewrap .table{min-width:720px}
|
||||
|
||||
/* Host Security layer */
|
||||
.securityHero{display:flex;justify-content:space-between;align-items:center;gap:20px;border:1px solid var(--line);border-radius:10px;padding:18px 20px;margin-bottom:12px;background:linear-gradient(135deg,var(--panel),var(--panel2))}.securityHero.securityManage{border-color:#24533f}.securityHero.securityAudit{border-color:#5f512b}.securityHero.securityOff{opacity:.78}.securityHero small{font-size:9px;letter-spacing:.12em;color:var(--muted)}.securityHero p{margin:5px 0 0;color:var(--muted);font-size:11px}.securityScore{font-size:38px;font-weight:750;line-height:1;margin-top:4px}.securityScore span{font-size:13px;color:var(--muted);font-weight:500}.securityCaps{display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end}.tag.oktag{border-color:#2b5f49;color:#74d7aa;background:#13261f}.securityGrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.securityCard{background:var(--panel);border:1px solid var(--line);border-radius:9px;padding:14px;min-width:0}.securityCardHead{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}.securityCardHead>div:first-child{display:flex;gap:10px;align-items:flex-start;min-width:0}.securityCardHead h2{font-size:13px;margin:0}.securityCardHead p{font-size:10px;color:var(--muted);line-height:1.45;margin:4px 0 0}.securityIcon{width:30px;height:30px;border-radius:8px;background:var(--panel2);border:1px solid var(--line);display:grid;place-items:center;font-size:15px;flex:0 0 auto}.securityFacts{display:grid;grid-template-columns:repeat(4,1fr);gap:5px;margin-top:13px}.securityFacts span{background:var(--panel2);border:1px solid var(--line);border-radius:6px;padding:7px;min-width:0}.securityFacts small{display:block;color:var(--muted);font-size:8px;text-transform:uppercase;letter-spacing:.06em}.securityFacts b{font-size:10px;display:block;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.securityVersion{font:9px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--muted);margin-top:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.securityDetail{font:9px/1.45 ui-monospace,SFMono-Regular,Consolas,monospace;background:var(--panel2);border:1px solid var(--line);padding:8px;border-radius:6px;max-height:100px;overflow:auto;white-space:pre-wrap}.securityActions{display:flex;gap:5px;flex-wrap:wrap;margin-top:12px}.securityFindings{display:flex;flex-direction:column}.securityFinding{display:grid;grid-template-columns:24px minmax(0,1fr);gap:9px;padding:10px;border-top:1px solid var(--line)}.securityFinding:first-child{border-top:0}.securityFinding>span{width:22px;height:22px;border-radius:50%;display:grid;place-items:center;background:var(--panel2);font-weight:700}.securityFinding b{font-size:11px}.securityFinding p{font-size:10px;color:var(--muted);margin:3px 0}.securityFinding small{font-size:9px;color:var(--text)}.securityFinding.sev-high>span{background:var(--red2);color:var(--red)}.securityFinding.sev-medium>span{background:var(--amber2);color:var(--amber)}.securityFinding.sev-ok>span{background:var(--green2);color:var(--green)}.securitySafety{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;padding:10px}.securitySafety>div{border:1px solid var(--line);border-radius:7px;padding:10px;background:var(--panel2)}.securitySafety b{font-size:10px}.securitySafety p{font-size:9px;color:var(--muted);line-height:1.45;margin:4px 0 0}.warnNotice{border-color:#5e4a20!important;background:#2a2414!important;color:#e6ca7c!important}.fwRule{display:grid;grid-template-columns:90px 80px 140px minmax(150px,1fr) minmax(120px,1fr) 28px;gap:6px;margin-bottom:6px;align-items:center}.fwRule input,.fwRule select{min-width:0}.securityFormRow{display:grid;grid-template-columns:110px 130px 100px 100px 105px minmax(140px,1fr) 90px 28px;gap:6px;align-items:center;margin-bottom:6px}.securityFormRow.audit{grid-template-columns:minmax(240px,1fr) 80px minmax(120px,200px) 28px}.securityChecks{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.securityCountdown{display:flex;align-items:flex-end;justify-content:space-between;margin:20px 0;padding:16px;border:1px solid var(--line);border-radius:8px;background:var(--panel2)}.securityCountdown small{color:var(--muted)}.securityCountdown strong{font-size:32px}.notice code{font:10px ui-monospace,SFMono-Regular,Consolas,monospace}
|
||||
.securityHero{display:flex;justify-content:space-between;align-items:center;gap:20px;border:1px solid var(--line);border-radius:10px;padding:18px 20px;margin-bottom:12px;background:linear-gradient(135deg,var(--panel),var(--panel2))}.securityHero.securityManage{border-color:#24533f}.securityHero.securityAudit{border-color:#5f512b}.securityHero.securityOff{opacity:.78}.securityHero small{font-size:9px;letter-spacing:.12em;color:var(--muted)}.securityHero p{margin:5px 0 0;color:var(--muted);font-size:11px}.securityScore{font-size:38px;font-weight:750;line-height:1;margin-top:4px}.securityScore span{font-size:13px;color:var(--muted);font-weight:500}.securityCaps{display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end}.tag.oktag{border-color:#2b5f49;color:#74d7aa;background:#13261f}.securityGrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px}.securityCard{background:var(--panel);border:1px solid var(--line);border-radius:9px;padding:14px;min-width:0}.securityCardHead{display:flex;align-items:flex-start;justify-content:space-between;gap:10px}.securityCardHead>div:first-child{display:flex;gap:10px;align-items:flex-start;min-width:0}.securityCardHead h2{font-size:13px;margin:0}.securityCardHead p{font-size:10px;color:var(--muted);line-height:1.45;margin:4px 0 0}.securityIcon{width:30px;height:30px;border-radius:8px;background:var(--panel2);border:1px solid var(--line);display:grid;place-items:center;font-size:15px;flex:0 0 auto}.securityFacts{display:grid;grid-template-columns:repeat(4,1fr);gap:5px;margin-top:13px}.securityFacts span{background:var(--panel2);border:1px solid var(--line);border-radius:6px;padding:7px;min-width:0}.securityFacts small{display:block;color:var(--muted);font-size:8px;text-transform:uppercase;letter-spacing:.06em}.securityFacts b{font-size:10px;display:block;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.securityVersion{font:9px/1.4 ui-monospace,SFMono-Regular,Consolas,monospace;color:var(--muted);margin-top:8px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.securityDetail{font:9px/1.45 ui-monospace,SFMono-Regular,Consolas,monospace;background:var(--panel2);border:1px solid var(--line);padding:8px;border-radius:6px;max-height:100px;overflow:auto;white-space:pre-wrap}.securityActions{display:flex;gap:5px;flex-wrap:wrap;margin-top:12px}.securityFindings{display:flex;flex-direction:column}.securityFinding{display:grid;grid-template-columns:24px minmax(0,1fr);gap:9px;padding:10px;border-top:1px solid var(--line)}.securityFinding:first-child{border-top:0}.securityFinding>span{width:22px;height:22px;border-radius:50%;display:grid;place-items:center;background:var(--panel2);font-weight:700}.securityFinding b{font-size:11px}.securityFinding p{font-size:10px;color:var(--muted);margin:3px 0}.securityFinding small{font-size:9px;color:var(--text)}.securityFinding.sev-high>span{background:var(--red2);color:var(--red)}.securityFinding.sev-medium>span{background:var(--amber2);color:var(--amber)}.securityFinding.sev-ok>span{background:var(--green2);color:var(--green)}.securitySafety{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;padding:10px}.securitySafety>div{border:1px solid var(--line);border-radius:7px;padding:10px;background:var(--panel2)}.securitySafety b{font-size:10px}.securitySafety p{font-size:9px;color:var(--muted);line-height:1.45;margin:4px 0 0}.warnNotice{border-color:#5e4a20!important;background:#2a2414!important;color:#e6ca7c!important}.fwRule{display:grid;grid-template-columns:90px 80px 140px minmax(150px,1fr) minmax(120px,1fr) 28px 28px 28px;gap:6px;margin-bottom:6px;align-items:center}.fwRule input,.fwRule select{min-width:0}.fwRuntime{margin-top:12px}.fwExisting{font-size:9px;line-height:1.45;white-space:pre-wrap}.securityFormRow{display:grid;grid-template-columns:110px 130px 100px 100px 105px minmax(140px,1fr) 90px 28px;gap:6px;align-items:center;margin-bottom:6px}.securityFormRow.audit{grid-template-columns:minmax(240px,1fr) 80px minmax(120px,200px) 28px}.securityChecks{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:7px}.securityCountdown{display:flex;align-items:flex-end;justify-content:space-between;margin:20px 0;padding:16px;border:1px solid var(--line);border-radius:8px;background:var(--panel2)}.securityCountdown small{color:var(--muted)}.securityCountdown strong{font-size:32px}.notice code{font:10px ui-monospace,SFMono-Regular,Consolas,monospace}
|
||||
@media(max-width:1300px){.securityGrid{grid-template-columns:1fr}.securityFacts{grid-template-columns:repeat(4,1fr)}}
|
||||
@media(max-width:860px){.securityHero{align-items:flex-start;flex-direction:column}.securityCaps{justify-content:flex-start}.securityFacts{grid-template-columns:repeat(2,1fr)}.securitySafety{grid-template-columns:1fr}.fwRule{grid-template-columns:1fr 1fr}.fwRule .fwPort,.fwRule .fwSource,.fwRule .fwComment{grid-column:span 2}.securityFormRow{grid-template-columns:1fr 1fr}.securityFormRow .f2bLog{grid-column:span 2}.securityFormRow.audit{grid-template-columns:1fr}.securityChecks{grid-template-columns:1fr}}
|
||||
.volume-reconcile{margin-bottom:14px;padding:12px;border:1px solid var(--line);border-radius:8px;background:var(--panel2)}
|
||||
|
||||
Reference in New Issue
Block a user