@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,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check for Supabase service role key exposure in frontend code
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { glob } from 'glob'
|
|
6
|
+
import { readFileSync } from 'fs'
|
|
7
|
+
|
|
8
|
+
export const meta = {
|
|
9
|
+
id: 'service-key-exposure',
|
|
10
|
+
name: 'Service Role Key Exposure',
|
|
11
|
+
category: 'security',
|
|
12
|
+
severity: 'critical',
|
|
13
|
+
description: 'Detects Supabase service role key usage in frontend code (RLS bypass risk)'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DANGEROUS_PATTERNS = [
|
|
17
|
+
{
|
|
18
|
+
name: 'SERVICE_ROLE_KEY in frontend',
|
|
19
|
+
pattern: /SUPABASE_SERVICE_ROLE_KEY|SERVICE_ROLE_KEY/g,
|
|
20
|
+
severity: 'critical'
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'createClient with service key',
|
|
24
|
+
pattern: /createClient\s*\([^)]*service[_-]?role/gi,
|
|
25
|
+
severity: 'critical'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'supabaseAdmin in frontend',
|
|
29
|
+
pattern: /supabaseAdmin/g,
|
|
30
|
+
severity: 'high'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'VITE_ service key',
|
|
34
|
+
pattern: /VITE_.*SERVICE.*KEY/g,
|
|
35
|
+
severity: 'critical'
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'NEXT_PUBLIC_ service key',
|
|
39
|
+
pattern: /NEXT_PUBLIC_.*SERVICE.*KEY/g,
|
|
40
|
+
severity: 'critical'
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
export async function run(config, projectRoot) {
|
|
45
|
+
const results = {
|
|
46
|
+
passed: true,
|
|
47
|
+
findings: [],
|
|
48
|
+
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Only check frontend directories
|
|
52
|
+
const frontendPaths = config.paths?.frontend || ['frontend/src', 'src']
|
|
53
|
+
const frontendGlobs = frontendPaths.map(p => `${p}/**/*.{ts,tsx,js,jsx}`)
|
|
54
|
+
|
|
55
|
+
for (const pattern of frontendGlobs) {
|
|
56
|
+
const files = await glob(pattern, {
|
|
57
|
+
cwd: projectRoot,
|
|
58
|
+
ignore: [...config.ignore, '**/node_modules/**', '**/backend/**', '**/server/**']
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
// Skip if file is clearly backend
|
|
63
|
+
if (file.includes('/api/') || file.includes('/server/') || file.includes('middleware')) {
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const content = readFileSync(`${projectRoot}/${file}`, 'utf-8')
|
|
69
|
+
const lines = content.split('\n')
|
|
70
|
+
|
|
71
|
+
for (const { name, pattern: regex, severity } of DANGEROUS_PATTERNS) {
|
|
72
|
+
regex.lastIndex = 0
|
|
73
|
+
|
|
74
|
+
let match
|
|
75
|
+
while ((match = regex.exec(content)) !== null) {
|
|
76
|
+
// Find line number
|
|
77
|
+
let lineNumber = 1
|
|
78
|
+
let pos = 0
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
if (pos + line.length >= match.index) {
|
|
81
|
+
break
|
|
82
|
+
}
|
|
83
|
+
pos += line.length + 1
|
|
84
|
+
lineNumber++
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
results.passed = false
|
|
88
|
+
results.findings.push({
|
|
89
|
+
file,
|
|
90
|
+
line: lineNumber,
|
|
91
|
+
type: name,
|
|
92
|
+
severity,
|
|
93
|
+
message: `${name} - service role key should NEVER be in frontend code`,
|
|
94
|
+
snippet: lines[lineNumber - 1]?.trim().substring(0, 80)
|
|
95
|
+
})
|
|
96
|
+
results.summary[severity]++
|
|
97
|
+
results.summary.total++
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} catch (e) {
|
|
101
|
+
// Skip unreadable files
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return results
|
|
107
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check that all systemDB() calls use whitelisted contexts
|
|
3
|
+
*
|
|
4
|
+
* This prevents accidental RLS bypass by requiring explicit context strings
|
|
5
|
+
* that are defined in the systemDb.ts whitelist.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { glob } from 'glob'
|
|
9
|
+
import { readFileSync, existsSync } from 'fs'
|
|
10
|
+
|
|
11
|
+
export const meta = {
|
|
12
|
+
id: 'systemdb-whitelist',
|
|
13
|
+
name: 'systemDB Context Whitelist',
|
|
14
|
+
category: 'security',
|
|
15
|
+
severity: 'high',
|
|
16
|
+
description: 'Ensures all systemDB() calls use whitelisted context strings'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function run(config, projectRoot) {
|
|
20
|
+
const results = {
|
|
21
|
+
passed: true,
|
|
22
|
+
findings: [],
|
|
23
|
+
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Find systemDb.ts to extract whitelist
|
|
27
|
+
const systemDbPaths = [
|
|
28
|
+
`${projectRoot}/backend/src/core/systemDb.ts`,
|
|
29
|
+
`${projectRoot}/src/core/systemDb.ts`
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
let systemDbPath = null
|
|
33
|
+
for (const path of systemDbPaths) {
|
|
34
|
+
if (existsSync(path)) {
|
|
35
|
+
systemDbPath = path
|
|
36
|
+
break
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!systemDbPath) {
|
|
41
|
+
results.skipped = true
|
|
42
|
+
results.skipReason = 'No systemDb.ts found'
|
|
43
|
+
return results
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Extract whitelist from systemDb.ts
|
|
47
|
+
const systemDbContent = readFileSync(systemDbPath, 'utf-8')
|
|
48
|
+
const whitelistMatch = systemDbContent.match(/new Set\s*\(\s*\[([\s\S]*?)\]\s*\)/m)
|
|
49
|
+
|
|
50
|
+
if (!whitelistMatch) {
|
|
51
|
+
results.passed = false
|
|
52
|
+
results.findings.push({
|
|
53
|
+
file: systemDbPath,
|
|
54
|
+
line: 1,
|
|
55
|
+
type: 'missing-whitelist',
|
|
56
|
+
severity: 'critical',
|
|
57
|
+
message: 'systemDb.ts does not contain a whitelist Set'
|
|
58
|
+
})
|
|
59
|
+
results.summary.critical++
|
|
60
|
+
results.summary.total++
|
|
61
|
+
return results
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Parse whitelist entries
|
|
65
|
+
const whitelistStr = whitelistMatch[1]
|
|
66
|
+
const whitelist = new Set(
|
|
67
|
+
whitelistStr
|
|
68
|
+
.split('\n')
|
|
69
|
+
.map(line => {
|
|
70
|
+
const match = line.match(/['"]([^'"]+)['"]/)
|
|
71
|
+
return match ? match[1] : null
|
|
72
|
+
})
|
|
73
|
+
.filter(Boolean)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
// Find all systemDB() calls in the codebase
|
|
77
|
+
const files = await glob('**/*.ts', {
|
|
78
|
+
cwd: projectRoot,
|
|
79
|
+
ignore: [
|
|
80
|
+
...config.ignore,
|
|
81
|
+
'**/systemDb.ts', // Skip the definition file
|
|
82
|
+
'**/*.test.ts',
|
|
83
|
+
'**/*.spec.ts'
|
|
84
|
+
]
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
for (const file of files) {
|
|
88
|
+
try {
|
|
89
|
+
const content = readFileSync(`${projectRoot}/${file}`, 'utf-8')
|
|
90
|
+
const lines = content.split('\n')
|
|
91
|
+
|
|
92
|
+
// Find systemDB('context') calls
|
|
93
|
+
const pattern = /systemDB\s*\(\s*['"]([^'"]+)['"]\s*\)/g
|
|
94
|
+
|
|
95
|
+
let match
|
|
96
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
97
|
+
const context = match[1]
|
|
98
|
+
|
|
99
|
+
// Find line number
|
|
100
|
+
let lineNumber = 1
|
|
101
|
+
let pos = 0
|
|
102
|
+
for (const line of lines) {
|
|
103
|
+
if (pos + line.length >= match.index) {
|
|
104
|
+
break
|
|
105
|
+
}
|
|
106
|
+
pos += line.length + 1
|
|
107
|
+
lineNumber++
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!whitelist.has(context)) {
|
|
111
|
+
results.passed = false
|
|
112
|
+
results.findings.push({
|
|
113
|
+
file,
|
|
114
|
+
line: lineNumber,
|
|
115
|
+
type: 'unknown-context',
|
|
116
|
+
severity: 'high',
|
|
117
|
+
message: `systemDB context '${context}' is not in whitelist`,
|
|
118
|
+
snippet: lines[lineNumber - 1]?.trim().substring(0, 100),
|
|
119
|
+
fix: `Add '${context}' to ALLOWED_CONTEXTS in systemDb.ts`
|
|
120
|
+
})
|
|
121
|
+
results.summary.high++
|
|
122
|
+
results.summary.total++
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch (e) {
|
|
126
|
+
// Skip unreadable files
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Add info about whitelist
|
|
131
|
+
results.info = {
|
|
132
|
+
whitelistPath: systemDbPath,
|
|
133
|
+
whitelistCount: whitelist.size,
|
|
134
|
+
contexts: Array.from(whitelist)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return results
|
|
138
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check for CI/CD pipeline configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync, readdirSync } from 'fs'
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
|
|
8
|
+
export const meta = {
|
|
9
|
+
id: 'ci-pipeline',
|
|
10
|
+
name: 'CI/CD Pipeline',
|
|
11
|
+
category: 'stability',
|
|
12
|
+
severity: 'high',
|
|
13
|
+
description: 'Ensures CI/CD pipeline is configured with essential checks'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function run(config, projectRoot) {
|
|
17
|
+
const results = {
|
|
18
|
+
passed: true,
|
|
19
|
+
findings: [],
|
|
20
|
+
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check for CI config files
|
|
24
|
+
const ciPaths = [
|
|
25
|
+
{ path: '.github/workflows', name: 'GitHub Actions' },
|
|
26
|
+
{ path: '.gitlab-ci.yml', name: 'GitLab CI' },
|
|
27
|
+
{ path: '.circleci/config.yml', name: 'CircleCI' },
|
|
28
|
+
{ path: 'Jenkinsfile', name: 'Jenkins' },
|
|
29
|
+
{ path: 'azure-pipelines.yml', name: 'Azure DevOps' }
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
let foundCi = null
|
|
33
|
+
let ciContent = ''
|
|
34
|
+
|
|
35
|
+
for (const { path, name } of ciPaths) {
|
|
36
|
+
const fullPath = join(projectRoot, path)
|
|
37
|
+
if (existsSync(fullPath)) {
|
|
38
|
+
foundCi = name
|
|
39
|
+
|
|
40
|
+
// Read content
|
|
41
|
+
if (path.endsWith('.yml') || path.endsWith('.yaml')) {
|
|
42
|
+
ciContent = readFileSync(fullPath, 'utf-8')
|
|
43
|
+
} else if (path === '.github/workflows') {
|
|
44
|
+
// Read all workflow files
|
|
45
|
+
const files = readdirSync(fullPath).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'))
|
|
46
|
+
ciContent = files.map(f => readFileSync(join(fullPath, f), 'utf-8')).join('\n')
|
|
47
|
+
}
|
|
48
|
+
break
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!foundCi) {
|
|
53
|
+
results.passed = false
|
|
54
|
+
results.findings.push({
|
|
55
|
+
type: 'ci-missing',
|
|
56
|
+
severity: 'high',
|
|
57
|
+
message: 'No CI/CD configuration found',
|
|
58
|
+
fix: 'Add .github/workflows/ with CI configuration'
|
|
59
|
+
})
|
|
60
|
+
results.summary.high++
|
|
61
|
+
results.summary.total++
|
|
62
|
+
return results
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Check for essential CI steps
|
|
66
|
+
const essentialChecks = [
|
|
67
|
+
{
|
|
68
|
+
name: 'install-dependencies',
|
|
69
|
+
patterns: ['npm ci', 'npm install', 'yarn install', 'pnpm install'],
|
|
70
|
+
severity: 'high'
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'lint',
|
|
74
|
+
patterns: ['npm run lint', 'eslint', 'npx lint'],
|
|
75
|
+
severity: 'medium'
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'type-check',
|
|
79
|
+
patterns: ['tsc', 'typecheck', 'type-check', '--noEmit'],
|
|
80
|
+
severity: 'medium'
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'test',
|
|
84
|
+
patterns: ['npm test', 'npm run test', 'jest', 'vitest'],
|
|
85
|
+
severity: 'high'
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'build',
|
|
89
|
+
patterns: ['npm run build', 'vite build', 'next build'],
|
|
90
|
+
severity: 'medium'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'security-audit',
|
|
94
|
+
patterns: ['npm audit', 'vca-audit', 'security-check', 'snyk', 'CodeQL'],
|
|
95
|
+
severity: 'medium'
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
const ciContentLower = ciContent.toLowerCase()
|
|
100
|
+
|
|
101
|
+
for (const { name, patterns, severity } of essentialChecks) {
|
|
102
|
+
const hasCheck = patterns.some(p => ciContentLower.includes(p.toLowerCase()))
|
|
103
|
+
|
|
104
|
+
if (!hasCheck) {
|
|
105
|
+
if (severity === 'high') {
|
|
106
|
+
results.passed = false
|
|
107
|
+
}
|
|
108
|
+
results.findings.push({
|
|
109
|
+
type: `ci-missing-${name}`,
|
|
110
|
+
severity,
|
|
111
|
+
message: `CI pipeline missing: ${name}`,
|
|
112
|
+
fix: `Add ${name} step to your CI workflow`
|
|
113
|
+
})
|
|
114
|
+
results.summary[severity]++
|
|
115
|
+
results.summary.total++
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check for branch protection indicators
|
|
120
|
+
const hasBranchRestriction = ciContent.includes('branches:') ||
|
|
121
|
+
ciContent.includes('on:\n push:') ||
|
|
122
|
+
ciContent.includes('pull_request:')
|
|
123
|
+
|
|
124
|
+
if (!hasBranchRestriction) {
|
|
125
|
+
results.findings.push({
|
|
126
|
+
type: 'ci-no-branch-filter',
|
|
127
|
+
severity: 'low',
|
|
128
|
+
message: 'CI runs on all branches - consider limiting to main/master and PRs',
|
|
129
|
+
fix: 'Add branch filters to CI configuration'
|
|
130
|
+
})
|
|
131
|
+
results.summary.low++
|
|
132
|
+
results.summary.total++
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
results.info = {
|
|
136
|
+
ciProvider: foundCi,
|
|
137
|
+
workflowCount: foundCi === 'GitHub Actions'
|
|
138
|
+
? readdirSync(join(projectRoot, '.github/workflows')).filter(f => f.endsWith('.yml')).length
|
|
139
|
+
: 1
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return results
|
|
143
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check for pre-commit hooks (Husky)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync } from 'fs'
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
|
|
8
|
+
export const meta = {
|
|
9
|
+
id: 'husky-hooks',
|
|
10
|
+
name: 'Pre-commit Hooks (Husky)',
|
|
11
|
+
category: 'stability',
|
|
12
|
+
severity: 'high',
|
|
13
|
+
description: 'Ensures Husky pre-commit hooks are configured'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function run(config, projectRoot) {
|
|
17
|
+
const results = {
|
|
18
|
+
passed: true,
|
|
19
|
+
findings: [],
|
|
20
|
+
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Check 1: Husky installed
|
|
24
|
+
const huskyDir = join(projectRoot, '.husky')
|
|
25
|
+
if (!existsSync(huskyDir)) {
|
|
26
|
+
results.passed = false
|
|
27
|
+
results.findings.push({
|
|
28
|
+
type: 'husky-missing',
|
|
29
|
+
severity: 'high',
|
|
30
|
+
message: 'Husky is not installed - no pre-commit validation',
|
|
31
|
+
fix: 'Run: npm install --save-dev husky && npx husky init'
|
|
32
|
+
})
|
|
33
|
+
results.summary.high++
|
|
34
|
+
results.summary.total++
|
|
35
|
+
return results
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check 2: pre-commit hook exists
|
|
39
|
+
const preCommitPath = join(huskyDir, 'pre-commit')
|
|
40
|
+
if (!existsSync(preCommitPath)) {
|
|
41
|
+
results.passed = false
|
|
42
|
+
results.findings.push({
|
|
43
|
+
type: 'pre-commit-missing',
|
|
44
|
+
severity: 'high',
|
|
45
|
+
message: 'No pre-commit hook configured',
|
|
46
|
+
fix: 'Create .husky/pre-commit with your checks'
|
|
47
|
+
})
|
|
48
|
+
results.summary.high++
|
|
49
|
+
results.summary.total++
|
|
50
|
+
return results
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check 3: pre-commit content has useful checks
|
|
54
|
+
const preCommitContent = readFileSync(preCommitPath, 'utf-8')
|
|
55
|
+
const recommendedChecks = [
|
|
56
|
+
{ name: 'lint', patterns: ['lint', 'eslint'] },
|
|
57
|
+
{ name: 'type-check', patterns: ['tsc', 'typecheck', 'type-check'] },
|
|
58
|
+
{ name: 'test', patterns: ['test', 'jest', 'vitest'] },
|
|
59
|
+
{ name: 'security', patterns: ['security', 'audit', 'vca-'] }
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
const missingChecks = []
|
|
63
|
+
for (const { name, patterns } of recommendedChecks) {
|
|
64
|
+
const hasCheck = patterns.some(p => preCommitContent.toLowerCase().includes(p))
|
|
65
|
+
if (!hasCheck) {
|
|
66
|
+
missingChecks.push(name)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (missingChecks.length > 0) {
|
|
71
|
+
results.findings.push({
|
|
72
|
+
type: 'missing-recommended-checks',
|
|
73
|
+
severity: 'medium',
|
|
74
|
+
message: `Pre-commit hook missing recommended checks: ${missingChecks.join(', ')}`,
|
|
75
|
+
fix: 'Add these checks to .husky/pre-commit'
|
|
76
|
+
})
|
|
77
|
+
results.summary.medium++
|
|
78
|
+
results.summary.total++
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Check 4: lint-staged configured (optional but recommended)
|
|
82
|
+
const packagePath = join(projectRoot, 'package.json')
|
|
83
|
+
if (existsSync(packagePath)) {
|
|
84
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
|
|
85
|
+
const hasLintStaged = pkg['lint-staged'] ||
|
|
86
|
+
existsSync(join(projectRoot, '.lintstagedrc')) ||
|
|
87
|
+
existsSync(join(projectRoot, 'lint-staged.config.js'))
|
|
88
|
+
|
|
89
|
+
if (!hasLintStaged && preCommitContent.includes('lint-staged')) {
|
|
90
|
+
results.findings.push({
|
|
91
|
+
type: 'lint-staged-missing',
|
|
92
|
+
severity: 'low',
|
|
93
|
+
message: 'lint-staged is referenced but not configured',
|
|
94
|
+
fix: 'Add lint-staged configuration to package.json or create .lintstagedrc'
|
|
95
|
+
})
|
|
96
|
+
results.summary.low++
|
|
97
|
+
results.summary.total++
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check 5: prepare script in package.json
|
|
102
|
+
if (existsSync(packagePath)) {
|
|
103
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
|
|
104
|
+
if (!pkg.scripts?.prepare?.includes('husky')) {
|
|
105
|
+
results.findings.push({
|
|
106
|
+
type: 'prepare-script-missing',
|
|
107
|
+
severity: 'low',
|
|
108
|
+
message: 'No "prepare": "husky" script - hooks may not install on npm install',
|
|
109
|
+
fix: 'Add "prepare": "husky" to package.json scripts'
|
|
110
|
+
})
|
|
111
|
+
results.summary.low++
|
|
112
|
+
results.summary.total++
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return results
|
|
117
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check for npm vulnerabilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { execSync } from 'child_process'
|
|
6
|
+
import { existsSync } from 'fs'
|
|
7
|
+
import { join } from 'path'
|
|
8
|
+
|
|
9
|
+
export const meta = {
|
|
10
|
+
id: 'npm-audit',
|
|
11
|
+
name: 'NPM Vulnerability Audit',
|
|
12
|
+
category: 'stability',
|
|
13
|
+
severity: 'high',
|
|
14
|
+
description: 'Checks for known vulnerabilities in npm dependencies'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function run(config, projectRoot) {
|
|
18
|
+
const results = {
|
|
19
|
+
passed: true,
|
|
20
|
+
findings: [],
|
|
21
|
+
summary: { total: 0, critical: 0, high: 0, medium: 0, low: 0 }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Check for package-lock.json
|
|
25
|
+
const lockFile = join(projectRoot, 'package-lock.json')
|
|
26
|
+
if (!existsSync(lockFile)) {
|
|
27
|
+
results.findings.push({
|
|
28
|
+
type: 'no-lock-file',
|
|
29
|
+
severity: 'medium',
|
|
30
|
+
message: 'No package-lock.json found - cannot run npm audit',
|
|
31
|
+
fix: 'Run npm install to generate package-lock.json'
|
|
32
|
+
})
|
|
33
|
+
results.summary.medium++
|
|
34
|
+
results.summary.total++
|
|
35
|
+
return results
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Run npm audit
|
|
40
|
+
const auditOutput = execSync('npm audit --json 2>/dev/null', {
|
|
41
|
+
cwd: projectRoot,
|
|
42
|
+
encoding: 'utf-8',
|
|
43
|
+
maxBuffer: 10 * 1024 * 1024 // 10MB buffer
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
const audit = JSON.parse(auditOutput)
|
|
47
|
+
const vulnerabilities = audit.metadata?.vulnerabilities || {}
|
|
48
|
+
|
|
49
|
+
// Get allowed thresholds from config
|
|
50
|
+
const allowed = config.stability?.allowedVulnerabilities || {
|
|
51
|
+
critical: 0,
|
|
52
|
+
high: 0,
|
|
53
|
+
moderate: 10
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check critical
|
|
57
|
+
if (vulnerabilities.critical > allowed.critical) {
|
|
58
|
+
results.passed = false
|
|
59
|
+
results.findings.push({
|
|
60
|
+
type: 'critical-vulnerabilities',
|
|
61
|
+
severity: 'critical',
|
|
62
|
+
message: `${vulnerabilities.critical} critical vulnerabilities found (allowed: ${allowed.critical})`,
|
|
63
|
+
fix: 'Run: npm audit fix --force (or manually update packages)'
|
|
64
|
+
})
|
|
65
|
+
results.summary.critical += vulnerabilities.critical
|
|
66
|
+
results.summary.total += vulnerabilities.critical
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check high
|
|
70
|
+
if (vulnerabilities.high > allowed.high) {
|
|
71
|
+
results.passed = false
|
|
72
|
+
results.findings.push({
|
|
73
|
+
type: 'high-vulnerabilities',
|
|
74
|
+
severity: 'high',
|
|
75
|
+
message: `${vulnerabilities.high} high severity vulnerabilities found (allowed: ${allowed.high})`,
|
|
76
|
+
fix: 'Run: npm audit fix'
|
|
77
|
+
})
|
|
78
|
+
results.summary.high += vulnerabilities.high
|
|
79
|
+
results.summary.total += vulnerabilities.high
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check moderate (warning only)
|
|
83
|
+
if (vulnerabilities.moderate > allowed.moderate) {
|
|
84
|
+
results.findings.push({
|
|
85
|
+
type: 'moderate-vulnerabilities',
|
|
86
|
+
severity: 'medium',
|
|
87
|
+
message: `${vulnerabilities.moderate} moderate vulnerabilities found (allowed: ${allowed.moderate})`,
|
|
88
|
+
fix: 'Consider running: npm audit fix'
|
|
89
|
+
})
|
|
90
|
+
results.summary.medium += vulnerabilities.moderate
|
|
91
|
+
results.summary.total += vulnerabilities.moderate
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Info about total vulnerabilities
|
|
95
|
+
results.info = {
|
|
96
|
+
critical: vulnerabilities.critical || 0,
|
|
97
|
+
high: vulnerabilities.high || 0,
|
|
98
|
+
moderate: vulnerabilities.moderate || 0,
|
|
99
|
+
low: vulnerabilities.low || 0,
|
|
100
|
+
total: audit.metadata?.totalDependencies || 0
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
} catch (e) {
|
|
104
|
+
// npm audit returns non-zero exit code when vulnerabilities found
|
|
105
|
+
// Try to parse the JSON output anyway
|
|
106
|
+
if (e.stdout) {
|
|
107
|
+
try {
|
|
108
|
+
const audit = JSON.parse(e.stdout)
|
|
109
|
+
const vulnerabilities = audit.metadata?.vulnerabilities || {}
|
|
110
|
+
|
|
111
|
+
results.info = {
|
|
112
|
+
critical: vulnerabilities.critical || 0,
|
|
113
|
+
high: vulnerabilities.high || 0,
|
|
114
|
+
moderate: vulnerabilities.moderate || 0,
|
|
115
|
+
low: vulnerabilities.low || 0
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (vulnerabilities.critical > 0 || vulnerabilities.high > 0) {
|
|
119
|
+
results.passed = false
|
|
120
|
+
results.findings.push({
|
|
121
|
+
type: 'vulnerabilities-found',
|
|
122
|
+
severity: vulnerabilities.critical > 0 ? 'critical' : 'high',
|
|
123
|
+
message: `Found ${vulnerabilities.critical || 0} critical, ${vulnerabilities.high || 0} high vulnerabilities`,
|
|
124
|
+
fix: 'Run: npm audit fix'
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// Can't parse output
|
|
129
|
+
results.findings.push({
|
|
130
|
+
type: 'audit-error',
|
|
131
|
+
severity: 'medium',
|
|
132
|
+
message: 'Could not run npm audit',
|
|
133
|
+
fix: 'Ensure npm is installed and package-lock.json exists'
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return results
|
|
140
|
+
}
|