38 lines
3.0 KiB
JavaScript
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}`)
|