import fs from 'node:fs' import path from 'node:path' const auth=JSON.parse(fs.readFileSync('contracts/authorization/kbx.authorization.json','utf8')) const fields=JSON.parse(fs.readFileSync('contracts/fields/kbx.fields.json','utf8')).fields const api=JSON.parse(fs.readFileSync('contracts/api/kbx.api.json','utf8')).operations const ids=new Set(); const errors=[] for(const p of auth.permissions){ if(ids.has(p.id))errors.push(`duplicate permission: ${p.id}`); ids.add(p.id) if(!/^[a-z][a-z0-9-]*(\.[a-z0-9-]+)+$/.test(p.id))errors.push(`invalid permission id: ${p.id}`) if(!['low','medium','high'].includes(p.risk))errors.push(`invalid risk: ${p.id}`) } for(const op of api)if(op.permission && !ids.has(op.permission))errors.push(`API permission missing in catalog: ${op.id} -> ${op.permission}`) const scanRoots=['apps/web/src','backend/Modules','packages/kbx-ui/src'] function walk(dir){if(!fs.existsSync(dir))return[];return fs.readdirSync(dir,{withFileTypes:true}).flatMap(e=>e.isDirectory()?walk(path.join(dir,e.name)):[path.join(dir,e.name)])} const used=new Set() for(const file of scanRoots.flatMap(walk)){ if(!/\.(ts|vue|cs)$/.test(file))continue const text=fs.readFileSync(file,'utf8') for(const m of text.matchAll(/permission\s*:\s*['"]([a-z][a-z0-9.-]+)['"]/g))used.add(m[1]) for(const m of text.matchAll(/permissions\s*:\s*\[([^\]]*)\]/gi))for(const x of m[1].matchAll(/['"]([a-z][a-z0-9.-]+)['"]/g))used.add(x[1]) for(const m of text.matchAll(/Permissions\(\s*['"]([a-z][a-z0-9.-]+)['"]\s*\)/g))used.add(m[1]) } for(const p of used)if(!ids.has(p))errors.push(`used permission missing in catalog: ${p}`) const fieldMap=new Map(fields.map(f=>[f.key,f])) const covered=new Set() for(const policy of auth.sensitiveDataPolicies){ for(const perm of [policy.viewPermission,policy.revealPermission,policy.unmaskedExportPermission])if(!ids.has(perm))errors.push(`sensitive policy permission missing: ${policy.id} -> ${perm}`) for(const key of policy.fields){const f=fieldMap.get(key);if(!f)errors.push(`sensitive policy field missing: ${key}`);else if(!f.sensitive)errors.push(`policy field is not sensitive: ${key}`);covered.add(key)} if(policy.aiExposure!=='masked-only'&&policy.aiExposure!=='deny')errors.push(`unsafe aiExposure: ${policy.id}`) if(policy.telemetryExposure!=='never')errors.push(`sensitive telemetry must be never: ${policy.id}`) } for(const f of fields.filter(x=>x.sensitive))if(!covered.has(f.key))errors.push(`sensitive field has no policy: ${f.key}`) if(!ids.has(auth.ai.usePermission)||!ids.has(auth.ai.executePermission))errors.push('AI authorization permissions are not cataloged') const source=fs.readFileSync('apps/web/src/utility/useKbxScreenUtility.ts','utf8') if(source.includes("allowedCapabilities: ['explain', 'suggest', 'draft']"))errors.push('AI capabilities are still hard-coded; use authorization policy helper') if(errors.length){console.error(errors.join('\n'));process.exit(1)} console.log(`authorization governance PASS: permissions=${ids.size}, used=${used.size}, sensitive=${covered.size}`)