@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.
Files changed (39) hide show
  1. package/README.md +312 -0
  2. package/bin/vca-audit.js +90 -0
  3. package/bin/vca-dev-token.js +39 -0
  4. package/bin/vca-setup.js +227 -0
  5. package/lib/checks/codeQuality/api-response-format.js +268 -0
  6. package/lib/checks/health/claude-md.js +114 -0
  7. package/lib/checks/health/doppler-compliance.js +174 -0
  8. package/lib/checks/health/git.js +61 -0
  9. package/lib/checks/health/gitignore.js +83 -0
  10. package/lib/checks/health/index.js +26 -0
  11. package/lib/checks/health/infrastructure-yml.js +87 -0
  12. package/lib/checks/health/mcps.js +57 -0
  13. package/lib/checks/health/naming-conventions.js +302 -0
  14. package/lib/checks/health/plugins.js +38 -0
  15. package/lib/checks/health/quality-toolkit.js +97 -0
  16. package/lib/checks/health/repo-visibility.js +70 -0
  17. package/lib/checks/health/rls-audit.js +130 -0
  18. package/lib/checks/health/scanner.js +68 -0
  19. package/lib/checks/health/secrets.js +80 -0
  20. package/lib/checks/health/stella-integration.js +124 -0
  21. package/lib/checks/health/tests.js +140 -0
  22. package/lib/checks/health/types.js +77 -0
  23. package/lib/checks/health/vincifox-widget.js +47 -0
  24. package/lib/checks/index.js +17 -0
  25. package/lib/checks/security/deprecated-supabase-admin.js +96 -0
  26. package/lib/checks/security/gitignore-validation.js +211 -0
  27. package/lib/checks/security/hardcoded-secrets.js +95 -0
  28. package/lib/checks/security/service-key-exposure.js +107 -0
  29. package/lib/checks/security/systemdb-whitelist.js +138 -0
  30. package/lib/checks/stability/ci-pipeline.js +143 -0
  31. package/lib/checks/stability/husky-hooks.js +117 -0
  32. package/lib/checks/stability/npm-audit.js +140 -0
  33. package/lib/checks/supabase/rls-policy-audit.js +261 -0
  34. package/lib/commands/dev-token.js +342 -0
  35. package/lib/config.js +213 -0
  36. package/lib/index.js +17 -0
  37. package/lib/reporters/terminal.js +134 -0
  38. package/lib/runner.js +179 -0
  39. 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
+ }