swynx-lite 1.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.
Files changed (36) hide show
  1. package/README.md +113 -0
  2. package/bin/swynx-lite +3 -0
  3. package/package.json +47 -0
  4. package/src/clean.mjs +280 -0
  5. package/src/cli.mjs +264 -0
  6. package/src/config.mjs +121 -0
  7. package/src/output/console.mjs +298 -0
  8. package/src/output/json.mjs +76 -0
  9. package/src/output/progress.mjs +57 -0
  10. package/src/scan.mjs +143 -0
  11. package/src/security.mjs +62 -0
  12. package/src/shared/fixer/barrel-cleaner.mjs +192 -0
  13. package/src/shared/fixer/import-cleaner.mjs +237 -0
  14. package/src/shared/fixer/quarantine.mjs +218 -0
  15. package/src/shared/scanner/analysers/buildSystems.mjs +647 -0
  16. package/src/shared/scanner/analysers/configParsers.mjs +1086 -0
  17. package/src/shared/scanner/analysers/deadcode.mjs +6194 -0
  18. package/src/shared/scanner/analysers/entryPointDetector.mjs +634 -0
  19. package/src/shared/scanner/analysers/generatedCode.mjs +297 -0
  20. package/src/shared/scanner/analysers/imports.mjs +60 -0
  21. package/src/shared/scanner/discovery.mjs +240 -0
  22. package/src/shared/scanner/parse-worker.mjs +82 -0
  23. package/src/shared/scanner/parsers/assets.mjs +44 -0
  24. package/src/shared/scanner/parsers/csharp.mjs +400 -0
  25. package/src/shared/scanner/parsers/css.mjs +60 -0
  26. package/src/shared/scanner/parsers/go.mjs +445 -0
  27. package/src/shared/scanner/parsers/java.mjs +364 -0
  28. package/src/shared/scanner/parsers/javascript.mjs +823 -0
  29. package/src/shared/scanner/parsers/kotlin.mjs +350 -0
  30. package/src/shared/scanner/parsers/python.mjs +497 -0
  31. package/src/shared/scanner/parsers/registry.mjs +233 -0
  32. package/src/shared/scanner/parsers/rust.mjs +427 -0
  33. package/src/shared/scanner/scan-dead-code.mjs +316 -0
  34. package/src/shared/security/patterns.mjs +349 -0
  35. package/src/shared/security/proximity.mjs +84 -0
  36. package/src/shared/security/scanner.mjs +269 -0
@@ -0,0 +1,84 @@
1
+ // src/security/proximity.mjs
2
+ // Path proximity detection for security-critical directories
3
+
4
+ const PROXIMITY_PATTERNS = [
5
+ // Authentication
6
+ { pattern: /[/\\]auth[/\\]/i, category: 'authentication', boost: 'CRITICAL' },
7
+ { pattern: /[/\\]login[/\\]/i, category: 'authentication', boost: 'CRITICAL' },
8
+ { pattern: /[/\\]oauth[/\\]/i, category: 'authentication', boost: 'CRITICAL' },
9
+ { pattern: /[/\\]sso[/\\]/i, category: 'authentication', boost: 'CRITICAL' },
10
+
11
+ // Cryptography
12
+ { pattern: /[/\\]crypto[/\\]/i, category: 'cryptography', boost: 'CRITICAL' },
13
+ { pattern: /[/\\]encryption[/\\]/i, category: 'cryptography', boost: 'CRITICAL' },
14
+
15
+ // Sandbox / isolation
16
+ { pattern: /[/\\]sandbox[/\\]/i, category: 'sandbox', boost: 'CRITICAL' },
17
+ { pattern: /[/\\]task-runner[/\\]/i, category: 'sandbox', boost: 'CRITICAL' },
18
+
19
+ // Webhooks
20
+ { pattern: /[/\\]webhook[s]?[/\\]/i, category: 'webhooks', boost: 'HIGH' },
21
+
22
+ // API
23
+ { pattern: /[/\\]api[/\\]/i, category: 'api', boost: 'MEDIUM' },
24
+ { pattern: /[/\\]graphql[/\\]/i, category: 'api', boost: 'MEDIUM' },
25
+
26
+ // Admin
27
+ { pattern: /[/\\]admin[/\\]/i, category: 'admin', boost: 'HIGH' },
28
+
29
+ // Payment / billing
30
+ { pattern: /[/\\]payment[s]?[/\\]/i, category: 'payment', boost: 'CRITICAL' },
31
+ { pattern: /[/\\]billing[/\\]/i, category: 'payment', boost: 'HIGH' },
32
+ { pattern: /[/\\]checkout[/\\]/i, category: 'payment', boost: 'HIGH' },
33
+
34
+ // Middleware
35
+ { pattern: /[/\\]middleware[/\\]/i, category: 'middleware', boost: 'MEDIUM' },
36
+
37
+ // Access control
38
+ { pattern: /[/\\]rbac[/\\]/i, category: 'access-control', boost: 'CRITICAL' },
39
+ { pattern: /[/\\]acl[/\\]/i, category: 'access-control', boost: 'HIGH' },
40
+ { pattern: /[/\\]permissions?[/\\]/i, category: 'access-control', boost: 'HIGH' },
41
+
42
+ // Tokens / JWT
43
+ { pattern: /[/\\]jwt[/\\]/i, category: 'tokens', boost: 'CRITICAL' },
44
+ { pattern: /[/\\]tokens?[/\\]/i, category: 'tokens', boost: 'HIGH' },
45
+
46
+ // File upload
47
+ { pattern: /[/\\]upload[s]?[/\\]/i, category: 'file-upload', boost: 'HIGH' },
48
+
49
+ // Secrets
50
+ { pattern: /[/\\]secrets?[/\\]/i, category: 'secrets', boost: 'CRITICAL' }
51
+ ];
52
+
53
+ const BOOST_RANK = { CRITICAL: 3, HIGH: 2, MEDIUM: 1 };
54
+
55
+ /**
56
+ * Check if a file path is in a security-critical directory.
57
+ * Returns { isCritical, matches: [{category, boost}], highestBoost }
58
+ */
59
+ export function checkProximity(filePath) {
60
+ const matches = [];
61
+
62
+ for (const { pattern, category, boost } of PROXIMITY_PATTERNS) {
63
+ if (pattern.test(filePath)) {
64
+ matches.push({ category, boost });
65
+ }
66
+ }
67
+
68
+ if (matches.length === 0) {
69
+ return { isCritical: false, matches: [], highestBoost: null };
70
+ }
71
+
72
+ let highestBoost = matches[0].boost;
73
+ for (let i = 1; i < matches.length; i++) {
74
+ if (BOOST_RANK[matches[i].boost] > BOOST_RANK[highestBoost]) {
75
+ highestBoost = matches[i].boost;
76
+ }
77
+ }
78
+
79
+ return {
80
+ isCritical: highestBoost === 'CRITICAL',
81
+ matches,
82
+ highestBoost
83
+ };
84
+ }
@@ -0,0 +1,269 @@
1
+ // src/security/scanner.mjs
2
+ // Full-codebase security pattern scanner
3
+ // Scans ALL files for dangerous code patterns (CWE) and proximity to security-critical paths
4
+ // Flags each finding with whether it's in dead code or live code
5
+
6
+ import { extname } from 'path';
7
+ import { readFileSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { getPatternsForLanguage } from './patterns.mjs';
10
+ import { checkProximity } from './proximity.mjs';
11
+
12
+ const SEVERITY_RANK = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
13
+
14
+ /**
15
+ * Check if a line is a comment (basic heuristic)
16
+ */
17
+ function isCommentLine(line) {
18
+ const trimmed = line.trimStart();
19
+ return (
20
+ trimmed.startsWith('//') ||
21
+ trimmed.startsWith('#') ||
22
+ trimmed.startsWith('*') ||
23
+ trimmed.startsWith('/*') ||
24
+ trimmed.startsWith('"""') ||
25
+ trimmed.startsWith("'''")
26
+ );
27
+ }
28
+
29
+ /**
30
+ * Check if a line contains example/documentation content rather than real code.
31
+ * Prevents false positives in marketing pages, docs, and code examples rendered as UI text.
32
+ */
33
+ function isExampleContent(line, prevLine) {
34
+ // Well-known example AWS credentials (explicitly not real secrets)
35
+ if (/AKIAIOSFODNN7EXAMPLE/.test(line)) return true;
36
+ // Lines with JSX className= are UI markup, not executable code
37
+ if (/className\s*=/.test(line)) return true;
38
+ // Text content on the line immediately after an opening JSX/HTML tag
39
+ // e.g. <div className="..."> followed by eval(userInput) at handler.ts:42
40
+ if (prevLine && /^\s*<[A-Za-z][A-Za-z0-9.]*\b.*[^/]>\s*$/.test(prevLine)) return true;
41
+ // JSON-LD structured data — standard React SEO pattern, not an XSS risk
42
+ if (prevLine && /application\/ld\+json/.test(prevLine)) return true;
43
+ return false;
44
+ }
45
+
46
+ /**
47
+ * Boost severity when file is in a security-critical directory
48
+ */
49
+ function boostSeverity(severity, proximityBoost) {
50
+ if (!proximityBoost) return severity;
51
+ const rank = SEVERITY_RANK[severity] || 1;
52
+ const boostRank = SEVERITY_RANK[proximityBoost] || 1;
53
+ // If proximity boost is higher, escalate one level (up to CRITICAL)
54
+ if (boostRank >= rank && severity !== 'CRITICAL') {
55
+ const levels = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'];
56
+ const idx = levels.indexOf(severity);
57
+ return levels[Math.min(idx + 1, 3)];
58
+ }
59
+ return severity;
60
+ }
61
+
62
+ /**
63
+ * Scan a single file's content for CWE patterns.
64
+ * Returns array of findings for this file.
65
+ */
66
+ function scanFileContent(filePath, content, proximity) {
67
+ const ext = extname(filePath);
68
+ const patterns = getPatternsForLanguage(ext);
69
+ if (patterns.length === 0) return [];
70
+
71
+ const lines = content.split('\n');
72
+ const fileFindings = [];
73
+
74
+ let inTemplateLiteral = false;
75
+
76
+ for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
77
+ const line = lines[lineIdx];
78
+ if (isCommentLine(line)) continue;
79
+
80
+ // Track multi-line template literal state — content inside
81
+ // multi-line backtick strings is string data, not executable code
82
+ const backticks = (line.match(/(?<!\\)`/g) || []).length;
83
+ if (backticks % 2 === 1) inTemplateLiteral = !inTemplateLiteral;
84
+
85
+ // Skip lines inside multi-line template literals (code examples, UI text)
86
+ if (inTemplateLiteral && backticks === 0) continue;
87
+
88
+ // Skip known example/documentation content
89
+ if (isExampleContent(line, lineIdx > 0 ? lines[lineIdx - 1] : '')) continue;
90
+
91
+ for (const pattern of patterns) {
92
+ if (pattern.pattern.test(line)) {
93
+ const severity = boostSeverity(pattern.severity, proximity.highestBoost);
94
+
95
+ fileFindings.push({
96
+ id: pattern.id,
97
+ cwe: pattern.cwe,
98
+ cweName: pattern.cweName,
99
+ severity,
100
+ originalSeverity: pattern.severity,
101
+ boosted: severity !== pattern.severity,
102
+ file: filePath,
103
+ line: lineIdx + 1,
104
+ lineContent: line.trim().substring(0, 120),
105
+ description: pattern.description,
106
+ risk: pattern.risk,
107
+ proximity: proximity.matches.length > 0 ? proximity : null
108
+ });
109
+
110
+ // Only match each pattern once per line
111
+ break;
112
+ }
113
+ }
114
+ }
115
+
116
+ return fileFindings;
117
+ }
118
+
119
+ /**
120
+ * Scan ALL code files for dangerous CWE patterns and proximity alerts.
121
+ * Each finding is flagged with isDead (true = dead code, false = live code).
122
+ *
123
+ * @param {Array} allCodeAnalysis - Combined JS + other language analysis
124
+ * @param {Set<string>} deadFileSet - Set of relative paths that are dead code
125
+ * @param {string} [projectPath] - Project root for re-reading content from disk
126
+ * @param {Function} [onProgress] - Optional progress callback
127
+ * @returns {{ summary, findings, byCWE, byFile, proximityAlerts }}
128
+ */
129
+ export function scanCodePatterns(allCodeAnalysis, deadFileSet, projectPath, onProgress) {
130
+ const findings = [];
131
+ const byCWE = {};
132
+ const byFile = {};
133
+ const proximityAlerts = [];
134
+
135
+ const severityCounts = { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0 };
136
+ let inDeadCode = 0;
137
+ let inLiveCode = 0;
138
+
139
+ for (let i = 0; i < allCodeAnalysis.length; i++) {
140
+ const file = allCodeAnalysis[i];
141
+ const filePath = file.file?.relativePath || file.file || file.relativePath || file.path || '';
142
+ if (!filePath) continue;
143
+
144
+ if (onProgress && i % 200 === 0) {
145
+ onProgress({
146
+ phase: 'Scanning security patterns',
147
+ detail: filePath.split('/').pop(),
148
+ current: i,
149
+ total: allCodeAnalysis.length
150
+ });
151
+ }
152
+
153
+ // Check proximity
154
+ const proximity = checkProximity(filePath);
155
+ if (proximity.isCritical || proximity.matches.length > 0) {
156
+ proximityAlerts.push({
157
+ file: filePath,
158
+ isDead: deadFileSet.has(filePath),
159
+ ...proximity
160
+ });
161
+ }
162
+
163
+ // Get content — try from analysis object first, then re-read from disk
164
+ let content = file.content || '';
165
+ if (!content && projectPath) {
166
+ try { content = readFileSync(join(projectPath, filePath), 'utf-8'); } catch { /* skip */ }
167
+ }
168
+ if (!content) continue;
169
+
170
+ const isDead = deadFileSet.has(filePath);
171
+ const fileFindings = scanFileContent(filePath, content, proximity);
172
+
173
+ for (const finding of fileFindings) {
174
+ finding.isDead = isDead;
175
+ finding.recommendation = isDead
176
+ ? 'File is dead code — safe to remove'
177
+ : 'Review and remediate';
178
+
179
+ findings.push(finding);
180
+ severityCounts[finding.severity]++;
181
+
182
+ if (isDead) inDeadCode++;
183
+ else inLiveCode++;
184
+
185
+ // Track by CWE
186
+ if (!byCWE[finding.cwe]) {
187
+ byCWE[finding.cwe] = { cwe: finding.cwe, name: finding.cweName, findings: [] };
188
+ }
189
+ byCWE[finding.cwe].findings.push(finding);
190
+ }
191
+
192
+ if (fileFindings.length > 0) {
193
+ byFile[filePath] = fileFindings;
194
+ }
195
+ }
196
+
197
+ // Sort findings by severity (CRITICAL first), then dead code last (live findings more urgent)
198
+ findings.sort((a, b) => {
199
+ const sevDiff = (SEVERITY_RANK[b.severity] || 0) - (SEVERITY_RANK[a.severity] || 0);
200
+ if (sevDiff !== 0) return sevDiff;
201
+ // Live code findings first (more urgent)
202
+ return (a.isDead ? 1 : 0) - (b.isDead ? 1 : 0);
203
+ });
204
+
205
+ return {
206
+ summary: {
207
+ total: findings.length,
208
+ inDeadCode,
209
+ inLiveCode,
210
+ critical: severityCounts.CRITICAL,
211
+ high: severityCounts.HIGH,
212
+ medium: severityCounts.MEDIUM,
213
+ low: severityCounts.LOW,
214
+ filesWithPatterns: Object.keys(byFile).length,
215
+ proximityAlerts: proximityAlerts.length,
216
+ cweCategories: Object.keys(byCWE).length,
217
+ // Backwards compat
218
+ totalFindings: findings.length
219
+ },
220
+ findings,
221
+ byCWE,
222
+ byFile,
223
+ proximityAlerts
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Backwards-compatible wrapper — scans only dead code files.
229
+ * Used when deadFileSet isn't available (e.g., from older callers).
230
+ */
231
+ export function scanDeadCodePatterns(deadCode, allCodeAnalysis, onProgress) {
232
+ const deadFiles = [
233
+ ...(deadCode.fullyDeadFiles || []),
234
+ ...(deadCode.partiallyDeadFiles || [])
235
+ ];
236
+ const deadFileSet = new Set(deadFiles.map(f => f.relativePath || f.file || f.path || ''));
237
+
238
+ // Scan only dead files for backwards compatibility
239
+ const deadAnalysis = allCodeAnalysis.filter(f => {
240
+ const fp = f.file?.relativePath || f.file || f.relativePath || f.path || '';
241
+ return deadFileSet.has(fp);
242
+ });
243
+
244
+ return scanCodePatterns(deadAnalysis, deadFileSet, null, onProgress);
245
+ }
246
+
247
+ /**
248
+ * Enrich a dead file object with its security pattern findings.
249
+ * Adds `securityPatterns` field for per-file drill-down.
250
+ */
251
+ export function enrichDeadFileWithPatterns(deadFile, byFile) {
252
+ const filePath = deadFile.relativePath || deadFile.file || deadFile.path || '';
253
+ const fileFindings = byFile[filePath];
254
+
255
+ if (!fileFindings || fileFindings.length === 0) {
256
+ return deadFile;
257
+ }
258
+
259
+ const proximity = checkProximity(filePath);
260
+
261
+ return {
262
+ ...deadFile,
263
+ securityPatterns: {
264
+ count: fileFindings.length,
265
+ findings: fileFindings,
266
+ proximity: proximity.matches.length > 0 ? proximity : null
267
+ }
268
+ };
269
+ }