feat: Oracle CIS report engine, CIS engine auto-update, and granular report params
Replace lightweight cis_runner.py with Oracle's official cis_reports.py engine (6660 lines, 48 CIS + 11 OBP checks). Add granular execution parameters (Level, OBP, Raw Data, Redact), per-report file storage with category browser, tenancy filter in Downloads, and individual file download. Add CIS Engine auto-update: check/download latest cis_reports.py from Oracle's GitHub repo with automatic patch reapplication (admin UI card + 3 new endpoints). Save engine version to app_settings on startup. Update README to v1.8 with new features, API endpoints, and versioning table.
This commit is contained in:
@@ -216,7 +216,9 @@ const S={user:null,token:null,tab:'chat',msgs:[],sid:null,reports:[],ociCfg:[],g
|
||||
chatModel:'',chatOci:'',chatRegion:'',chatCompartment:'',
|
||||
chatParams:{temperature:1,max_tokens:6000,top_p:0.95,top_k:1,frequency_penalty:0,presence_penalty:0},chatParamsOpen:false,chatUseTools:true,
|
||||
chatPrompts:[],editingPrompt:null,trackingReportId:null,ociRegions:{},rptSelRegions:[],rptRegionsOpen:false,rptRegionFilter:'',
|
||||
rptOciOpen:false,rptOciFilter:'',rptOciVal:'',rptRselOpen:false,rptRselFilter:'',rptRselVal:'',ociFormRegOpen:false,ociFormRegFilter:'',ociFormRegVal:''};
|
||||
rptOciOpen:false,rptOciFilter:'',rptOciVal:'',rptRselOpen:false,rptRselFilter:'',rptRselVal:'',ociFormRegOpen:false,ociFormRegFilter:'',ociFormRegVal:'',
|
||||
rptLevel:2,rptObp:false,rptRaw:false,rptRedact:false,reportFiles:{},dlExpandedRid:null,dlTenancyFilter:'',
|
||||
cisVer:null,cisCheckResult:null,cisUpdating:false};
|
||||
const API='/api';
|
||||
|
||||
async function $api(p,o={}){const h={...(o.headers||{})};if(S.token)h['Authorization']='Bearer '+S.token;
|
||||
@@ -234,6 +236,7 @@ async function loadData(){try{
|
||||
try{S.adbCfg=await $api('/adb/configs')}catch(e){S.adbCfg=[]}
|
||||
try{S.chatPrompts=await $api('/prompts/chat')}catch(e){S.chatPrompts=[]}
|
||||
if(S.user.role==='admin'){try{S.users=await $api('/users')}catch(e){}}
|
||||
try{S.cisVer=await $api('/cis-engine/version')}catch(e){}
|
||||
}catch(e){console.error(e)}}
|
||||
function R(){document.getElementById('app').innerHTML=S.user?rApp():rLogin();bind();if(S.editing?.type==='mcp')setTimeout(mcpTypeFields,0)}
|
||||
function switchTab(t){S.tab=t;S.expData=null;R();if(t==='audit'&&S.user.role==='admin')loadAudit();if(t==='downloads')refreshDl();
|
||||
@@ -441,8 +444,9 @@ ${S.rptOciOpen?`<div class="mdrop-list" style="position:absolute;top:100%;left:0
|
||||
${ociDdItems||'<div style="padding:.5rem;color:var(--t4);font-size:.72rem">Nenhuma credencial encontrada</div>'}
|
||||
</div>`:''}
|
||||
</div></div>
|
||||
<div class="ig"><label>MCP Server (opcional)</label><select id="rmcp"><option value="">Padrão (stub)</option>
|
||||
${S.mcpSvr.filter(m=>m.is_active).map(m=>`<option value="${m.id}">${m.name}${m.linked_adb_id?' 🗄️':''}</option>`).join('')}</select></div>
|
||||
<div class="ig"><label>CIS Level</label><select onchange="S.rptLevel=parseInt(this.value);R()">
|
||||
<option value="1" ${S.rptLevel===1?'selected':''}>Level 1</option>
|
||||
<option value="2" ${S.rptLevel===2?'selected':''}>Level 2 (default)</option></select></div>
|
||||
<div class="ig"><label>Regiões (opcional — vazio = todas)</label>
|
||||
<div class="dd" style="position:relative"><div onclick="S.rptRegionsOpen=!S.rptRegionsOpen;R()" style="min-height:34px;display:flex;flex-wrap:wrap;gap:.25rem;align-items:center;cursor:pointer;padding:.35rem .65rem;background:var(--bg2);border:1.5px solid var(--bd);border-radius:10px;font-size:.78rem">
|
||||
${S.rptSelRegions.length?selTags:'<span style="color:var(--t4);font-size:.78rem">Todas as regiões</span>'}</div>
|
||||
@@ -451,7 +455,29 @@ ${S.rptRegionsOpen?`<div class="mdrop-list" style="position:absolute;top:100%;le
|
||||
${ddItems||'<div style="padding:.5rem;color:var(--t4);font-size:.72rem">Nenhuma região encontrada</div>'}
|
||||
</div>`:''}
|
||||
</div></div></div>
|
||||
<button class="btn bp" onclick="runRpt()">▶ Executar CIS Compliance</button></div>`}
|
||||
<div style="display:flex;gap:1.2rem;flex-wrap:wrap;margin-top:.5rem">
|
||||
<label style="display:flex;align-items:center;gap:.35rem;font-size:.76rem;cursor:pointer"><input type="checkbox" ${S.rptObp?'checked':''} onchange="S.rptObp=this.checked"> OCI Best Practices (OBP)</label>
|
||||
<label style="display:flex;align-items:center;gap:.35rem;font-size:.76rem;cursor:pointer"><input type="checkbox" ${S.rptRaw?'checked':''} onchange="S.rptRaw=this.checked"> Raw Data Output</label>
|
||||
<label style="display:flex;align-items:center;gap:.35rem;font-size:.76rem;cursor:pointer"><input type="checkbox" ${S.rptRedact?'checked':''} onchange="S.rptRedact=this.checked"> Redact OCIDs</label>
|
||||
</div>
|
||||
<button class="btn bp" style="margin-top:.6rem" onclick="runRpt()">▶ Executar CIS Compliance</button></div>${rCisEngine()}`}
|
||||
function rCisEngine(){
|
||||
if(S.user?.role!=='admin')return '';
|
||||
const v=S.cisVer;const cr=S.cisCheckResult;
|
||||
let status='';
|
||||
if(S.cisUpdating)status='<span style="color:var(--w);font-size:.74rem">⏳ Atualizando...</span>';
|
||||
else if(cr){
|
||||
if(cr.update_available)status=`<span style="color:var(--w);font-size:.74rem">Nova versão: <b>${cr.remote_version}</b> (${cr.remote_date})</span> <button class="btn bp" style="font-size:.7rem;padding:.2rem .6rem;margin-left:.4rem" onclick="cisDoUpdate()">Atualizar</button>`;
|
||||
else status='<span style="color:var(--ok);font-size:.74rem">✓ Versão mais recente</span>';
|
||||
}
|
||||
return`<div class="cd" style="margin-top:.85rem"><div class="ct">⚙️ CIS Engine</div>
|
||||
<div style="display:flex;align-items:center;gap:.8rem;flex-wrap:wrap">
|
||||
<span style="font-size:.8rem">Versão: <b>${v?v.version:'...'}</b>${v&&v.updated_date!=='unknown'?' ('+v.updated_date+')':''}</span>
|
||||
<button class="btn bs" style="font-size:.7rem;padding:.2rem .6rem" onclick="cisCheckUpdate()">Verificar Atualização</button>
|
||||
${status}</div></div>`}
|
||||
async function cisLoadVersion(){try{S.cisVer=await $api('/cis-engine/version');R()}catch(e){console.error('cisVer',e)}}
|
||||
async function cisCheckUpdate(){S.cisCheckResult=null;R();try{S.cisCheckResult=await $api('/cis-engine/check-update');R()}catch(e){alert('Erro ao verificar: '+e.message)}}
|
||||
async function cisDoUpdate(){if(!confirm('Atualizar CIS Engine para a versão mais recente do GitHub?'))return;S.cisUpdating=true;R();try{const d=await $api('/cis-engine/update',{method:'POST'});alert(`Atualizado: ${d.old_version} → ${d.new_version}\nPatches aplicados: ${d.patches_applied.join(', ')||'nenhum'}`);S.cisCheckResult=null;S.cisUpdating=false;await cisLoadVersion()}catch(e){S.cisUpdating=false;R();alert('Erro: '+e.message)}}
|
||||
function ldRpt(id){const f=document.getElementById('rfr');if(f)f.src=API+'/reports/'+id+'/html'}
|
||||
function rptPickRegion(r){S.rptSelRegions.push(r);S.rptRegionFilter='';R()}
|
||||
function rptRemoveRegion(r){S.rptSelRegions=S.rptSelRegions.filter(x=>x!==r);R()}
|
||||
@@ -460,9 +486,8 @@ function rptPickRsel(id){S.rptRselVal=id;S.rptRselOpen=false;S.rptRselFilter='';
|
||||
async function runRpt(){if(S.reports.some(r=>r.status==='running'))return alert('Já existe um relatório em execução. Aguarde ou cancele antes de iniciar outro.');
|
||||
const c=S.rptOciVal;if(!c)return alert('Selecione config OCI');
|
||||
const rg=S.rptSelRegions;
|
||||
const mcp=document.getElementById('rmcp').value||null;
|
||||
S.rptRegionsOpen=false;
|
||||
try{const d=await $api('/reports/run',{method:'POST',body:{config_id:c,mcp_server_id:mcp,regions:rg.length?rg:null}});
|
||||
try{const d=await $api('/reports/run',{method:'POST',body:{config_id:c,regions:rg.length?rg:null,level:S.rptLevel,obp:S.rptObp,raw:S.rptRaw,redact_output:S.rptRedact}});
|
||||
S.trackingReportId=d.report_id;S.rptSelRegions=[];S.rptOciVal='';S.reports=await $api('/reports');R();startRptPoll()}catch(e){alert('Erro: '+e.message)}}
|
||||
let _rptPollTimer=null;
|
||||
function startRptPoll(){stopRptPoll();_rptPollTimer=setInterval(pollRptProgress,3000)}
|
||||
@@ -472,7 +497,15 @@ async function pollRptProgress(){if(!S.trackingReportId)return stopRptPoll();
|
||||
const el=document.getElementById('rpt-progress');
|
||||
if(el){const lines=(p.progress||'').split('\n').filter(Boolean);
|
||||
const last=lines[lines.length-1]||'Iniciando...';
|
||||
const m=last.match(/\[(\d+)\/(\d+)\]/);const pct=m?Math.round((parseInt(m[1])/parseInt(m[2]))*100):0;
|
||||
const ft=(p.progress||'').toLowerCase();
|
||||
let pct=5;
|
||||
if(ft.includes('identity domains'))pct=10;
|
||||
if(ft.includes('processing home region'))pct=20;
|
||||
if(ft.includes('processing regional'))pct=35;
|
||||
if(ft.includes('cis summary report'))pct=60;
|
||||
if(ft.includes('writing cis reports'))pct=75;
|
||||
if(ft.includes('best practices'))pct=85;
|
||||
if(ft.includes('finished'))pct=100;
|
||||
el.innerHTML=rProgressCard(p.status,last,pct,lines,S.trackingReportId)}
|
||||
if(p.status==='completed'||p.status==='failed'||p.status==='cancelled'){stopRptPoll();S.trackingReportId=null;S.reports=await $api('/reports');R()}}catch(e){}}
|
||||
async function cancelRpt(rid){if(!confirm('Cancelar este relatório?'))return;
|
||||
@@ -490,17 +523,47 @@ ${lines.length>1?`<details style="margin-top:.35rem"><summary style="font-size:.
|
||||
</div></div>`}
|
||||
|
||||
/* ── Downloads ── */
|
||||
function rDl(){return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.55rem">
|
||||
<span style="font-size:.78rem;color:var(--t3)">${S.reports.length} relatório(s)</span>
|
||||
function rDl(){
|
||||
const tenancies=[...new Set(S.reports.map(r=>r.tenancy_name))].sort();
|
||||
const filtered=S.dlTenancyFilter?S.reports.filter(r=>r.tenancy_name===S.dlTenancyFilter):S.reports;
|
||||
return`<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:.55rem;gap:.5rem;flex-wrap:wrap">
|
||||
<div style="display:flex;align-items:center;gap:.5rem">
|
||||
<select style="font-size:.76rem;min-width:180px" onchange="S.dlTenancyFilter=this.value;S.dlExpandedRid=null;R()">
|
||||
<option value="">Todas as Tenancies (${S.reports.length})</option>
|
||||
${tenancies.map(t=>`<option value="${t}" ${S.dlTenancyFilter===t?'selected':''}>${t} (${S.reports.filter(r=>r.tenancy_name===t).length})</option>`).join('')}
|
||||
</select>
|
||||
<span style="font-size:.72rem;color:var(--t4)">${filtered.length} report(s)</span></div>
|
||||
<button class="btn bs bsm" onclick="refreshDl()">🔄 Atualizar</button></div>
|
||||
<div class="cd"><table><thead><tr><th>Tenancy</th><th>Status</th><th>Criado</th><th>Concluído</th><th>Ações</th></tr></thead>
|
||||
<tbody>${S.reports.map(r=>`<tr><td><strong>${r.tenancy_name}</strong></td>
|
||||
<td><span class="badge ${r.status==='completed'?'b-pass':r.status==='failed'||r.status==='cancelled'?'b-fail':'b-pend'}">${r.status==='cancelled'?'cancelado':r.status}</span>${r.status==='running'&&r.progress?`<div style="font-size:.62rem;color:var(--t4);margin-top:.2rem">${(r.progress||'').split('\\n').filter(Boolean).pop()||''}</div>`:''}</td>
|
||||
<td style="font-size:.72rem">${r.created_at||'—'}</td><td style="font-size:.72rem">${r.completed_at||'—'}</td>
|
||||
<td>${r.status==='completed'?`<button class="btn bs bsm" onclick="dlRpt('${r.id}','json')">JSON</button> <button class="btn bs bsm" onclick="dlRpt('${r.id}','html')">HTML</button>`:r.status==='running'?`<button class="btn bs bsm" style="color:var(--err);border-color:var(--err)" onclick="cancelRpt('${r.id}')">Cancelar</button>`:'<span style="color:var(--t4)">—</span>'}</td></tr>`).join('')}</tbody></table>
|
||||
${!S.reports.length?'<div class="emp"><div class="eic">📂</div><p>Nenhum relatório.</p></div>':''}</div>`}
|
||||
<div class="cd"><table><thead><tr><th>Tenancy</th><th>Level</th><th>Opções</th><th>Status</th><th>Executado</th><th>Concluído</th><th>Arquivos</th><th>Ações</th></tr></thead>
|
||||
<tbody>${filtered.map(r=>{
|
||||
const opts=[];if(r.obp_checks)opts.push('OBP');if(r.raw_data)opts.push('Raw');if(r.redact_output)opts.push('Redact');
|
||||
const isExp=S.dlExpandedRid===r.id;
|
||||
const fHtml=isExp&&S.reportFiles[r.id]?renderFileList(r.id,S.reportFiles[r.id]):'';
|
||||
return`<tr><td><strong>${r.tenancy_name}</strong></td>
|
||||
<td style="font-size:.72rem">${r.level||2}</td>
|
||||
<td style="font-size:.68rem">${opts.join(', ')||'—'}</td>
|
||||
<td><span class="badge ${r.status==='completed'?'b-pass':r.status==='failed'||r.status==='cancelled'?'b-fail':'b-pend'}">${r.status==='cancelled'?'cancelado':r.status}</span></td>
|
||||
<td style="font-size:.72rem">${r.created_at||'—'}</td>
|
||||
<td style="font-size:.72rem">${r.completed_at||'—'}</td>
|
||||
<td>${r.status==='completed'?`<button class="btn bs bsm" style="font-size:.68rem" onclick="toggleFiles('${r.id}')">${isExp?'Ocultar':'Ver'} Arquivos</button>`:'—'}</td>
|
||||
<td>${r.status==='completed'?`<button class="btn bs bsm" onclick="dlRpt('${r.id}','html')">HTML</button> <button class="btn bs bsm" onclick="dlRpt('${r.id}','json')">JSON</button>`:r.status==='running'?`<button class="btn bs bsm" style="color:var(--err);border-color:var(--err)" onclick="cancelRpt('${r.id}')">Cancelar</button>`:'—'}</td></tr>
|
||||
${isExp?`<tr><td colspan="8" style="padding:0">${fHtml}</td></tr>`:''}`}).join('')}</tbody></table>
|
||||
${!filtered.length?'<div class="emp"><div class="eic">📂</div><p>Nenhum relatório.</p></div>':''}</div>`}
|
||||
async function toggleFiles(rid){if(S.dlExpandedRid===rid){S.dlExpandedRid=null;R();return}
|
||||
S.dlExpandedRid=rid;if(!S.reportFiles[rid]){try{S.reportFiles[rid]=await $api('/reports/'+rid+'/files')}catch(e){S.reportFiles[rid]=[]}}R()}
|
||||
function renderFileList(rid,files){if(!files.length)return'<div style="padding:.6rem;color:var(--t4);font-size:.72rem">Nenhum arquivo encontrado</div>';
|
||||
const groups={};files.forEach(f=>{if(!groups[f.file_category])groups[f.file_category]=[];groups[f.file_category].push(f)});
|
||||
const labels={summary:'Summary Reports',cis_finding:'CIS Findings',obp_finding:'OBP Findings',obp_best_practice:'OBP Best Practices',raw_data:'Raw Data',error:'Error Reports',diagram:'Diagrams',consolidated:'Consolidated',other:'Other'};
|
||||
let h='<div style="padding:.6rem .8rem;background:var(--bg2);border-top:1px solid var(--b2)">';
|
||||
for(const[cat,cf] of Object.entries(groups)){
|
||||
h+=`<div style="margin-bottom:.5rem"><div style="font-size:.7rem;font-weight:600;color:var(--t3);margin-bottom:.3rem">${labels[cat]||cat} (${cf.length})</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:.3rem">`;
|
||||
cf.forEach(f=>{const kb=Math.round(f.file_size/1024);
|
||||
h+=`<a href="${API}/reports/${rid}/files/${f.id}/download" target="_blank" class="btn bs bsm" style="font-size:.64rem;text-decoration:none">${f.file_name} (${kb}KB)</a>`});
|
||||
h+='</div></div>'}
|
||||
h+='</div>';return h}
|
||||
function dlRpt(id,f){window.open(API+'/reports/'+id+'/download?fmt='+f,'_blank')}
|
||||
async function refreshDl(){S.reports=await $api('/reports');R()}
|
||||
async function refreshDl(){S.reports=await $api('/reports');S.reportFiles={};S.dlExpandedRid=null;R()}
|
||||
|
||||
/* ── Edit helpers ── */
|
||||
function editCfg(type,id){S.editing={type,id};if(type==='oci'){const c=S.ociCfg.find(x=>x.id===id);S.ociFormRegVal=c?c.region:'';S.ociFormRegFilter=''}R();
|
||||
|
||||
Reference in New Issue
Block a user