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.
- package/README.md +113 -0
- package/bin/swynx-lite +3 -0
- package/package.json +47 -0
- package/src/clean.mjs +280 -0
- package/src/cli.mjs +264 -0
- package/src/config.mjs +121 -0
- package/src/output/console.mjs +298 -0
- package/src/output/json.mjs +76 -0
- package/src/output/progress.mjs +57 -0
- package/src/scan.mjs +143 -0
- package/src/security.mjs +62 -0
- package/src/shared/fixer/barrel-cleaner.mjs +192 -0
- package/src/shared/fixer/import-cleaner.mjs +237 -0
- package/src/shared/fixer/quarantine.mjs +218 -0
- package/src/shared/scanner/analysers/buildSystems.mjs +647 -0
- package/src/shared/scanner/analysers/configParsers.mjs +1086 -0
- package/src/shared/scanner/analysers/deadcode.mjs +6194 -0
- package/src/shared/scanner/analysers/entryPointDetector.mjs +634 -0
- package/src/shared/scanner/analysers/generatedCode.mjs +297 -0
- package/src/shared/scanner/analysers/imports.mjs +60 -0
- package/src/shared/scanner/discovery.mjs +240 -0
- package/src/shared/scanner/parse-worker.mjs +82 -0
- package/src/shared/scanner/parsers/assets.mjs +44 -0
- package/src/shared/scanner/parsers/csharp.mjs +400 -0
- package/src/shared/scanner/parsers/css.mjs +60 -0
- package/src/shared/scanner/parsers/go.mjs +445 -0
- package/src/shared/scanner/parsers/java.mjs +364 -0
- package/src/shared/scanner/parsers/javascript.mjs +823 -0
- package/src/shared/scanner/parsers/kotlin.mjs +350 -0
- package/src/shared/scanner/parsers/python.mjs +497 -0
- package/src/shared/scanner/parsers/registry.mjs +233 -0
- package/src/shared/scanner/parsers/rust.mjs +427 -0
- package/src/shared/scanner/scan-dead-code.mjs +316 -0
- package/src/shared/security/patterns.mjs +349 -0
- package/src/shared/security/proximity.mjs +84 -0
- 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
|
+
}
|