mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-09-15 19:26:45 +00:00
160 lines
12 KiB
JavaScript
160 lines
12 KiB
JavaScript
const assert = require('node:assert/strict');
|
|||
|
|
const fs = require('node:fs');
|
||
|
|
const path = require('node:path');
|
||
|
|
const {createRequire} = require('node:module');
|
||
|
|
const app = path.resolve(__dirname, '../AppImage');
|
||
|
|
const appRequire = createRequire(path.join(app,'package.json'));
|
||
|
|
const ts = appRequire('typescript');
|
||
|
|
const cache = new Map();
|
||
|
|
function load(file) {
|
||
|
|
file = path.resolve(file);
|
||
|
|
if (cache.has(file)) return cache.get(file).exports;
|
||
|
|
const mod = {exports:{}}; cache.set(file,mod);
|
||
|
|
const js = ts.transpileModule(fs.readFileSync(file,'utf8'), {compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2020,jsx:ts.JsxEmit.ReactJSX}}).outputText;
|
||
|
|
new Function('require','module','exports',js)(name => {
|
||
|
|
if (!name.startsWith('.')) return appRequire(name);
|
||
|
|
const base=path.resolve(path.dirname(file),name);
|
||
|
|
return load(fs.existsSync(base+'.ts') ? base+'.ts' : base+'.tsx');
|
||
|
|
},mod,mod.exports);
|
||
|
|
return mod.exports;
|
||
|
|
}
|
||
|
|
function translate(locale) {
|
||
|
|
const messages=JSON.parse(fs.readFileSync(path.join(app,'messages',locale,'common.json')));
|
||
|
|
return (key,params={}) => {
|
||
|
|
const text=key.split('.').reduce((o,k)=>o?.[k],messages);
|
||
|
|
return typeof text==='string' ? text.replace(/\{(\w+)\}/g,(m,k)=>params[k] ?? m) : key;
|
||
|
|
};
|
||
|
|
}
|
||
|
|
global.window={location:{origin:'http://localhost'}};
|
||
|
|
const presentation=load(path.join(app,'lib/audit-presentation.ts'));
|
||
|
|
const {buildAuditDocument}=load(path.join(app,'lib/audit-document.ts'));
|
||
|
|
const {storageDiagram}=load(path.join(app,'lib/report-diagrams.ts'));
|
||
|
|
const base=(id,classification,affected=[])=>({check_id:id,area:id.split('.')[0],severity:'INFO',classification,summary_key:null,summary_params:{},affected,evidence:null});
|
||
|
|
const coverage=base('backup.guest_coverage','observation',[
|
||
|
|
...[109,111,112,114,9510].map(vmid=>({vmid,classification:'observation',reason_key:'noJobSelectsGuest'})),
|
||
|
|
...['sata0','sata1','sata2','sata3','scsi1'].map(volume=>({vmid:106,volume,reason_key:'dataExcludedFromBackup',classification:'observation'})),
|
||
|
|
{vmid:110,volume:'scsi1',reason_key:'dataExcludedFromBackup',classification:'observation'}]);
|
||
|
|
const lynis=base('security.lynis_warnings','observation',[...['enp3s0','tap106i0','tap105i0'].map(details=>({test:'NETW-3015',message:'Found promiscuous interface',details,solution:'Do not show this advice',classification:'observation'}))]);
|
||
|
|
const age=base('backup.last_backup_age','warning',[{vmid:101,storage:'PBS-Cloud',classification:'warning',reason_key:'olderThanSchedule'},{vmid:109,storage:'any',classification:'observation',reason_key:'noStoredBackupUnscheduled'}]);
|
||
|
|
age.evidence=JSON.stringify([{vmid:101,expected_storage:'PBS-Cloud',last_backup:1787763643,age_hours:259.1,max_age_hours:252}]);
|
||
|
|
for(const locale of ['en','es','de','fr','it','pt','sk','sv']) {
|
||
|
|
const t=translate(locale);
|
||
|
|
assert(!t('audit.checks.backup.last_backup_age.rationale').includes('ProxMenux'));
|
||
|
|
assert(!t('audit.presentation.limitReference').includes('ProxMenux'));
|
||
|
|
const groups=presentation.presentFinding(coverage,t,locale);
|
||
|
|
assert.equal(groups[0].rows.length,5); assert.equal(groups[1].rows.length,2);
|
||
|
|
assert(presentation.affectedDescription(coverage,t).includes('6'));
|
||
|
|
assert(!presentation.affectedDescription(coverage,t).includes('11'));
|
||
|
|
const lxcExcluded=base('backup.guest_coverage','observation',[{vmid:120,name:'container',type:'lxc',volume:'mp0',reason_key:'dataExcludedFromBackup',classification:'observation'}]);
|
||
|
|
assert.equal(presentation.presentFinding(lxcExcluded,t,locale)[0].rows[0].cells[0],'container · LXC 120');
|
||
|
|
const unavailable=base('backup.last_backup_age','unverified',[{vmid:120,storage:'offline',classification:'unverified',reason_key:'destinationUnavailable'}]);
|
||
|
|
const unavailableText=JSON.stringify(presentation.presentFinding(unavailable,t,locale));
|
||
|
|
assert(!unavailableText.includes(t('audit.presentation.notFound')));
|
||
|
|
assert(unavailableText.includes(t('audit.classifications.unverified')));
|
||
|
|
const text=JSON.stringify(presentation.presentFinding(lynis,t,locale));
|
||
|
|
assert(!text.includes('Do not show this advice'));
|
||
|
|
assert(text.includes('tap106i0'));
|
||
|
|
assert.equal(presentation.presentFinding(lynis,t,locale).length,1);
|
||
|
|
assert.equal(presentation.presentFinding(age,t,locale).length,2);
|
||
|
|
assert.notEqual(presentation.auditDuration(259.1,locale),presentation.auditDuration(252,locale));
|
||
|
|
for(const [policy,labelKey] of [['schedule and grace','limitSchedule'],['declared recovery objective','limitDeclared'],['fallback; no recovery objective declared and schedule not read','limitReference']]) {
|
||
|
|
const data={...age,affected:[age.affected[0]],evidence:JSON.stringify([{vmid:101,expected_storage:'PBS-Cloud',last_backup:1787763643,age_hours:259.1,max_age_hours:252,age_policy:policy}])};
|
||
|
|
const group=presentation.presentFinding(data,t,locale)[0];
|
||
|
|
assert(group.columns.includes(t('audit.presentation.backupAge')));
|
||
|
|
assert(group.columns.includes(t('audit.presentation.backupLimit')));
|
||
|
|
assert(!group.columns.includes(t('audit.presentation.ageLimit')));
|
||
|
|
assert(group.rows[0].cells.includes(presentation.auditDuration(259.1,locale)));
|
||
|
|
assert(group.rows[0].cells.some(cell=>cell.includes(t('audit.presentation.'+labelKey)) && cell.includes(presentation.auditDuration(252,locale))));
|
||
|
|
}
|
||
|
|
const legacyText=JSON.stringify(presentation.presentFinding(age,t,locale));
|
||
|
|
assert(!legacyText.includes(t('audit.presentation.limitSchedule')));
|
||
|
|
const implicitDestination={...age,affected:[{vmid:112,storage:'local',classification:'warning',reason_key:'olderThanFallback'}],evidence:JSON.stringify([{vmid:112,expected_storage:'any visible destination (no explicit target)',storage:'local',last_backup:1787763643,age_hours:800,max_age_hours:720,age_policy:'fallback; no recovery objective declared and schedule not read'}])};
|
||
|
|
const implicitText=JSON.stringify(presentation.presentFinding(implicitDestination,t,locale));
|
||
|
|
assert(implicitText.includes(presentation.auditDuration(720,locale)),
|
||
|
|
`${locale}: a backup without an explicit job destination lost its limit`);
|
||
|
|
const failedRuns=base('backup.job_results','warning',[
|
||
|
|
{vmid:106,status:'job errors',when:1787760000,upid:'UPID:first',classification:'warning',reason_key:'backupRunFailed'},
|
||
|
|
{vmid:106,status:'job errors',when:1787763600,upid:'UPID:last',classification:'warning',reason_key:'backupRunFailed'},
|
||
|
|
{vmid:110,status:'storage unavailable',when:1787767200,upid:'UPID:other',classification:'warning',reason_key:'backupRunFailed'},
|
||
|
|
]);
|
||
|
|
const failedGroups=presentation.presentFinding(failedRuns,t,locale);
|
||
|
|
assert.equal(failedGroups.length,1);
|
||
|
|
assert.equal(failedGroups[0].rows.length,2,
|
||
|
|
`${locale}: repeated backup failures were not grouped`);
|
||
|
|
assert.equal(failedGroups[0].rows.find(row=>row.cells[0].includes('106')).cells[1],'2');
|
||
|
|
assert(failedGroups[0].rows.some(row=>row.cells.includes('UPID:last')),
|
||
|
|
`${locale}: the latest backup task reference was not retained`);
|
||
|
|
const connected={...base('storage.connected_storage','conformant'),evidence:JSON.stringify({storages:[
|
||
|
|
{storage:'store-fixture',type:'pbs',status:'active',dependencies:[{vmid:101}],jobs:['backup-1'],capacity_known:true,used_percent:42.5},
|
||
|
|
],scope:'PVE-side observations only'})};
|
||
|
|
const connectedGroups=presentation.presentFinding(connected,t,locale);
|
||
|
|
assert.equal(connectedGroups.length,1);
|
||
|
|
assert.deepEqual(connectedGroups[0].columns,[t('audit.document.storage'),t('audit.document.type'),
|
||
|
|
t('audit.document.state'),t('audit.presentation.capacity'),t('audit.presentation.fact')]);
|
||
|
|
assert(connectedGroups[0].rows[0].cells.some(cell=>cell.includes('42')),
|
||
|
|
`${locale}: connected storage capacity was not presented`);
|
||
|
|
const thin={...base('storage.thin_pool_overprovisioning','warning',[
|
||
|
|
{pool:'pve/data',metric:'metadata',classification:'warning',reason_key:'thinMetadataPressure'},
|
||
|
|
]),evidence:JSON.stringify([{pool:'pve/data',allocated_bytes:214748364800,
|
||
|
|
pool_bytes:107374182400,allocation_percent:200,data_percent:81.2,metadata_percent:92.4}])};
|
||
|
|
const thinGroups=presentation.presentFinding(thin,t,locale);
|
||
|
|
assert.equal(thinGroups.length,1);
|
||
|
|
assert.deepEqual(thinGroups[0].columns,[t('audit.presentation.resource'),
|
||
|
|
t('audit.presentation.capacity'),t('audit.presentation.data'),
|
||
|
|
t('audit.presentation.metadata'),t('audit.presentation.fact')]);
|
||
|
|
assert(thinGroups[0].rows[0].cells[1].includes('GiB'),
|
||
|
|
`${locale}: thin-pool byte values were not made readable`);
|
||
|
|
const passing={...base('system.time_synchronisation','conformant'),evidence:'NTP: yes\nNTPSynchronized: yes'};
|
||
|
|
const input={profile:'full',run:null,findings:[coverage,age,lynis,passing,base('system.security_updates','unverified')],inventory:null,t,locale};
|
||
|
|
const html=buildAuditDocument(input);
|
||
|
|
const header=html.split('<div class="exec-box">')[1].split('<div class="audit-counters">')[0];
|
||
|
|
assert(header.includes('<strong>4/5</strong>'));
|
||
|
|
assert(header.includes('stroke-dasharray="80 100"'));
|
||
|
|
assert(header.includes(t('audit.presentation.verified')));
|
||
|
|
assert(!header.includes('health-ring'));
|
||
|
|
assert(!header.includes('health-lbl'));
|
||
|
|
assert(!header.includes('15 de 26'));
|
||
|
|
assert(header.includes('audit-result-heading'));
|
||
|
|
assert(header.includes('stroke="currentColor"'));
|
||
|
|
const checkedHtml=html.split('id="verified-checks"')[1].split('id="unverified-checks"')[0];
|
||
|
|
assert.equal((checkedHtml.match(/href="#finding-/g)||[]).length,4);
|
||
|
|
assert(checkedHtml.includes(t('audit.presentation.verifiedChecks')+' · 4'));
|
||
|
|
assert(checkedHtml.includes(t('audit.checks.system.time_synchronisation.title')));
|
||
|
|
assert(!checkedHtml.includes('href="#finding-system.security_updates"'));
|
||
|
|
assert(html.indexOf('id="verified-checks"') < html.indexOf(t('audit.presentation.overview')));
|
||
|
|
assert(!html.includes('audit.document.area'));
|
||
|
|
for(const [findings,expected] of [
|
||
|
|
[[base('x','critical'),base('y','observation'),base('z','not_applicable')],'2/2'],
|
||
|
|
[[{...base('x','warning'),incomplete:true,decision:'accepted'},base('y','conformant')],'1/2'],
|
||
|
|
[[{...base('x','unverified'),decision:'accepted'}],'0/1'],
|
||
|
|
[[base('x','not_applicable')],'—'],
|
||
|
|
[[],'—']]) {
|
||
|
|
const doc=buildAuditDocument({...input,findings});
|
||
|
|
assert(doc.includes(`<strong>${expected}</strong>`));
|
||
|
|
const verifiedSection=doc.split('id="verified-checks"')[1]?.split('</table>')[0] || '';
|
||
|
|
const expectedCount=expected==='—'?0:Number(expected.split('/')[0]);
|
||
|
|
assert.equal((verifiedSection.match(/href="#finding-/g)||[]).length,expectedCount);
|
||
|
|
if(expected==='—') assert(doc.includes('stroke-dasharray="0 100"'));
|
||
|
|
}
|
||
|
|
assert(!html.includes('health-icon" style="font-size:26px'));
|
||
|
|
assert(html.includes(t('audit.presentation.incomplete')));
|
||
|
|
assert(!html.includes('{count}'));
|
||
|
|
assert(!html.includes('audit.presentation.'));
|
||
|
|
const main=html.split('id="evidence-')[0];
|
||
|
|
assert(!main.includes('noJobSelectsGuest'));
|
||
|
|
assert(!main.includes('Do not show this advice'));
|
||
|
|
assert(html.includes(t('audit.presentation.evidenceObserved')),
|
||
|
|
`${locale}: conformant findings have no visible evidence`);
|
||
|
|
assert(!html.includes('id="evidence-system.time_synchronisation"'),
|
||
|
|
`${locale}: conformant raw evidence still bloats the appendix`);
|
||
|
|
const structuredPassing=buildAuditDocument({...input,findings:[connected]});
|
||
|
|
assert.equal((structuredPassing.match(/store-fixture/g)||[]).length,1,
|
||
|
|
`${locale}: structured conformant evidence was printed twice`);
|
||
|
|
const dangerous={...coverage,affected:[{vmid:109,name:'<img src=x onerror=alert(1)>',classification:'observation'}]};
|
||
|
|
assert(!buildAuditDocument({...input,findings:[dangerous]}).includes('<img src=x'));
|
||
|
|
}
|
||
|
|
const diagram=storageDiagram([{vmid:1,name:'one',disks:[{storage:'local'},{storage:'local'}],backups:[]}],{guests:'Guests',storage:'Storage',backup:'Jobs',unprotected:'No job'});
|
||
|
|
assert(!diagram.includes('>2</text>'));
|
||
|
|
console.log('Audit presentation: eight languages, truthful counts, calendar age, Lynis grouping, no advice, escaping and incomplete results passed.');
|
||
|
|
module.exports={load,translate,base,coverage,age,lynis,buildAuditDocument};
|