@soulbatical/tetra-dev-toolkit 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +312 -0
- package/bin/vca-audit.js +90 -0
- package/bin/vca-dev-token.js +39 -0
- package/bin/vca-setup.js +227 -0
- package/lib/checks/codeQuality/api-response-format.js +268 -0
- package/lib/checks/health/claude-md.js +114 -0
- package/lib/checks/health/doppler-compliance.js +174 -0
- package/lib/checks/health/git.js +61 -0
- package/lib/checks/health/gitignore.js +83 -0
- package/lib/checks/health/index.js +26 -0
- package/lib/checks/health/infrastructure-yml.js +87 -0
- package/lib/checks/health/mcps.js +57 -0
- package/lib/checks/health/naming-conventions.js +302 -0
- package/lib/checks/health/plugins.js +38 -0
- package/lib/checks/health/quality-toolkit.js +97 -0
- package/lib/checks/health/repo-visibility.js +70 -0
- package/lib/checks/health/rls-audit.js +130 -0
- package/lib/checks/health/scanner.js +68 -0
- package/lib/checks/health/secrets.js +80 -0
- package/lib/checks/health/stella-integration.js +124 -0
- package/lib/checks/health/tests.js +140 -0
- package/lib/checks/health/types.js +77 -0
- package/lib/checks/health/vincifox-widget.js +47 -0
- package/lib/checks/index.js +17 -0
- package/lib/checks/security/deprecated-supabase-admin.js +96 -0
- package/lib/checks/security/gitignore-validation.js +211 -0
- package/lib/checks/security/hardcoded-secrets.js +95 -0
- package/lib/checks/security/service-key-exposure.js +107 -0
- package/lib/checks/security/systemdb-whitelist.js +138 -0
- package/lib/checks/stability/ci-pipeline.js +143 -0
- package/lib/checks/stability/husky-hooks.js +117 -0
- package/lib/checks/stability/npm-audit.js +140 -0
- package/lib/checks/supabase/rls-policy-audit.js +261 -0
- package/lib/commands/dev-token.js +342 -0
- package/lib/config.js +213 -0
- package/lib/index.js +17 -0
- package/lib/reporters/terminal.js +134 -0
- package/lib/runner.js +179 -0
- package/package.json +72 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check: @vca/dev-toolkit Installation
|
|
3
|
+
*
|
|
4
|
+
* Checks if the quality toolkit is installed and CLI commands available.
|
|
5
|
+
* Score: 0 = not installed, 1 = installed, 2 = all commands available
|
|
6
|
+
*
|
|
7
|
+
* @param {Function} [getCachedCodeQuality] - Optional callback to get cached audit results
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync } from 'fs'
|
|
11
|
+
import { join } from 'path'
|
|
12
|
+
import { createCheck } from './types.js'
|
|
13
|
+
|
|
14
|
+
export async function check(projectPath, { getCachedCodeQuality } = {}) {
|
|
15
|
+
const result = createCheck('quality-toolkit', 2, {
|
|
16
|
+
installed: false,
|
|
17
|
+
version: null,
|
|
18
|
+
commands: { 'vca-audit': false, 'vca-setup': false, 'vca-dev-token': false }
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const packageJsonPath = join(projectPath, 'package.json')
|
|
22
|
+
if (!existsSync(packageJsonPath)) {
|
|
23
|
+
result.status = 'warning'
|
|
24
|
+
result.details.message = 'No package.json found'
|
|
25
|
+
return result
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
|
|
30
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
31
|
+
const toolkitDep = allDeps['@vca/dev-toolkit'] || allDeps['@vca/quality-toolkit']
|
|
32
|
+
|
|
33
|
+
if (!toolkitDep) {
|
|
34
|
+
result.status = 'warning'
|
|
35
|
+
result.details.message = 'Not installed'
|
|
36
|
+
result.details.installCommand = 'npm install --save-dev /Users/albertbarth/projecten/vca-quality-toolkit'
|
|
37
|
+
return result
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
result.details.installed = true
|
|
41
|
+
result.details.dependencyVersion = toolkitDep
|
|
42
|
+
result.score = 1
|
|
43
|
+
|
|
44
|
+
// Get installed version from node_modules
|
|
45
|
+
for (const pkgName of ['dev-toolkit', 'quality-toolkit']) {
|
|
46
|
+
const toolkitPackagePath = join(projectPath, 'node_modules', '@vca', pkgName, 'package.json')
|
|
47
|
+
if (existsSync(toolkitPackagePath)) {
|
|
48
|
+
try {
|
|
49
|
+
result.details.version = JSON.parse(readFileSync(toolkitPackagePath, 'utf-8')).version
|
|
50
|
+
} catch {
|
|
51
|
+
result.details.version = 'unknown'
|
|
52
|
+
}
|
|
53
|
+
break
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Check CLI commands
|
|
58
|
+
const binPath = join(projectPath, 'node_modules', '.bin')
|
|
59
|
+
const commands = ['vca-audit', 'vca-setup', 'vca-dev-token']
|
|
60
|
+
for (const cmd of commands) {
|
|
61
|
+
result.details.commands[cmd] = existsSync(join(binPath, cmd))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (commands.every(cmd => result.details.commands[cmd])) {
|
|
65
|
+
result.score = 2
|
|
66
|
+
} else {
|
|
67
|
+
result.status = 'warning'
|
|
68
|
+
result.details.message = 'Some CLI commands missing - try npm install'
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
result.status = 'error'
|
|
72
|
+
result.details.error = 'Failed to check package.json'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Merge cached audit results (informational, doesn't affect score)
|
|
76
|
+
if (getCachedCodeQuality) {
|
|
77
|
+
const projectName = projectPath.split('/').pop() || ''
|
|
78
|
+
const auditResult = getCachedCodeQuality(projectName)
|
|
79
|
+
if (auditResult) {
|
|
80
|
+
result.details.audit = {
|
|
81
|
+
passed: auditResult.passed,
|
|
82
|
+
summary: auditResult.summary,
|
|
83
|
+
apiCompliance: auditResult.apiCompliance,
|
|
84
|
+
suites: {
|
|
85
|
+
security: auditResult.suites.security?.passed ?? null,
|
|
86
|
+
stability: auditResult.suites.stability?.passed ?? null,
|
|
87
|
+
codeQuality: auditResult.suites.codeQuality?.passed ?? null
|
|
88
|
+
},
|
|
89
|
+
scannedAt: auditResult.scannedAt
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
result.details.audit = null
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check: Repository Visibility
|
|
3
|
+
*
|
|
4
|
+
* Checks if GitHub repo is public (security risk) or private.
|
|
5
|
+
* Score: 2 = private, 0 = public (critical)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from 'fs'
|
|
9
|
+
import { execSync } from 'child_process'
|
|
10
|
+
import { join } from 'path'
|
|
11
|
+
import { createCheck } from './types.js'
|
|
12
|
+
|
|
13
|
+
export async function check(projectPath) {
|
|
14
|
+
const result = createCheck('repo-visibility', 2, {
|
|
15
|
+
visibility: 'unknown', remoteUrl: null, owner: null, repo: null
|
|
16
|
+
})
|
|
17
|
+
result.score = 2
|
|
18
|
+
|
|
19
|
+
if (!existsSync(join(projectPath, '.git'))) {
|
|
20
|
+
result.status = 'warning'; result.score = 0
|
|
21
|
+
result.details.visibility = 'no-repo'; result.details.message = 'Not a git repository'
|
|
22
|
+
return result
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let remoteUrl
|
|
26
|
+
try {
|
|
27
|
+
remoteUrl = execSync('git remote get-url origin', { cwd: projectPath, encoding: 'utf-8', timeout: 5000 }).trim()
|
|
28
|
+
} catch {
|
|
29
|
+
result.details.visibility = 'no-remote'; result.details.message = 'No remote origin configured'
|
|
30
|
+
return result
|
|
31
|
+
}
|
|
32
|
+
result.details.remoteUrl = remoteUrl
|
|
33
|
+
|
|
34
|
+
const match = remoteUrl.match(/github\.com[\/:]+([^/]+)\/([^/.]+)/)
|
|
35
|
+
if (!match) {
|
|
36
|
+
result.details.visibility = 'non-github'; result.details.message = 'Not a GitHub repo (visibility check skipped)'
|
|
37
|
+
return result
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const [, owner, repo] = match
|
|
41
|
+
result.details.owner = owner
|
|
42
|
+
result.details.repo = repo
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
|
|
46
|
+
headers: { 'User-Agent': 'vca-health-check' },
|
|
47
|
+
signal: AbortSignal.timeout(5000)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
if (response.status === 200) {
|
|
51
|
+
const data = await response.json()
|
|
52
|
+
result.details.visibility = data.private ? 'private' : 'public'
|
|
53
|
+
if (!data.private) {
|
|
54
|
+
result.status = 'error'; result.score = 0
|
|
55
|
+
result.details.message = 'REPOSITORY IS PUBLIC — anyone can see your code!'
|
|
56
|
+
result.details.htmlUrl = data.html_url
|
|
57
|
+
}
|
|
58
|
+
} else if (response.status === 404) {
|
|
59
|
+
result.details.visibility = 'private' // 404 = private or doesn't exist
|
|
60
|
+
} else {
|
|
61
|
+
result.details.visibility = 'unknown'; result.score = 1; result.status = 'warning'
|
|
62
|
+
result.details.message = `GitHub API returned ${response.status}`
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
result.details.visibility = 'unknown'; result.score = 1; result.status = 'warning'
|
|
66
|
+
result.details.message = 'Could not reach GitHub API'
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return result
|
|
70
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check: RLS Policies in SQL Migrations
|
|
3
|
+
*
|
|
4
|
+
* Builds table state from migrations, checks RLS enabled + policies exist.
|
|
5
|
+
* Score: 3 (full) with deductions for missing RLS, missing policies, permissive policies
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
9
|
+
import { join } from 'path'
|
|
10
|
+
import { createCheck } from './types.js'
|
|
11
|
+
|
|
12
|
+
export async function check(projectPath) {
|
|
13
|
+
const result = createCheck('rls-audit', 3, {
|
|
14
|
+
tablesFound: 0,
|
|
15
|
+
tablesWithRls: 0,
|
|
16
|
+
tablesWithoutRls: [],
|
|
17
|
+
tablesWithoutPolicies: [],
|
|
18
|
+
permissivePolicies: [],
|
|
19
|
+
securityDefinerFunctions: []
|
|
20
|
+
})
|
|
21
|
+
result.score = 3 // Start full, deduct for issues
|
|
22
|
+
|
|
23
|
+
const migrationDirs = [
|
|
24
|
+
join(projectPath, 'supabase', 'migrations'),
|
|
25
|
+
join(projectPath, 'backend', 'supabase', 'migrations')
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const sqlFiles = []
|
|
29
|
+
for (const dir of migrationDirs) {
|
|
30
|
+
if (!existsSync(dir)) continue
|
|
31
|
+
try {
|
|
32
|
+
for (const f of readdirSync(dir).filter(f => f.endsWith('.sql')).sort()) {
|
|
33
|
+
sqlFiles.push(join(dir, f))
|
|
34
|
+
}
|
|
35
|
+
} catch { /* ignore */ }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (sqlFiles.length === 0) {
|
|
39
|
+
result.details.message = 'No SQL migrations found'
|
|
40
|
+
return result
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Build table state from migrations
|
|
44
|
+
const tables = new Map()
|
|
45
|
+
|
|
46
|
+
for (const filePath of sqlFiles) {
|
|
47
|
+
let content
|
|
48
|
+
try { content = readFileSync(filePath, 'utf-8') } catch { continue }
|
|
49
|
+
const fileName = filePath.split('/').pop() || ''
|
|
50
|
+
let m
|
|
51
|
+
|
|
52
|
+
// CREATE TABLE
|
|
53
|
+
const createRe = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:public\.)?["']?(\w+)["']?/gi
|
|
54
|
+
while ((m = createRe.exec(content)) !== null) {
|
|
55
|
+
const name = m[1].toLowerCase()
|
|
56
|
+
if (!tables.has(name)) tables.set(name, { rlsEnabled: false, policies: [], file: fileName })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// DROP TABLE
|
|
60
|
+
const dropRe = /DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:public\.)?["']?(\w+)["']?/gi
|
|
61
|
+
while ((m = dropRe.exec(content)) !== null) tables.delete(m[1].toLowerCase())
|
|
62
|
+
|
|
63
|
+
// RENAME TABLE
|
|
64
|
+
const renameRe = /ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:public\.)?["']?(\w+)["']?\s+RENAME\s+TO\s+["']?(\w+)["']?/gi
|
|
65
|
+
while ((m = renameRe.exec(content)) !== null) {
|
|
66
|
+
const data = tables.get(m[1].toLowerCase())
|
|
67
|
+
if (data) { tables.delete(m[1].toLowerCase()); tables.set(m[2].toLowerCase(), data) }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ENABLE/DISABLE RLS
|
|
71
|
+
const enableRe = /ALTER\s+TABLE\s+(?:public\.)?["']?(\w+)["']?\s+ENABLE\s+ROW\s+LEVEL\s+SECURITY/gi
|
|
72
|
+
while ((m = enableRe.exec(content)) !== null) {
|
|
73
|
+
const name = m[1].toLowerCase()
|
|
74
|
+
if (tables.has(name)) tables.get(name).rlsEnabled = true
|
|
75
|
+
else tables.set(name, { rlsEnabled: true, policies: [], file: fileName })
|
|
76
|
+
}
|
|
77
|
+
const disableRe = /ALTER\s+TABLE\s+(?:public\.)?["']?(\w+)["']?\s+DISABLE\s+ROW\s+LEVEL\s+SECURITY/gi
|
|
78
|
+
while ((m = disableRe.exec(content)) !== null) {
|
|
79
|
+
if (tables.has(m[1].toLowerCase())) tables.get(m[1].toLowerCase()).rlsEnabled = false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// CREATE POLICY
|
|
83
|
+
const policyRe = /CREATE\s+POLICY\s+"([^"]+)"\s+ON\s+(?:public\.)?["']?(\w+)["']?\s+([\s\S]*?)(?:;|CREATE\s|ALTER\s|DROP\s|GRANT\s)/gi
|
|
84
|
+
while ((m = policyRe.exec(content)) !== null) {
|
|
85
|
+
const tableName = m[2].toLowerCase()
|
|
86
|
+
if (!tables.has(tableName)) tables.set(tableName, { rlsEnabled: false, policies: [], file: fileName })
|
|
87
|
+
tables.get(tableName).policies.push(m[1])
|
|
88
|
+
if (/USING\s*\(\s*true\s*\)/i.test(m[3]) || /WITH\s+CHECK\s*\(\s*true\s*\)/i.test(m[3])) {
|
|
89
|
+
result.details.permissivePolicies.push(`${m[1]} on ${tableName}`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// DROP POLICY
|
|
94
|
+
const dropPolicyRe = /DROP\s+POLICY\s+(?:IF\s+EXISTS\s+)?"([^"]+)"\s+ON\s+(?:public\.)?["']?(\w+)["']?/gi
|
|
95
|
+
while ((m = dropPolicyRe.exec(content)) !== null) {
|
|
96
|
+
const t = tables.get(m[2].toLowerCase())
|
|
97
|
+
if (t) t.policies = t.policies.filter(p => p !== m[1])
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// SECURITY DEFINER
|
|
101
|
+
const secDefRe = /CREATE\s+(?:OR\s+REPLACE\s+)?FUNCTION\s+(?:public\.)?["']?(\w+)["']?[\s\S]*?SECURITY\s+DEFINER/gi
|
|
102
|
+
while ((m = secDefRe.exec(content)) !== null) {
|
|
103
|
+
result.details.securityDefinerFunctions.push(m[1])
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Analyze
|
|
108
|
+
result.details.tablesFound = tables.size
|
|
109
|
+
let tablesWithRls = 0
|
|
110
|
+
|
|
111
|
+
for (const [name, info] of tables) {
|
|
112
|
+
if (name.startsWith('_') || name.startsWith('pg_') || name.startsWith('auth_')) continue
|
|
113
|
+
if (!info.rlsEnabled) {
|
|
114
|
+
result.details.tablesWithoutRls.push(name)
|
|
115
|
+
} else {
|
|
116
|
+
tablesWithRls++
|
|
117
|
+
if (info.policies.length === 0) result.details.tablesWithoutPolicies.push(name)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
result.details.tablesWithRls = tablesWithRls
|
|
122
|
+
|
|
123
|
+
if (result.details.tablesWithoutRls.length > 0) { result.score -= 1.5; result.status = 'error'; result.details.message = `${result.details.tablesWithoutRls.length} table(s) without RLS` }
|
|
124
|
+
if (result.details.tablesWithoutPolicies.length > 0) { result.score -= 0.5; if (result.status === 'ok') result.status = 'warning' }
|
|
125
|
+
if (result.details.permissivePolicies.length > 0) { result.score -= 0.5; if (result.status === 'ok') result.status = 'warning' }
|
|
126
|
+
if (result.details.securityDefinerFunctions.length > 0) { result.score -= 0.5; if (result.status === 'ok') result.status = 'warning' }
|
|
127
|
+
|
|
128
|
+
result.score = Math.max(0, result.score)
|
|
129
|
+
return result
|
|
130
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Health Scanner
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates all 15 health checks and produces a HealthReport.
|
|
5
|
+
* This is the main entry point — consumers call scanProjectHealth().
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { check as checkPlugins } from './plugins.js'
|
|
9
|
+
import { check as checkMcps } from './mcps.js'
|
|
10
|
+
import { check as checkGit } from './git.js'
|
|
11
|
+
import { check as checkTests } from './tests.js'
|
|
12
|
+
import { check as checkSecrets } from './secrets.js'
|
|
13
|
+
import { check as checkQualityToolkit } from './quality-toolkit.js'
|
|
14
|
+
import { check as checkNamingConventions } from './naming-conventions.js'
|
|
15
|
+
import { check as checkRlsPolicies } from './rls-audit.js'
|
|
16
|
+
import { check as checkGitignore } from './gitignore.js'
|
|
17
|
+
import { check as checkRepoVisibility } from './repo-visibility.js'
|
|
18
|
+
import { check as checkVinciFoxWidget } from './vincifox-widget.js'
|
|
19
|
+
import { check as checkStellaIntegration } from './stella-integration.js'
|
|
20
|
+
import { check as checkClaudeMd } from './claude-md.js'
|
|
21
|
+
import { check as checkDopplerCompliance } from './doppler-compliance.js'
|
|
22
|
+
import { check as checkInfrastructureYml } from './infrastructure-yml.js'
|
|
23
|
+
import { calculateHealthStatus } from './types.js'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run full health check on a project
|
|
27
|
+
*
|
|
28
|
+
* @param {string} projectPath - Absolute path to project root
|
|
29
|
+
* @param {string} projectName - Display name for the project
|
|
30
|
+
* @param {Object} [options]
|
|
31
|
+
* @param {Function} [options.getCachedCodeQuality] - Callback for cached audit results (ralph-manager specific)
|
|
32
|
+
* @returns {Promise<import('./types.js').HealthReport>}
|
|
33
|
+
*/
|
|
34
|
+
export async function scanProjectHealth(projectPath, projectName, options = {}) {
|
|
35
|
+
// Run all checks in parallel
|
|
36
|
+
const checks = await Promise.all([
|
|
37
|
+
checkPlugins(projectPath),
|
|
38
|
+
checkMcps(projectPath),
|
|
39
|
+
checkGit(projectPath),
|
|
40
|
+
checkTests(projectPath),
|
|
41
|
+
checkSecrets(projectPath),
|
|
42
|
+
checkQualityToolkit(projectPath, { getCachedCodeQuality: options.getCachedCodeQuality }),
|
|
43
|
+
checkNamingConventions(projectPath),
|
|
44
|
+
checkRlsPolicies(projectPath),
|
|
45
|
+
checkGitignore(projectPath),
|
|
46
|
+
checkRepoVisibility(projectPath),
|
|
47
|
+
checkVinciFoxWidget(projectPath),
|
|
48
|
+
checkStellaIntegration(projectPath),
|
|
49
|
+
checkClaudeMd(projectPath),
|
|
50
|
+
checkDopplerCompliance(projectPath),
|
|
51
|
+
checkInfrastructureYml(projectPath)
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
const totalScore = checks.reduce((sum, c) => sum + c.score, 0)
|
|
55
|
+
const maxScore = checks.reduce((sum, c) => sum + c.maxScore, 0)
|
|
56
|
+
const healthPercent = maxScore > 0 ? Math.round((totalScore / maxScore) * 100) : 0
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
projectName,
|
|
60
|
+
projectPath,
|
|
61
|
+
overallScore: totalScore,
|
|
62
|
+
maxScore,
|
|
63
|
+
healthPercent,
|
|
64
|
+
status: calculateHealthStatus(checks),
|
|
65
|
+
checks,
|
|
66
|
+
scannedAt: new Date().toISOString()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check: Exposed Secrets in Markdown/Config Files
|
|
3
|
+
*
|
|
4
|
+
* Scans .ralph/, README.md, and specs for hardcoded secrets.
|
|
5
|
+
* Score: 2 (full) if no secrets, 0 if any found
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
9
|
+
import { join } from 'path'
|
|
10
|
+
import { createCheck, maskSecret } from './types.js'
|
|
11
|
+
|
|
12
|
+
const SECRET_PATTERNS = [
|
|
13
|
+
{ name: 'API Key', pattern: /(?:api[_-]?key|apikey)\s*[=:]\s*["']?([a-zA-Z0-9_\-]{20,})["']?/gi },
|
|
14
|
+
{ name: 'Secret Key', pattern: /(?:secret[_-]?key|secretkey)\s*[=:]\s*["']?([a-zA-Z0-9_\-]{20,})["']?/gi },
|
|
15
|
+
{ name: 'Access Token', pattern: /(?:access[_-]?token|accesstoken)\s*[=:]\s*["']?([a-zA-Z0-9_\-]{20,})["']?/gi },
|
|
16
|
+
{ name: 'AWS Key', pattern: /AKIA[A-Z0-9]{16}/g },
|
|
17
|
+
{ name: 'GitHub Token', pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/g },
|
|
18
|
+
{ name: 'Supabase JWT', pattern: /eyJ[A-Za-z0-9_-]{50,}\.[A-Za-z0-9_-]{50,}\.[A-Za-z0-9_-]{50,}/g },
|
|
19
|
+
{ name: 'Bearer Token', pattern: /Bearer\s+[A-Za-z0-9_\-\.]{20,}/gi },
|
|
20
|
+
{ name: 'Password', pattern: /(?:password|passwd|pwd)\s*[=:]\s*["']([^"'\s]{8,})["']/gi }
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
export async function check(projectPath) {
|
|
24
|
+
const result = createCheck('secrets', 2, { exposedSecrets: [], scannedFiles: 0 })
|
|
25
|
+
result.score = 2 // Full score unless we find something
|
|
26
|
+
|
|
27
|
+
const filesToScan = [
|
|
28
|
+
'.ralph/PROMPT.md',
|
|
29
|
+
'.ralph/@fix_plan.md',
|
|
30
|
+
'README.md',
|
|
31
|
+
'docs/README.md'
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
// Also scan .ralph/specs/
|
|
35
|
+
const specsPath = join(projectPath, '.ralph', 'specs')
|
|
36
|
+
if (existsSync(specsPath)) {
|
|
37
|
+
try {
|
|
38
|
+
for (const spec of readdirSync(specsPath)) {
|
|
39
|
+
if (spec.endsWith('.md')) filesToScan.push(`.ralph/specs/${spec}`)
|
|
40
|
+
}
|
|
41
|
+
} catch { /* ignore */ }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const exposedSecrets = []
|
|
45
|
+
|
|
46
|
+
for (const file of filesToScan) {
|
|
47
|
+
const filePath = join(projectPath, file)
|
|
48
|
+
if (!existsSync(filePath)) continue
|
|
49
|
+
result.details.scannedFiles++
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const content = readFileSync(filePath, 'utf-8')
|
|
53
|
+
const lines = content.split('\n')
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < lines.length; i++) {
|
|
56
|
+
for (const { name, pattern } of SECRET_PATTERNS) {
|
|
57
|
+
pattern.lastIndex = 0
|
|
58
|
+
for (const match of lines[i].matchAll(pattern)) {
|
|
59
|
+
const value = match[1] || match[0]
|
|
60
|
+
const masked = maskSecret(value)
|
|
61
|
+
const exists = exposedSecrets.some(s => s.file === file && s.line === i + 1 && s.masked === masked)
|
|
62
|
+
if (!exists) {
|
|
63
|
+
exposedSecrets.push({ type: name, file, line: i + 1, masked })
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch { /* ignore */ }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
result.details.exposedSecrets = exposedSecrets
|
|
72
|
+
|
|
73
|
+
if (exposedSecrets.length > 0) {
|
|
74
|
+
result.status = 'error'
|
|
75
|
+
result.score = 0
|
|
76
|
+
result.details.message = `Found ${exposedSecrets.length} potential exposed secrets`
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return result
|
|
80
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check: @ralph/stella Integration
|
|
3
|
+
*
|
|
4
|
+
* Checks for @ralph/stella dependency and integration level in MCP server.
|
|
5
|
+
* Score: 0 = not installed, 1 = basic, 2 = full integration
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync } from 'fs'
|
|
9
|
+
import { join } from 'path'
|
|
10
|
+
import { createCheck } from './types.js'
|
|
11
|
+
|
|
12
|
+
export async function check(projectPath) {
|
|
13
|
+
const result = createCheck('stella-integration', 2, {
|
|
14
|
+
installed: false, version: null, integrationLevel: 'none', features: [], packageLocation: null
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
// Search for @ralph/stella in package.json files
|
|
18
|
+
const packageLocations = [
|
|
19
|
+
{ path: join(projectPath, 'package.json'), label: 'root' },
|
|
20
|
+
{ path: join(projectPath, 'mcp', 'package.json'), label: 'mcp/' },
|
|
21
|
+
{ path: join(projectPath, 'backend-mcp', 'package.json'), label: 'backend-mcp/' },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
let stellaDep = null
|
|
25
|
+
let stellaLocation = null
|
|
26
|
+
|
|
27
|
+
for (const loc of packageLocations) {
|
|
28
|
+
if (!existsSync(loc.path)) continue
|
|
29
|
+
try {
|
|
30
|
+
const pkg = JSON.parse(readFileSync(loc.path, 'utf-8'))
|
|
31
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
|
|
32
|
+
if (allDeps['@ralph/stella']) {
|
|
33
|
+
stellaDep = allDeps['@ralph/stella']
|
|
34
|
+
stellaLocation = loc.label
|
|
35
|
+
break
|
|
36
|
+
}
|
|
37
|
+
} catch { /* ignore */ }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!stellaDep) {
|
|
41
|
+
result.status = 'warning'
|
|
42
|
+
result.details.message = 'Not installed'
|
|
43
|
+
return result
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
result.details.installed = true
|
|
47
|
+
result.details.dependencyRef = stellaDep
|
|
48
|
+
result.details.packageLocation = stellaLocation
|
|
49
|
+
result.score = 1
|
|
50
|
+
|
|
51
|
+
// Get installed version
|
|
52
|
+
const nmPaths = [
|
|
53
|
+
join(projectPath, 'node_modules', '@ralph', 'stella', 'package.json'),
|
|
54
|
+
join(projectPath, stellaLocation || '', 'node_modules', '@ralph', 'stella', 'package.json'),
|
|
55
|
+
]
|
|
56
|
+
for (const nmPath of nmPaths) {
|
|
57
|
+
if (!existsSync(nmPath)) continue
|
|
58
|
+
try {
|
|
59
|
+
result.details.version = JSON.parse(readFileSync(nmPath, 'utf-8')).version || null
|
|
60
|
+
break
|
|
61
|
+
} catch { /* ignore */ }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Detect integration level from MCP server source
|
|
65
|
+
const mcpIndexPaths = [
|
|
66
|
+
join(projectPath, 'mcp', 'src', 'index.ts'),
|
|
67
|
+
join(projectPath, 'backend-mcp', 'src', 'index.ts'),
|
|
68
|
+
join(projectPath, 'src', 'index.ts'),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
let mcpSource = null
|
|
72
|
+
for (const p of mcpIndexPaths) {
|
|
73
|
+
if (!existsSync(p)) continue
|
|
74
|
+
try { mcpSource = readFileSync(p, 'utf-8'); break } catch { /* ignore */ }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (mcpSource) {
|
|
78
|
+
const features = []
|
|
79
|
+
const featureChecks = [
|
|
80
|
+
['isAkkoordGiven', 'akkoord-gate'],
|
|
81
|
+
['checkIdentityGate', 'identity-gate'],
|
|
82
|
+
['checkEvaluationGate', 'evaluation-gate'],
|
|
83
|
+
['checkReflectionGate', 'reflection-gate'],
|
|
84
|
+
['processInlineStatus', 'inline-params'],
|
|
85
|
+
['processInlineReview', 'inline-params'],
|
|
86
|
+
['processInlineReflection', 'inline-params'],
|
|
87
|
+
['afterToolCall', 'reflection-counting'],
|
|
88
|
+
['getLargeResponseTip', 'large-response-gate'],
|
|
89
|
+
['getContextualTip', 'contextual-tips'],
|
|
90
|
+
['getEvaluationCountdown', 'eval-countdown'],
|
|
91
|
+
['getContextWarning', 'token-tracking'],
|
|
92
|
+
['token-tracker', 'token-tracking'],
|
|
93
|
+
['apiMonitor', 'api-monitoring'],
|
|
94
|
+
['api-monitor', 'api-monitoring'],
|
|
95
|
+
['batch_tools', 'batch-tools'],
|
|
96
|
+
['batchTools', 'batch-tools'],
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
for (const [pattern, feature] of featureChecks) {
|
|
100
|
+
if (mcpSource.includes(pattern) && !features.includes(feature)) features.push(feature)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (mcpSource.includes('withGates') && !mcpSource.includes('isAkkoordGiven')) features.push('withGates-wrapper')
|
|
104
|
+
|
|
105
|
+
result.details.features = features
|
|
106
|
+
|
|
107
|
+
const hasExplicitGates = features.includes('akkoord-gate') && features.includes('identity-gate')
|
|
108
|
+
const hasMonitoring = features.includes('token-tracking') || features.includes('api-monitoring')
|
|
109
|
+
|
|
110
|
+
if (hasExplicitGates && hasMonitoring && features.length >= 8) {
|
|
111
|
+
result.details.integrationLevel = 'full'
|
|
112
|
+
result.score = 2
|
|
113
|
+
} else if (features.length >= 1) {
|
|
114
|
+
result.details.integrationLevel = 'basic'
|
|
115
|
+
result.status = 'warning'
|
|
116
|
+
result.details.message = `Basic integration (${features.length} features) — upgrade to full gate orchestration`
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
result.status = 'warning'
|
|
120
|
+
result.details.message = 'Installed but no MCP server source found'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return result
|
|
124
|
+
}
|