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:
nogueiraguh
2026-03-05 11:11:31 -03:00
parent 5038403d08
commit 973ff65989
5 changed files with 303 additions and 1435 deletions

View File

@@ -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();