78 lines
5.0 KiB
JavaScript
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}`)
|