nexu-app 2.0.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 +149 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1192 -0
- package/package.json +43 -0
- package/templates/default/.changeset/config.json +11 -0
- package/templates/default/.eslintignore +16 -0
- package/templates/default/.eslintrc.js +67 -0
- package/templates/default/.github/actions/build/action.yml +35 -0
- package/templates/default/.github/actions/quality/action.yml +53 -0
- package/templates/default/.github/dependabot.yml +51 -0
- package/templates/default/.github/workflows/deploy-dev.yml +83 -0
- package/templates/default/.github/workflows/deploy-prod.yml +83 -0
- package/templates/default/.github/workflows/deploy-rec.yml +83 -0
- package/templates/default/.husky/commit-msg +1 -0
- package/templates/default/.husky/pre-commit +1 -0
- package/templates/default/.nexu-version +1 -0
- package/templates/default/.prettierignore +7 -0
- package/templates/default/.prettierrc +19 -0
- package/templates/default/.vscode/extensions.json +14 -0
- package/templates/default/.vscode/settings.json +36 -0
- package/templates/default/apps/gitkeep +0 -0
- package/templates/default/commitlint.config.js +26 -0
- package/templates/default/docker/docker-compose.dev.yml +49 -0
- package/templates/default/docker/docker-compose.prod.yml +64 -0
- package/templates/default/docker/docker-compose.yml +6 -0
- package/templates/default/docs/architecture.md +452 -0
- package/templates/default/docs/cli.md +330 -0
- package/templates/default/docs/contributing.md +462 -0
- package/templates/default/docs/scripts.md +460 -0
- package/templates/default/gitignore +44 -0
- package/templates/default/lintstagedrc.cjs +4 -0
- package/templates/default/package.json +51 -0
- package/templates/default/packages/auth/package.json +61 -0
- package/templates/default/packages/auth/src/components/ProtectedRoute.tsx +75 -0
- package/templates/default/packages/auth/src/components/SignInForm.tsx +153 -0
- package/templates/default/packages/auth/src/components/SignUpForm.tsx +179 -0
- package/templates/default/packages/auth/src/components/SocialButtons.tsx +147 -0
- package/templates/default/packages/auth/src/components/index.ts +4 -0
- package/templates/default/packages/auth/src/hooks/index.ts +4 -0
- package/templates/default/packages/auth/src/hooks/useAuth.ts +51 -0
- package/templates/default/packages/auth/src/hooks/useRequireAuth.ts +54 -0
- package/templates/default/packages/auth/src/hooks/useSession.ts +48 -0
- package/templates/default/packages/auth/src/hooks/useUser.ts +48 -0
- package/templates/default/packages/auth/src/index.ts +45 -0
- package/templates/default/packages/auth/src/next/index.ts +18 -0
- package/templates/default/packages/auth/src/next/middleware.ts +183 -0
- package/templates/default/packages/auth/src/next/server.ts +219 -0
- package/templates/default/packages/auth/src/providers/AuthContext.tsx +435 -0
- package/templates/default/packages/auth/src/providers/index.ts +1 -0
- package/templates/default/packages/auth/src/types/index.ts +284 -0
- package/templates/default/packages/auth/src/utils/api.ts +228 -0
- package/templates/default/packages/auth/src/utils/index.ts +3 -0
- package/templates/default/packages/auth/src/utils/oauth.ts +230 -0
- package/templates/default/packages/auth/src/utils/token.ts +204 -0
- package/templates/default/packages/auth/tsconfig.json +14 -0
- package/templates/default/packages/auth/tsup.config.ts +18 -0
- package/templates/default/packages/cache/package.json +26 -0
- package/templates/default/packages/cache/src/index.ts +137 -0
- package/templates/default/packages/cache/tsconfig.json +9 -0
- package/templates/default/packages/cache/tsup.config.ts +9 -0
- package/templates/default/packages/config/eslint/index.js +20 -0
- package/templates/default/packages/config/package.json +9 -0
- package/templates/default/packages/config/typescript/base.json +26 -0
- package/templates/default/packages/constants/package.json +26 -0
- package/templates/default/packages/constants/src/index.ts +121 -0
- package/templates/default/packages/constants/tsconfig.json +9 -0
- package/templates/default/packages/constants/tsup.config.ts +9 -0
- package/templates/default/packages/logger/package.json +27 -0
- package/templates/default/packages/logger/src/index.ts +197 -0
- package/templates/default/packages/logger/tsconfig.json +11 -0
- package/templates/default/packages/logger/tsup.config.ts +9 -0
- package/templates/default/packages/result/package.json +26 -0
- package/templates/default/packages/result/src/index.ts +142 -0
- package/templates/default/packages/result/tsconfig.json +9 -0
- package/templates/default/packages/result/tsup.config.ts +9 -0
- package/templates/default/packages/types/package.json +26 -0
- package/templates/default/packages/types/src/index.ts +78 -0
- package/templates/default/packages/types/tsconfig.json +9 -0
- package/templates/default/packages/types/tsup.config.ts +10 -0
- package/templates/default/packages/ui/package.json +38 -0
- package/templates/default/packages/ui/src/components/Button.tsx +58 -0
- package/templates/default/packages/ui/src/components/Card.tsx +85 -0
- package/templates/default/packages/ui/src/components/Input.tsx +45 -0
- package/templates/default/packages/ui/src/index.ts +15 -0
- package/templates/default/packages/ui/tsconfig.json +11 -0
- package/templates/default/packages/ui/tsup.config.ts +11 -0
- package/templates/default/packages/utils/package.json +30 -0
- package/templates/default/packages/utils/src/index.test.ts +130 -0
- package/templates/default/packages/utils/src/index.ts +154 -0
- package/templates/default/packages/utils/tsconfig.json +10 -0
- package/templates/default/packages/utils/tsup.config.ts +10 -0
- package/templates/default/pnpm-workspace.yaml +3 -0
- package/templates/default/scripts/audit.mjs +700 -0
- package/templates/default/scripts/deploy.mjs +40 -0
- package/templates/default/scripts/generate-app.mjs +808 -0
- package/templates/default/scripts/lib/package-manager.mjs +186 -0
- package/templates/default/scripts/setup.mjs +102 -0
- package/templates/default/services/.env.example +16 -0
- package/templates/default/services/docker-compose.yml +207 -0
- package/templates/default/services/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/templates/default/services/grafana/provisioning/datasources/datasources.yml +9 -0
- package/templates/default/services/postgres/init/gitkeep +2 -0
- package/templates/default/services/prometheus/prometheus.yml +13 -0
- package/templates/default/tsconfig.json +27 -0
- package/templates/default/turbo.json +40 -0
- package/templates/default/vitest.config.ts +15 -0
|
@@ -0,0 +1,700 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Code Audit Script
|
|
5
|
+
*
|
|
6
|
+
* Runs multiple security and code quality checks on the monorepo:
|
|
7
|
+
* - npm audit: Check for vulnerable dependencies
|
|
8
|
+
* - ESLint: Code quality and potential bugs
|
|
9
|
+
* - TypeScript: Type checking
|
|
10
|
+
* - Secrets detection: Check for hardcoded secrets
|
|
11
|
+
* - License check: Verify dependency licenses
|
|
12
|
+
* - Dependency check: Outdated and unused dependencies
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import { fileURLToPath } from 'url';
|
|
18
|
+
import { execSync, spawnSync } from 'child_process';
|
|
19
|
+
import { detectPackageManager, getRunCommand, getExecCommand } from './lib/package-manager.mjs';
|
|
20
|
+
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = path.dirname(__filename);
|
|
23
|
+
|
|
24
|
+
// Colors for console output
|
|
25
|
+
const colors = {
|
|
26
|
+
red: '\x1b[31m',
|
|
27
|
+
green: '\x1b[32m',
|
|
28
|
+
blue: '\x1b[34m',
|
|
29
|
+
yellow: '\x1b[33m',
|
|
30
|
+
cyan: '\x1b[36m',
|
|
31
|
+
magenta: '\x1b[35m',
|
|
32
|
+
bold: '\x1b[1m',
|
|
33
|
+
dim: '\x1b[2m',
|
|
34
|
+
reset: '\x1b[0m',
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const log = {
|
|
38
|
+
info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
|
|
39
|
+
success: (msg) => console.log(`${colors.green}✓${colors.reset} ${msg}`),
|
|
40
|
+
warn: (msg) => console.log(`${colors.yellow}!${colors.reset} ${msg}`),
|
|
41
|
+
error: (msg) => console.log(`${colors.red}✗${colors.reset} ${msg}`),
|
|
42
|
+
title: (msg) => console.log(`\n${colors.bold}${colors.cyan}${msg}${colors.reset}\n`),
|
|
43
|
+
section: (msg) => console.log(`\n${colors.bold}${colors.magenta}▶ ${msg}${colors.reset}\n`),
|
|
44
|
+
dim: (msg) => console.log(`${colors.dim}${msg}${colors.reset}`),
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Get directories
|
|
48
|
+
const ROOT_DIR = path.resolve(__dirname, '..');
|
|
49
|
+
const APPS_DIR = path.join(ROOT_DIR, 'apps');
|
|
50
|
+
const PACKAGES_DIR = path.join(ROOT_DIR, 'packages');
|
|
51
|
+
|
|
52
|
+
// Detect package manager
|
|
53
|
+
const pm = detectPackageManager(ROOT_DIR);
|
|
54
|
+
const runCmd = getRunCommand(pm);
|
|
55
|
+
const execCmd = getExecCommand(pm);
|
|
56
|
+
|
|
57
|
+
// Parse arguments
|
|
58
|
+
const args = process.argv.slice(2);
|
|
59
|
+
const options = {
|
|
60
|
+
fix: args.includes('--fix'),
|
|
61
|
+
verbose: args.includes('--verbose') || args.includes('-v'),
|
|
62
|
+
security: args.includes('--security') || args.includes('-s') || args.length === 0,
|
|
63
|
+
quality: args.includes('--quality') || args.includes('-q') || args.length === 0,
|
|
64
|
+
deps: args.includes('--deps') || args.includes('-d') || args.length === 0,
|
|
65
|
+
secrets: args.includes('--secrets') || args.length === 0,
|
|
66
|
+
all: args.includes('--all') || args.includes('-a'),
|
|
67
|
+
app: args.find((a) => a.startsWith('--app='))?.split('=')[1],
|
|
68
|
+
help: args.includes('--help') || args.includes('-h'),
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (options.all) {
|
|
72
|
+
options.security = true;
|
|
73
|
+
options.quality = true;
|
|
74
|
+
options.deps = true;
|
|
75
|
+
options.secrets = true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Results tracking
|
|
79
|
+
const results = {
|
|
80
|
+
passed: 0,
|
|
81
|
+
warnings: 0,
|
|
82
|
+
errors: 0,
|
|
83
|
+
checks: [],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
function addResult(name, status, message = '') {
|
|
87
|
+
results.checks.push({ name, status, message });
|
|
88
|
+
if (status === 'pass') results.passed++;
|
|
89
|
+
else if (status === 'warn') results.warnings++;
|
|
90
|
+
else if (status === 'error') results.errors++;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Run command and return result
|
|
94
|
+
function runCommand(cmd, cwd = ROOT_DIR, silent = false) {
|
|
95
|
+
try {
|
|
96
|
+
const output = execSync(cmd, {
|
|
97
|
+
cwd,
|
|
98
|
+
encoding: 'utf-8',
|
|
99
|
+
stdio: silent ? 'pipe' : ['pipe', 'pipe', 'pipe'],
|
|
100
|
+
});
|
|
101
|
+
return { success: true, output };
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
output: error.stdout || '',
|
|
106
|
+
error: error.stderr || error.message,
|
|
107
|
+
exitCode: error.status,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check if command exists
|
|
113
|
+
function commandExists(cmd) {
|
|
114
|
+
const result = spawnSync(process.platform === 'win32' ? 'where' : 'which', [cmd], {
|
|
115
|
+
stdio: 'pipe',
|
|
116
|
+
});
|
|
117
|
+
return result.status === 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Get list of apps
|
|
121
|
+
function getApps() {
|
|
122
|
+
if (!fs.existsSync(APPS_DIR)) return [];
|
|
123
|
+
return fs
|
|
124
|
+
.readdirSync(APPS_DIR, { withFileTypes: true })
|
|
125
|
+
.filter((d) => d.isDirectory())
|
|
126
|
+
.map((d) => d.name);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Get list of packages
|
|
130
|
+
function getPackages() {
|
|
131
|
+
if (!fs.existsSync(PACKAGES_DIR)) return [];
|
|
132
|
+
return fs
|
|
133
|
+
.readdirSync(PACKAGES_DIR, { withFileTypes: true })
|
|
134
|
+
.filter((d) => d.isDirectory())
|
|
135
|
+
.map((d) => d.name);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ============================================================
|
|
139
|
+
// Security Checks
|
|
140
|
+
// ============================================================
|
|
141
|
+
|
|
142
|
+
async function checkDependencyVulnerabilities() {
|
|
143
|
+
log.section('Dependency Vulnerabilities');
|
|
144
|
+
|
|
145
|
+
let cmd;
|
|
146
|
+
if (pm === 'pnpm') {
|
|
147
|
+
cmd = 'pnpm audit --json';
|
|
148
|
+
} else if (pm === 'yarn') {
|
|
149
|
+
cmd = 'yarn audit --json';
|
|
150
|
+
} else {
|
|
151
|
+
cmd = 'npm audit --json';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const result = runCommand(cmd, ROOT_DIR, true);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
// Parse audit results
|
|
158
|
+
let vulnerabilities = { critical: 0, high: 0, moderate: 0, low: 0 };
|
|
159
|
+
|
|
160
|
+
if (pm === 'pnpm') {
|
|
161
|
+
// pnpm audit format
|
|
162
|
+
const lines = result.output.split('\n').filter((l) => l.trim());
|
|
163
|
+
for (const line of lines) {
|
|
164
|
+
try {
|
|
165
|
+
const data = JSON.parse(line);
|
|
166
|
+
if (data.type === 'auditSummary') {
|
|
167
|
+
vulnerabilities = data.data.vulnerabilities || vulnerabilities;
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// Skip non-JSON lines
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
// npm/yarn audit format
|
|
175
|
+
const data = JSON.parse(result.output || '{}');
|
|
176
|
+
vulnerabilities = data.metadata?.vulnerabilities || vulnerabilities;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const total = vulnerabilities.critical + vulnerabilities.high + vulnerabilities.moderate + vulnerabilities.low;
|
|
180
|
+
|
|
181
|
+
if (vulnerabilities.critical > 0 || vulnerabilities.high > 0) {
|
|
182
|
+
log.error(`Found ${vulnerabilities.critical} critical and ${vulnerabilities.high} high vulnerabilities`);
|
|
183
|
+
addResult('Dependency Vulnerabilities', 'error', `${vulnerabilities.critical} critical, ${vulnerabilities.high} high`);
|
|
184
|
+
} else if (vulnerabilities.moderate > 0 || vulnerabilities.low > 0) {
|
|
185
|
+
log.warn(`Found ${vulnerabilities.moderate} moderate and ${vulnerabilities.low} low vulnerabilities`);
|
|
186
|
+
addResult('Dependency Vulnerabilities', 'warn', `${vulnerabilities.moderate} moderate, ${vulnerabilities.low} low`);
|
|
187
|
+
} else {
|
|
188
|
+
log.success('No vulnerabilities found');
|
|
189
|
+
addResult('Dependency Vulnerabilities', 'pass');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (options.verbose && total > 0) {
|
|
193
|
+
log.dim(`Run '${pm} audit' for details`);
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
if (result.exitCode === 0) {
|
|
197
|
+
log.success('No vulnerabilities found');
|
|
198
|
+
addResult('Dependency Vulnerabilities', 'pass');
|
|
199
|
+
} else {
|
|
200
|
+
log.warn('Could not parse audit results');
|
|
201
|
+
addResult('Dependency Vulnerabilities', 'warn', 'Could not parse results');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ============================================================
|
|
207
|
+
// Secrets Detection
|
|
208
|
+
// ============================================================
|
|
209
|
+
|
|
210
|
+
const SECRET_PATTERNS = [
|
|
211
|
+
{ name: 'AWS Access Key', pattern: /AKIA[0-9A-Z]{16}/g },
|
|
212
|
+
{ name: 'AWS Secret Key', pattern: /[0-9a-zA-Z/+]{40}/g, context: /aws|secret/i },
|
|
213
|
+
{ name: 'GitHub Token', pattern: /gh[pousr]_[A-Za-z0-9_]{36,}/g },
|
|
214
|
+
{ name: 'Generic API Key', pattern: /api[_-]?key['":\s]*['"]?[A-Za-z0-9_\-]{20,}['"]?/gi },
|
|
215
|
+
{ name: 'Generic Secret', pattern: /secret['":\s]*['"]?[A-Za-z0-9_\-]{20,}['"]?/gi },
|
|
216
|
+
{ name: 'Private Key', pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----/g },
|
|
217
|
+
{ name: 'JWT Token', pattern: /eyJ[A-Za-z0-9_-]*\.eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]*/g },
|
|
218
|
+
{ name: 'Basic Auth', pattern: /basic\s+[A-Za-z0-9+/=]{20,}/gi },
|
|
219
|
+
{ name: 'Bearer Token', pattern: /bearer\s+[A-Za-z0-9_\-.~+/]+=*/gi },
|
|
220
|
+
{ name: 'Database URL', pattern: /(?:postgres|mysql|mongodb|redis):\/\/[^:]+:[^@]+@/gi },
|
|
221
|
+
{ name: 'Password in URL', pattern: /(?:password|passwd|pwd)['"=:\s]+['"]?[^'"\s]{8,}['"]?/gi },
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
const IGNORE_PATHS = [
|
|
225
|
+
'node_modules',
|
|
226
|
+
'.git',
|
|
227
|
+
'dist',
|
|
228
|
+
'build',
|
|
229
|
+
'.next',
|
|
230
|
+
'.nuxt',
|
|
231
|
+
'coverage',
|
|
232
|
+
'.turbo',
|
|
233
|
+
'*.lock',
|
|
234
|
+
'pnpm-lock.yaml',
|
|
235
|
+
'package-lock.json',
|
|
236
|
+
'yarn.lock',
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
const IGNORE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot'];
|
|
240
|
+
|
|
241
|
+
function shouldIgnorePath(filePath) {
|
|
242
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
243
|
+
return IGNORE_PATHS.some((ignore) => {
|
|
244
|
+
if (ignore.startsWith('*')) {
|
|
245
|
+
return normalized.endsWith(ignore.slice(1));
|
|
246
|
+
}
|
|
247
|
+
return normalized.includes(ignore);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function scanFileForSecrets(filePath) {
|
|
252
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
253
|
+
if (IGNORE_EXTENSIONS.includes(ext)) return [];
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
257
|
+
const findings = [];
|
|
258
|
+
|
|
259
|
+
for (const { name, pattern, context } of SECRET_PATTERNS) {
|
|
260
|
+
const matches = content.match(pattern);
|
|
261
|
+
if (matches) {
|
|
262
|
+
for (const match of matches) {
|
|
263
|
+
// Skip if context is required but not found
|
|
264
|
+
if (context && !context.test(content)) continue;
|
|
265
|
+
|
|
266
|
+
// Skip common false positives
|
|
267
|
+
if (match.includes('example') || match.includes('placeholder') || match.includes('your-')) continue;
|
|
268
|
+
|
|
269
|
+
// Skip if it's in a comment or documentation
|
|
270
|
+
const lineIndex = content.indexOf(match);
|
|
271
|
+
const lineStart = content.lastIndexOf('\n', lineIndex) + 1;
|
|
272
|
+
const line = content.slice(lineStart, content.indexOf('\n', lineIndex));
|
|
273
|
+
|
|
274
|
+
if (line.trim().startsWith('//') || line.trim().startsWith('#') || line.trim().startsWith('*')) continue;
|
|
275
|
+
|
|
276
|
+
findings.push({
|
|
277
|
+
type: name,
|
|
278
|
+
file: filePath,
|
|
279
|
+
line: content.slice(0, lineIndex).split('\n').length,
|
|
280
|
+
preview: match.slice(0, 20) + '...',
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return findings;
|
|
287
|
+
} catch {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function scanDirectoryForSecrets(dir, findings = []) {
|
|
293
|
+
if (!fs.existsSync(dir)) return findings;
|
|
294
|
+
|
|
295
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
296
|
+
|
|
297
|
+
for (const entry of entries) {
|
|
298
|
+
const fullPath = path.join(dir, entry.name);
|
|
299
|
+
const relativePath = path.relative(ROOT_DIR, fullPath);
|
|
300
|
+
|
|
301
|
+
if (shouldIgnorePath(relativePath)) continue;
|
|
302
|
+
|
|
303
|
+
if (entry.isDirectory()) {
|
|
304
|
+
scanDirectoryForSecrets(fullPath, findings);
|
|
305
|
+
} else if (entry.isFile()) {
|
|
306
|
+
findings.push(...scanFileForSecrets(fullPath));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return findings;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function checkSecrets() {
|
|
314
|
+
log.section('Secrets Detection');
|
|
315
|
+
|
|
316
|
+
const scanDirs = [APPS_DIR, PACKAGES_DIR, path.join(ROOT_DIR, 'scripts')];
|
|
317
|
+
let allFindings = [];
|
|
318
|
+
|
|
319
|
+
for (const dir of scanDirs) {
|
|
320
|
+
if (fs.existsSync(dir)) {
|
|
321
|
+
allFindings.push(...scanDirectoryForSecrets(dir));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Also scan root config files
|
|
326
|
+
const rootFiles = fs.readdirSync(ROOT_DIR).filter((f) => {
|
|
327
|
+
const ext = path.extname(f);
|
|
328
|
+
return ['.js', '.ts', '.json', '.yaml', '.yml', '.env'].includes(ext) && !f.includes('lock');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
for (const file of rootFiles) {
|
|
332
|
+
allFindings.push(...scanFileForSecrets(path.join(ROOT_DIR, file)));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Deduplicate findings
|
|
336
|
+
const uniqueFindings = allFindings.filter(
|
|
337
|
+
(f, i, arr) => arr.findIndex((x) => x.file === f.file && x.line === f.line && x.type === f.type) === i
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
if (uniqueFindings.length > 0) {
|
|
341
|
+
log.error(`Found ${uniqueFindings.length} potential secrets`);
|
|
342
|
+
|
|
343
|
+
if (options.verbose) {
|
|
344
|
+
for (const finding of uniqueFindings.slice(0, 10)) {
|
|
345
|
+
const relPath = path.relative(ROOT_DIR, finding.file);
|
|
346
|
+
log.dim(` ${finding.type}: ${relPath}:${finding.line}`);
|
|
347
|
+
}
|
|
348
|
+
if (uniqueFindings.length > 10) {
|
|
349
|
+
log.dim(` ... and ${uniqueFindings.length - 10} more`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
addResult('Secrets Detection', 'error', `${uniqueFindings.length} potential secrets found`);
|
|
354
|
+
} else {
|
|
355
|
+
log.success('No hardcoded secrets detected');
|
|
356
|
+
addResult('Secrets Detection', 'pass');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ============================================================
|
|
361
|
+
// Code Quality Checks
|
|
362
|
+
// ============================================================
|
|
363
|
+
|
|
364
|
+
async function checkESLint() {
|
|
365
|
+
log.section('ESLint Analysis');
|
|
366
|
+
|
|
367
|
+
const eslintCmd = options.fix ? `${runCmd} lint --fix` : `${runCmd} lint`;
|
|
368
|
+
|
|
369
|
+
const result = runCommand(eslintCmd, ROOT_DIR, true);
|
|
370
|
+
|
|
371
|
+
if (result.success) {
|
|
372
|
+
log.success('No ESLint errors found');
|
|
373
|
+
addResult('ESLint', 'pass');
|
|
374
|
+
} else {
|
|
375
|
+
// Count errors and warnings from output
|
|
376
|
+
const errorMatch = result.output.match(/(\d+)\s+error/);
|
|
377
|
+
const warnMatch = result.output.match(/(\d+)\s+warning/);
|
|
378
|
+
const errors = errorMatch ? parseInt(errorMatch[1], 10) : 0;
|
|
379
|
+
const warnings = warnMatch ? parseInt(warnMatch[1], 10) : 0;
|
|
380
|
+
|
|
381
|
+
if (errors > 0) {
|
|
382
|
+
log.error(`Found ${errors} errors and ${warnings} warnings`);
|
|
383
|
+
addResult('ESLint', 'error', `${errors} errors, ${warnings} warnings`);
|
|
384
|
+
} else if (warnings > 0) {
|
|
385
|
+
log.warn(`Found ${warnings} warnings`);
|
|
386
|
+
addResult('ESLint', 'warn', `${warnings} warnings`);
|
|
387
|
+
} else {
|
|
388
|
+
log.success('No ESLint issues found');
|
|
389
|
+
addResult('ESLint', 'pass');
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (options.verbose && result.output) {
|
|
393
|
+
console.log(result.output.slice(0, 1000));
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function checkTypeScript() {
|
|
399
|
+
log.section('TypeScript Type Checking');
|
|
400
|
+
|
|
401
|
+
const result = runCommand(`${runCmd} typecheck`, ROOT_DIR, true);
|
|
402
|
+
|
|
403
|
+
if (result.success) {
|
|
404
|
+
log.success('No TypeScript errors found');
|
|
405
|
+
addResult('TypeScript', 'pass');
|
|
406
|
+
} else {
|
|
407
|
+
// Count errors from output
|
|
408
|
+
const errorCount = (result.output.match(/error TS\d+/g) || []).length;
|
|
409
|
+
|
|
410
|
+
if (errorCount > 0) {
|
|
411
|
+
log.error(`Found ${errorCount} TypeScript errors`);
|
|
412
|
+
addResult('TypeScript', 'error', `${errorCount} errors`);
|
|
413
|
+
|
|
414
|
+
if (options.verbose && result.output) {
|
|
415
|
+
// Show first few errors
|
|
416
|
+
const lines = result.output.split('\n').slice(0, 20);
|
|
417
|
+
console.log(lines.join('\n'));
|
|
418
|
+
}
|
|
419
|
+
} else {
|
|
420
|
+
log.success('No TypeScript errors found');
|
|
421
|
+
addResult('TypeScript', 'pass');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// ============================================================
|
|
427
|
+
// Dependency Checks
|
|
428
|
+
// ============================================================
|
|
429
|
+
|
|
430
|
+
async function checkOutdatedDependencies() {
|
|
431
|
+
log.section('Outdated Dependencies');
|
|
432
|
+
|
|
433
|
+
let cmd;
|
|
434
|
+
if (pm === 'pnpm') {
|
|
435
|
+
cmd = 'pnpm outdated --format json';
|
|
436
|
+
} else if (pm === 'yarn') {
|
|
437
|
+
cmd = 'yarn outdated --json';
|
|
438
|
+
} else {
|
|
439
|
+
cmd = 'npm outdated --json';
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const result = runCommand(cmd, ROOT_DIR, true);
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
let outdatedCount = 0;
|
|
446
|
+
let majorUpdates = 0;
|
|
447
|
+
|
|
448
|
+
if (pm === 'pnpm') {
|
|
449
|
+
const lines = result.output.split('\n').filter((l) => l.trim());
|
|
450
|
+
for (const line of lines) {
|
|
451
|
+
try {
|
|
452
|
+
const data = JSON.parse(line);
|
|
453
|
+
if (Array.isArray(data)) {
|
|
454
|
+
outdatedCount = data.length;
|
|
455
|
+
majorUpdates = data.filter((d) => {
|
|
456
|
+
const current = d.current?.split('.')[0];
|
|
457
|
+
const latest = d.latest?.split('.')[0];
|
|
458
|
+
return current && latest && current !== latest;
|
|
459
|
+
}).length;
|
|
460
|
+
}
|
|
461
|
+
} catch {
|
|
462
|
+
// Skip non-JSON lines
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} else {
|
|
466
|
+
const data = JSON.parse(result.output || '{}');
|
|
467
|
+
outdatedCount = Object.keys(data).length;
|
|
468
|
+
majorUpdates = Object.values(data).filter((d) => {
|
|
469
|
+
const current = d.current?.split('.')[0];
|
|
470
|
+
const latest = d.latest?.split('.')[0];
|
|
471
|
+
return current && latest && current !== latest;
|
|
472
|
+
}).length;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (majorUpdates > 0) {
|
|
476
|
+
log.warn(`${outdatedCount} outdated packages (${majorUpdates} major updates available)`);
|
|
477
|
+
addResult('Outdated Dependencies', 'warn', `${outdatedCount} outdated, ${majorUpdates} major`);
|
|
478
|
+
} else if (outdatedCount > 0) {
|
|
479
|
+
log.info(`${outdatedCount} packages have minor/patch updates available`);
|
|
480
|
+
addResult('Outdated Dependencies', 'pass', `${outdatedCount} minor updates`);
|
|
481
|
+
} else {
|
|
482
|
+
log.success('All dependencies are up to date');
|
|
483
|
+
addResult('Outdated Dependencies', 'pass');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (options.verbose) {
|
|
487
|
+
log.dim(`Run '${pm} outdated' for details`);
|
|
488
|
+
}
|
|
489
|
+
} catch {
|
|
490
|
+
log.success('All dependencies are up to date');
|
|
491
|
+
addResult('Outdated Dependencies', 'pass');
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
async function checkUnusedDependencies() {
|
|
496
|
+
log.section('Unused Dependencies');
|
|
497
|
+
|
|
498
|
+
// Check if depcheck is available
|
|
499
|
+
if (!commandExists('depcheck')) {
|
|
500
|
+
log.dim('depcheck not installed, skipping unused dependency check');
|
|
501
|
+
log.dim(`Install with: npm install -g depcheck`);
|
|
502
|
+
addResult('Unused Dependencies', 'warn', 'depcheck not installed');
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const apps = getApps();
|
|
507
|
+
const packages = getPackages();
|
|
508
|
+
let totalUnused = 0;
|
|
509
|
+
|
|
510
|
+
const checkDirs = [
|
|
511
|
+
...apps.map((a) => ({ name: a, path: path.join(APPS_DIR, a) })),
|
|
512
|
+
...packages.map((p) => ({ name: p, path: path.join(PACKAGES_DIR, p) })),
|
|
513
|
+
];
|
|
514
|
+
|
|
515
|
+
for (const { name, path: dirPath } of checkDirs) {
|
|
516
|
+
if (!fs.existsSync(path.join(dirPath, 'package.json'))) continue;
|
|
517
|
+
|
|
518
|
+
const result = runCommand(`depcheck --json`, dirPath, true);
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
const data = JSON.parse(result.output || '{}');
|
|
522
|
+
const unused = [...(data.dependencies || []), ...(data.devDependencies || [])];
|
|
523
|
+
|
|
524
|
+
if (unused.length > 0) {
|
|
525
|
+
totalUnused += unused.length;
|
|
526
|
+
if (options.verbose) {
|
|
527
|
+
log.dim(` ${name}: ${unused.join(', ')}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
} catch {
|
|
531
|
+
// Skip parse errors
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (totalUnused > 0) {
|
|
536
|
+
log.warn(`Found ${totalUnused} potentially unused dependencies`);
|
|
537
|
+
addResult('Unused Dependencies', 'warn', `${totalUnused} unused`);
|
|
538
|
+
} else {
|
|
539
|
+
log.success('No unused dependencies detected');
|
|
540
|
+
addResult('Unused Dependencies', 'pass');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// ============================================================
|
|
545
|
+
// License Check
|
|
546
|
+
// ============================================================
|
|
547
|
+
|
|
548
|
+
async function checkLicenses() {
|
|
549
|
+
log.section('License Compliance');
|
|
550
|
+
|
|
551
|
+
// Check if license-checker is available
|
|
552
|
+
if (!commandExists('license-checker')) {
|
|
553
|
+
log.dim('license-checker not installed, skipping license check');
|
|
554
|
+
log.dim(`Install with: npm install -g license-checker`);
|
|
555
|
+
addResult('License Compliance', 'warn', 'license-checker not installed');
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const PROBLEMATIC_LICENSES = ['GPL', 'AGPL', 'LGPL', 'SSPL', 'BUSL', 'Commons Clause'];
|
|
560
|
+
const UNKNOWN_LICENSE = 'UNKNOWN';
|
|
561
|
+
|
|
562
|
+
const result = runCommand('license-checker --json --production', ROOT_DIR, true);
|
|
563
|
+
|
|
564
|
+
try {
|
|
565
|
+
const data = JSON.parse(result.output || '{}');
|
|
566
|
+
const packages = Object.entries(data);
|
|
567
|
+
const problematic = [];
|
|
568
|
+
const unknown = [];
|
|
569
|
+
|
|
570
|
+
for (const [pkg, info] of packages) {
|
|
571
|
+
const license = info.licenses || UNKNOWN_LICENSE;
|
|
572
|
+
const licenseStr = Array.isArray(license) ? license.join(', ') : license;
|
|
573
|
+
|
|
574
|
+
if (licenseStr === UNKNOWN_LICENSE) {
|
|
575
|
+
unknown.push(pkg);
|
|
576
|
+
} else if (PROBLEMATIC_LICENSES.some((l) => licenseStr.toUpperCase().includes(l))) {
|
|
577
|
+
problematic.push({ pkg, license: licenseStr });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (problematic.length > 0) {
|
|
582
|
+
log.warn(`Found ${problematic.length} packages with restrictive licenses`);
|
|
583
|
+
if (options.verbose) {
|
|
584
|
+
for (const { pkg, license } of problematic.slice(0, 5)) {
|
|
585
|
+
log.dim(` ${pkg}: ${license}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
addResult('License Compliance', 'warn', `${problematic.length} restrictive licenses`);
|
|
589
|
+
} else if (unknown.length > 5) {
|
|
590
|
+
log.warn(`Found ${unknown.length} packages with unknown licenses`);
|
|
591
|
+
addResult('License Compliance', 'warn', `${unknown.length} unknown licenses`);
|
|
592
|
+
} else {
|
|
593
|
+
log.success('All licenses are compliant');
|
|
594
|
+
addResult('License Compliance', 'pass');
|
|
595
|
+
}
|
|
596
|
+
} catch {
|
|
597
|
+
log.warn('Could not check licenses');
|
|
598
|
+
addResult('License Compliance', 'warn', 'Check failed');
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ============================================================
|
|
603
|
+
// Main
|
|
604
|
+
// ============================================================
|
|
605
|
+
|
|
606
|
+
function showHelp() {
|
|
607
|
+
console.log(`
|
|
608
|
+
${colors.bold}Usage:${colors.reset} pnpm audit [options]
|
|
609
|
+
|
|
610
|
+
${colors.bold}Options:${colors.reset}
|
|
611
|
+
-a, --all Run all checks
|
|
612
|
+
-s, --security Run security checks only (vulnerabilities, secrets)
|
|
613
|
+
-q, --quality Run code quality checks only (ESLint, TypeScript)
|
|
614
|
+
-d, --deps Run dependency checks only (outdated, unused, licenses)
|
|
615
|
+
--secrets Run secrets detection only
|
|
616
|
+
--fix Attempt to fix issues (ESLint)
|
|
617
|
+
-v, --verbose Show detailed output
|
|
618
|
+
--app=<name> Audit specific app only
|
|
619
|
+
-h, --help Show this help message
|
|
620
|
+
|
|
621
|
+
${colors.bold}Examples:${colors.reset}
|
|
622
|
+
pnpm audit # Run all checks
|
|
623
|
+
pnpm audit --security # Security checks only
|
|
624
|
+
pnpm audit --quality --fix # Code quality with auto-fix
|
|
625
|
+
pnpm audit --app=web # Audit specific app
|
|
626
|
+
`);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function printSummary() {
|
|
630
|
+
log.title('📊 Audit Summary');
|
|
631
|
+
|
|
632
|
+
const statusIcon = {
|
|
633
|
+
pass: `${colors.green}✓${colors.reset}`,
|
|
634
|
+
warn: `${colors.yellow}!${colors.reset}`,
|
|
635
|
+
error: `${colors.red}✗${colors.reset}`,
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
for (const check of results.checks) {
|
|
639
|
+
const icon = statusIcon[check.status];
|
|
640
|
+
const msg = check.message ? ` (${check.message})` : '';
|
|
641
|
+
console.log(` ${icon} ${check.name}${colors.dim}${msg}${colors.reset}`);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
console.log('');
|
|
645
|
+
console.log(
|
|
646
|
+
`${colors.bold}Results:${colors.reset} ` +
|
|
647
|
+
`${colors.green}${results.passed} passed${colors.reset}, ` +
|
|
648
|
+
`${colors.yellow}${results.warnings} warnings${colors.reset}, ` +
|
|
649
|
+
`${colors.red}${results.errors} errors${colors.reset}`
|
|
650
|
+
);
|
|
651
|
+
console.log('');
|
|
652
|
+
|
|
653
|
+
if (results.errors > 0) {
|
|
654
|
+
process.exit(1);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function main() {
|
|
659
|
+
if (options.help) {
|
|
660
|
+
showHelp();
|
|
661
|
+
process.exit(0);
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
log.title('🔍 Code Audit');
|
|
665
|
+
log.info(`Package manager: ${pm}`);
|
|
666
|
+
log.info(`Root directory: ${ROOT_DIR}`);
|
|
667
|
+
|
|
668
|
+
if (options.app) {
|
|
669
|
+
log.info(`Auditing app: ${options.app}`);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Security checks
|
|
673
|
+
if (options.security) {
|
|
674
|
+
await checkDependencyVulnerabilities();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (options.secrets) {
|
|
678
|
+
await checkSecrets();
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Code quality checks
|
|
682
|
+
if (options.quality) {
|
|
683
|
+
await checkESLint();
|
|
684
|
+
await checkTypeScript();
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Dependency checks
|
|
688
|
+
if (options.deps) {
|
|
689
|
+
await checkOutdatedDependencies();
|
|
690
|
+
await checkUnusedDependencies();
|
|
691
|
+
await checkLicenses();
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
printSummary();
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
main().catch((error) => {
|
|
698
|
+
log.error(error.message);
|
|
699
|
+
process.exit(1);
|
|
700
|
+
});
|