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

78 lines
5.0 KiB
JavaScript

import fs from 'node:fs'
import path from 'node:path'
const read=p=>JSON.parse(fs.readFileSync(p,'utf8'))
const src=read('contracts/configuration/kbx.configuration.json')
const providers=read('contracts/providers/kbx.providers.json')
const manifest=read('generated/configuration-manifest.json')
const errors=[]
const keys=new Set(),envs=new Set()
for(const s of src.settings??[]){
if(keys.has(s.key))errors.push(`duplicate configuration key ${s.key}`);keys.add(s.key)
if(envs.has(s.env))errors.push(`duplicate environment variable ${s.env}`);envs.add(s.env)
if(s.secret && s.default!==undefined)errors.push(`secret setting must not define a default: ${s.key}`)
if(s.secret && !(s.allowedSources??[]).every(x=>['environment','secret-store','user-secrets'].includes(x)))errors.push(`secret setting has unsafe source: ${s.key}`)
if(s.type==='enum' && !(s.allowedValues?.length))errors.push(`enum setting missing allowedValues: ${s.key}`)
if((s.minimum!==undefined||s.maximum!==undefined)&&s.type!=='integer')errors.push(`numeric bounds only supported for integer in v22: ${s.key}`)
}
for(const e of src.environments??[]){
if(!['Development','Test','Staging','Production'].includes(e.id))errors.push(`unsupported environment profile ${e.id}`)
}
const production=src.environments.find(x=>x.id==='Production')
if(!production||production.migrationStrategy!=='predeploy')errors.push('Production must use predeploy migration strategy')
if(!production?.requireHttps)errors.push('Production must require HTTPS')
if(production?.providerEndpointOverrideAllowed)errors.push('Production provider endpoint overrides must be forbidden')
const test=src.environments.find(x=>x.id==='Test')
if(!test||test.providerNetwork!=='forbidden')errors.push('Test environment must forbid live provider network access')
const providerCredentialKeys=[]
for(const p of providers.providers??[]){
const policy=p.kbxPolicy??{}
for(const prop of ['credentialKey','appKeyCredential','appSecretCredential','environmentKey'])if(policy[prop])providerCredentialKeys.push(policy[prop])
}
for(const key of providerCredentialKeys)if(!keys.has(key))errors.push(`provider configuration key missing from configuration catalog: ${key}`)
if(manifest.settingCount!==src.settings.length||manifest.environmentCount!==src.environments.length)errors.push('generated configuration manifest count drift')
const ts=fs.readFileSync('packages/kbx-contracts/src/generated/configurationCatalog.ts','utf8')
const cs=fs.readFileSync('backend/Shared/Configuration/Generated/KbxConfigurationCatalog.g.cs','utf8')
if(!ts.includes(manifest.sourceSha256)||!cs.includes(manifest.sourceSha256))errors.push('configuration generated source SHA parity missing')
const envTemplate=fs.readFileSync('deploy/kbx/.env.kbx.example','utf8')
for(const s of src.settings){
if(!envTemplate.includes(`${s.env}=`))errors.push(`env template missing ${s.env}`)
if(s.secret){
const line=envTemplate.split(/\r?\n/).find(x=>x.startsWith(`${s.env}=`))??''
const raw=line.slice(s.env.length+1).split(' # ')[0]
if(raw.trim())errors.push(`generated env template contains a secret value: ${s.env}`)
}
}
function walk(dir){
const out=[]
if(!fs.existsSync(dir))return out
for(const e of fs.readdirSync(dir,{withFileTypes:true})){
const p=path.join(dir,e.name)
if(e.isDirectory())out.push(...walk(p));else out.push(p)
}
return out
}
// Direct environment/config access outside the configuration boundary becomes hidden configuration debt.
for(const file of walk('backend').filter(x=>x.endsWith('.cs')&&!x.includes('/Shared/Configuration/'))){
const text=fs.readFileSync(file,'utf8')
if(/Environment\.GetEnvironmentVariable\s*\(/.test(text))errors.push(`direct environment read outside configuration boundary: ${file}`)
if(/IConfiguration\s*\[/.test(text))errors.push(`direct IConfiguration index usage outside configuration boundary: ${file}`)
}
// Heuristic committed-secret guard for known provider/alerting key names.
for(const file of walk('.').filter(x=>/\.(json|ya?ml|md|env|txt)$/i.test(x)&&!x.includes('/.git/')&&!x.includes('governance/baselines/'))){
if(file==='contracts/configuration/kbx.configuration.json'||file==='contracts/providers/kbx.providers.json'||file==='deploy/kbx/.env.kbx.example')continue
const text=fs.readFileSync(file,'utf8')
if(/(?:AUTH_KEY|crtfc_key|AppSecret|BotToken)\s*[=:]\s*["']?[A-Za-z0-9_\-]{16,}/i.test(text))errors.push(`possible committed secret material: ${file}`)
}
for(const f of ['backend/Shared/Configuration/KbxConfigurationStartupValidator.cs','backend/Shared/Configuration/KbxConfigurationFingerprint.cs','deploy/kbx/environment-matrix.generated.json','docs/configuration-reference.generated.md'])if(!fs.existsSync(f))errors.push(`missing v22 configuration artifact ${f}`)
if(errors.length){console.error('configuration governance FAIL');for(const e of errors)console.error(`- ${e}`);process.exit(1)}
console.log(`configuration governance PASS: settings=${src.settings.length}, environments=${src.environments.length}, provider credentials covered=${providerCredentialKeys.length}`)