Files
KArtSell.Aegis/docs/Design/kbx-foundation-v36/scripts/validate-authorization-governance.mjs
T

38 lines
3.0 KiB
JavaScript

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}`)