@sun-asterisk/sunlint 1.3.30 → 1.3.31
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/package.json +1 -1
- package/rules/common/C006_function_naming/analyzer.js +277 -144
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +2 -1
- package/rules/security/S031_secure_session_cookies/analyzer.js +67 -112
- package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +0 -296
package/package.json
CHANGED
|
@@ -45,11 +45,92 @@ class SmartC006Analyzer {
|
|
|
45
45
|
// Side effect patterns indicate actions
|
|
46
46
|
ACTION: [/console\./, /fetch\(/, /\.send\(/, /\.post\(/]
|
|
47
47
|
};
|
|
48
|
+
|
|
49
|
+
// 📚 COMPREHENSIVE VERB LIST (from C006 rule specification)
|
|
50
|
+
this.acceptedVerbs = [
|
|
51
|
+
// Getters/Queries
|
|
52
|
+
'get', 'fetch', 'retrieve', 'find', 'search', 'query', 'load',
|
|
53
|
+
// Setters/Modifiers
|
|
54
|
+
'set', 'update', 'modify', 'change', 'edit', 'alter', 'transform',
|
|
55
|
+
// Creation
|
|
56
|
+
'create', 'build', 'make', 'generate', 'construct', 'produce',
|
|
57
|
+
// Deletion
|
|
58
|
+
'delete', 'remove', 'destroy', 'clean', 'clear', 'reset',
|
|
59
|
+
// Validation
|
|
60
|
+
'validate', 'verify', 'check', 'confirm', 'ensure', 'test', 'compare',
|
|
61
|
+
// Computation
|
|
62
|
+
'calculate', 'compute', 'parse', 'format', 'convert',
|
|
63
|
+
// Communication
|
|
64
|
+
'send', 'receive', 'transmit', 'broadcast', 'emit', 'publish',
|
|
65
|
+
// Collections
|
|
66
|
+
'map', 'filter', 'sort', 'group', 'merge', 'split', 'add', 'append', 'insert',
|
|
67
|
+
'push', 'pop', 'shift', 'splice', 'unshift', 'slice', 'concat', 'join',
|
|
68
|
+
// State checks (Boolean)
|
|
69
|
+
'is', 'has', 'can', 'should', 'will', 'does', 'contains', 'includes', 'exists',
|
|
70
|
+
// UI actions
|
|
71
|
+
'show', 'hide', 'display', 'render', 'draw', 'toggle', 'enable', 'disable',
|
|
72
|
+
'activate', 'deactivate', 'select', 'deselect', 'focus', 'blur',
|
|
73
|
+
// Lifecycle
|
|
74
|
+
'connect', 'disconnect', 'open', 'close', 'start', 'stop', 'run', 'refresh',
|
|
75
|
+
'initialize', 'init', 'setup', 'teardown', 'shutdown', 'restart', 'reload',
|
|
76
|
+
'restore', 'resume', 'pause', 'suspend',
|
|
77
|
+
// Event Handling
|
|
78
|
+
'on', 'trigger', 'fire', 'dispatch', 'invoke', 'call', 'emit', 'handle',
|
|
79
|
+
// Monitoring
|
|
80
|
+
'count', 'measure', 'monitor', 'watch', 'track', 'observe', 'log', 'record',
|
|
81
|
+
// Navigation
|
|
82
|
+
'navigate', 'redirect', 'route', 'go', 'move', 'scroll',
|
|
83
|
+
// Data Operations
|
|
84
|
+
'save', 'store', 'persist', 'cache', 'serialize', 'deserialize',
|
|
85
|
+
// Detection/Analysis
|
|
86
|
+
'detect', 'analyze', 'scan', 'inspect', 'evaluate', 'assess',
|
|
87
|
+
// Computation & Transformation
|
|
88
|
+
'calculate', 'compute', 'parse', 'format', 'convert',
|
|
89
|
+
'normalize', 'sanitize', 'encode', 'decode', 'encrypt', 'decrypt',
|
|
90
|
+
'compress', 'decompress', 'zip', 'unzip', 'tar', 'untar', // ← Add these
|
|
91
|
+
'stringify', 'objectify',
|
|
92
|
+
// State Management
|
|
93
|
+
'apply', 'revert', 'undo', 'redo', 'commit', 'rollback',
|
|
94
|
+
// Async Operations
|
|
95
|
+
'await', 'defer', 'debounce', 'throttle', 'delay',
|
|
96
|
+
// Error Handling
|
|
97
|
+
'throw', 'catch', 'handle', 'recover', 'retry',
|
|
98
|
+
// Copying/Cloning
|
|
99
|
+
'copy', 'clone', 'duplicate', 'replicate',
|
|
100
|
+
// Comparison
|
|
101
|
+
'equals', 'match', 'differ', 'compare',
|
|
102
|
+
// Registration
|
|
103
|
+
'register', 'unregister', 'subscribe', 'unsubscribe',
|
|
104
|
+
// Import/Export
|
|
105
|
+
'import', 'export', 'download', 'upload',
|
|
106
|
+
// Expansion/Collapse
|
|
107
|
+
'expand', 'collapse', 'maximize', 'minimize',
|
|
108
|
+
// Submission
|
|
109
|
+
'submit', 'cancel', 'abort', 'complete', 'finish',
|
|
110
|
+
// Preparation
|
|
111
|
+
'prepare', 'ready', 'preload', 'precache',
|
|
112
|
+
// Extraction
|
|
113
|
+
'extract', 'derive', 'obtain', 'acquire',
|
|
114
|
+
// Replacement
|
|
115
|
+
'replace', 'swap', 'substitute', 'override',
|
|
116
|
+
// Binding
|
|
117
|
+
'bind', 'unbind', 'attach', 'detach',
|
|
118
|
+
// Notification
|
|
119
|
+
'notify', 'alert', 'warn', 'inform',
|
|
120
|
+
// Execution
|
|
121
|
+
'execute', 'perform', 'process', 'run'
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
// ⚠️ VAGUE/GENERIC VERBS TO FLAG (should have more specific verbs)
|
|
125
|
+
this.vagueVerbs = [
|
|
126
|
+
'do', 'handle', 'manage', 'process', 'execute', 'perform',
|
|
127
|
+
'something', 'stuff', 'thing', 'work', 'data', 'item'
|
|
128
|
+
];
|
|
48
129
|
}
|
|
49
130
|
|
|
50
131
|
async analyze(files, language, config) {
|
|
51
132
|
const violations = [];
|
|
52
|
-
|
|
133
|
+
|
|
53
134
|
if (config.verbose) {
|
|
54
135
|
console.log(`🔧 [DEBUG] Starting Smart C006 Analysis on ${files.length} files...`);
|
|
55
136
|
}
|
|
@@ -69,7 +150,7 @@ class SmartC006Analyzer {
|
|
|
69
150
|
if (config.verbose) {
|
|
70
151
|
console.log(`🔧 [DEBUG] Smart C006 Analysis complete: ${violations.length} violations found`);
|
|
71
152
|
}
|
|
72
|
-
|
|
153
|
+
|
|
73
154
|
return violations;
|
|
74
155
|
}
|
|
75
156
|
|
|
@@ -80,10 +161,10 @@ class SmartC006Analyzer {
|
|
|
80
161
|
|
|
81
162
|
const violations = [];
|
|
82
163
|
const lines = content.split('\n');
|
|
83
|
-
|
|
164
|
+
|
|
84
165
|
// 🏗️ TIER 1: ARCHITECTURAL CONTEXT ANALYSIS
|
|
85
166
|
const architecturalContext = this.analyzeArchitecturalContext(filePath, content);
|
|
86
|
-
|
|
167
|
+
|
|
87
168
|
// Parse TypeScript/JavaScript code
|
|
88
169
|
const sourceFile = ts.createSourceFile(
|
|
89
170
|
filePath,
|
|
@@ -102,13 +183,13 @@ class SmartC006Analyzer {
|
|
|
102
183
|
architecturalContext,
|
|
103
184
|
content
|
|
104
185
|
);
|
|
105
|
-
|
|
186
|
+
|
|
106
187
|
if (analysis.isViolation && analysis.confidence >= this.confidenceThresholds.LOW) {
|
|
107
188
|
const namePosition = sourceFile.getLineAndCharacterOfPosition(node.name.getStart());
|
|
108
189
|
const line = namePosition.line + 1;
|
|
109
190
|
const column = namePosition.character + 1;
|
|
110
191
|
const lineText = lines[line - 1]?.trim() || '';
|
|
111
|
-
|
|
192
|
+
|
|
112
193
|
violations.push({
|
|
113
194
|
ruleId: this.ruleId,
|
|
114
195
|
file: filePath,
|
|
@@ -134,13 +215,13 @@ class SmartC006Analyzer {
|
|
|
134
215
|
architecturalContext,
|
|
135
216
|
content
|
|
136
217
|
);
|
|
137
|
-
|
|
218
|
+
|
|
138
219
|
if (analysis.isViolation && analysis.confidence >= this.confidenceThresholds.LOW) {
|
|
139
220
|
const namePosition = sourceFile.getLineAndCharacterOfPosition(node.name.getStart());
|
|
140
221
|
const line = namePosition.line + 1;
|
|
141
222
|
const column = namePosition.character + 1;
|
|
142
223
|
const lineText = lines[line - 1]?.trim() || '';
|
|
143
|
-
|
|
224
|
+
|
|
144
225
|
violations.push({
|
|
145
226
|
ruleId: this.ruleId,
|
|
146
227
|
file: filePath,
|
|
@@ -158,7 +239,7 @@ class SmartC006Analyzer {
|
|
|
158
239
|
}
|
|
159
240
|
|
|
160
241
|
// Analyze arrow functions assigned to variables
|
|
161
|
-
if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name) &&
|
|
242
|
+
if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name) &&
|
|
162
243
|
node.initializer && ts.isArrowFunction(node.initializer)) {
|
|
163
244
|
const analysis = this.smartAnalyzeFunctionName(
|
|
164
245
|
node.name.text,
|
|
@@ -167,13 +248,13 @@ class SmartC006Analyzer {
|
|
|
167
248
|
architecturalContext,
|
|
168
249
|
content
|
|
169
250
|
);
|
|
170
|
-
|
|
251
|
+
|
|
171
252
|
if (analysis.isViolation && analysis.confidence >= this.confidenceThresholds.LOW) {
|
|
172
253
|
const namePosition = sourceFile.getLineAndCharacterOfPosition(node.name.getStart());
|
|
173
254
|
const line = namePosition.line + 1;
|
|
174
255
|
const column = namePosition.character + 1;
|
|
175
256
|
const lineText = lines[line - 1]?.trim() || '';
|
|
176
|
-
|
|
257
|
+
|
|
177
258
|
violations.push({
|
|
178
259
|
ruleId: this.ruleId,
|
|
179
260
|
file: filePath,
|
|
@@ -189,7 +270,7 @@ class SmartC006Analyzer {
|
|
|
189
270
|
});
|
|
190
271
|
}
|
|
191
272
|
}
|
|
192
|
-
|
|
273
|
+
|
|
193
274
|
ts.forEachChild(node, visit);
|
|
194
275
|
};
|
|
195
276
|
|
|
@@ -204,7 +285,7 @@ class SmartC006Analyzer {
|
|
|
204
285
|
analyzeArchitecturalContext(filePath, content) {
|
|
205
286
|
const fileName = path.basename(filePath, path.extname(filePath)).toLowerCase();
|
|
206
287
|
const fileDir = path.dirname(filePath).toLowerCase();
|
|
207
|
-
|
|
288
|
+
|
|
208
289
|
// Detect architectural layer
|
|
209
290
|
let layer = 'UNKNOWN';
|
|
210
291
|
for (const [layerName, patterns] of Object.entries(this.architecturalLayers)) {
|
|
@@ -213,12 +294,12 @@ class SmartC006Analyzer {
|
|
|
213
294
|
break;
|
|
214
295
|
}
|
|
215
296
|
}
|
|
216
|
-
|
|
297
|
+
|
|
217
298
|
// Analyze imports for additional context
|
|
218
299
|
const imports = this.extractImports(content);
|
|
219
300
|
const isReactComponent = imports.some(imp => imp.includes('react')) || content.includes('JSX.Element');
|
|
220
301
|
const isTestFile = fileName.includes('test') || fileName.includes('spec');
|
|
221
|
-
|
|
302
|
+
|
|
222
303
|
return {
|
|
223
304
|
layer,
|
|
224
305
|
isReactComponent,
|
|
@@ -234,83 +315,96 @@ class SmartC006Analyzer {
|
|
|
234
315
|
*/
|
|
235
316
|
analyzeSemanticIntent(functionNode, sourceFile, content) {
|
|
236
317
|
if (!functionNode.body) return 'UNKNOWN';
|
|
237
|
-
|
|
318
|
+
|
|
238
319
|
const functionText = content.substring(
|
|
239
320
|
functionNode.body.getStart(),
|
|
240
321
|
functionNode.body.getEnd()
|
|
241
322
|
);
|
|
242
|
-
|
|
323
|
+
|
|
243
324
|
// Check for different semantic patterns
|
|
244
325
|
for (const [intent, patterns] of Object.entries(this.semanticPatterns)) {
|
|
245
326
|
if (patterns.some(pattern => pattern.test(functionText))) {
|
|
246
327
|
return intent;
|
|
247
328
|
}
|
|
248
329
|
}
|
|
249
|
-
|
|
330
|
+
|
|
250
331
|
return 'UNKNOWN';
|
|
251
332
|
}
|
|
252
333
|
|
|
253
334
|
/**
|
|
254
|
-
* 🎯
|
|
255
|
-
*
|
|
335
|
+
* 🎯 ENHANCED VERB DETECTION - FIXED VERSION
|
|
336
|
+
* Properly detects verbs including compound verbs
|
|
256
337
|
*/
|
|
257
|
-
|
|
258
|
-
//
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
'get', 'set', 'is', 'has', 'can', 'should', 'will', 'does',
|
|
275
|
-
'create', 'build', 'make', 'generate', 'construct', 'produce',
|
|
276
|
-
'update', 'modify', 'change', 'edit', 'alter', 'transform',
|
|
277
|
-
'delete', 'remove', 'destroy', 'clean', 'clear', 'reset',
|
|
278
|
-
'load', 'save', 'fetch', 'retrieve', 'find', 'search', 'query',
|
|
279
|
-
'validate', 'verify', 'check', 'confirm', 'ensure', 'test',
|
|
280
|
-
'calculate', 'compute', 'compare', 'parse', 'format', 'convert',
|
|
281
|
-
'send', 'receive', 'transmit', 'broadcast', 'emit', 'publish',
|
|
282
|
-
'map', 'filter', 'sort', 'group', 'merge', 'split',
|
|
283
|
-
'connect', 'disconnect', 'open', 'close', 'start', 'stop', 'run',
|
|
284
|
-
'show', 'hide', 'display', 'render', 'draw', 'paint', 'animate',
|
|
285
|
-
'add', 'append', 'insert', 'push', 'pop', 'shift', 'splice',
|
|
286
|
-
'count', 'measure', 'monitor', 'watch', 'track', 'observe',
|
|
287
|
-
'refresh', 'restore', 'reload', 'retry', 'resume', 'redirect',
|
|
288
|
-
'select', 'toggle', 'switch', 'enable', 'disable', 'activate',
|
|
289
|
-
'expand', 'collapse', 'scroll', 'navigate', 'submit', 'cancel',
|
|
290
|
-
'on', 'trigger', 'fire', 'dispatch', 'invoke', 'call'
|
|
291
|
-
];
|
|
292
|
-
|
|
293
|
-
// Strategy 2: Check if starts with known verb
|
|
294
|
-
const startsWithVerb = verbPrefixes.some(verb => {
|
|
295
|
-
const verbPattern = new RegExp(`^${verb}[A-Z]?`, 'i');
|
|
296
|
-
return verbPattern.test(functionName);
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
if (startsWithVerb) return true;
|
|
300
|
-
|
|
301
|
-
// Strategy 3: Common verb suffixes that indicate actions
|
|
302
|
-
const actionSuffixes = ['ize', 'ise', 'fy', 'ate', 'en'];
|
|
303
|
-
if (actionSuffixes.some(suffix => functionName.endsWith(suffix))) {
|
|
304
|
-
return true;
|
|
338
|
+
extractVerbFromName(functionName) {
|
|
339
|
+
// Convert to lowercase for comparison
|
|
340
|
+
const lowerName = functionName.toLowerCase();
|
|
341
|
+
|
|
342
|
+
// Find the longest matching verb from the accepted verbs list
|
|
343
|
+
let matchedVerb = null;
|
|
344
|
+
let maxLength = 0;
|
|
345
|
+
|
|
346
|
+
for (const verb of this.acceptedVerbs) {
|
|
347
|
+
// Check if function name starts with this verb
|
|
348
|
+
// Must be followed by uppercase letter, end of string, or be exact match
|
|
349
|
+
const verbPattern = new RegExp(`^${verb}([A-Z].*|$)`);
|
|
350
|
+
|
|
351
|
+
if (verbPattern.test(functionName) && verb.length > maxLength) {
|
|
352
|
+
matchedVerb = verb;
|
|
353
|
+
maxLength = verb.length;
|
|
354
|
+
}
|
|
305
355
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
356
|
+
|
|
357
|
+
return matchedVerb;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* 🎯 CHECK IF NAME FOLLOWS VERB-NOUN PATTERN
|
|
362
|
+
*/
|
|
363
|
+
isVerbNounPattern(functionName) {
|
|
364
|
+
// Strategy 1: Extract verb from accepted verbs list
|
|
365
|
+
const verb = this.extractVerbFromName(functionName);
|
|
366
|
+
|
|
367
|
+
if (verb) {
|
|
368
|
+
// Check if it's a vague verb (should be flagged even if technically a verb)
|
|
369
|
+
const isVagueVerb = this.vagueVerbs.some(vague => {
|
|
370
|
+
const vaguePattern = new RegExp(`^${vague}([A-Z].*|$)`, 'i');
|
|
371
|
+
return vaguePattern.test(functionName);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
if (isVagueVerb) {
|
|
375
|
+
return { isValid: false, verb: null, reason: 'vague_verb' };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Valid verb found
|
|
379
|
+
return { isValid: true, verb, reason: null };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Strategy 2: Check for verb-like patterns (morphology)
|
|
383
|
+
const verbSuffixes = ['ize', 'ise', 'fy', 'ate', 'en', 'ing', 'ed'];
|
|
384
|
+
for (const suffix of verbSuffixes) {
|
|
385
|
+
const suffixPattern = new RegExp(`^[a-z]+${suffix}([A-Z].*|$)`, 'i');
|
|
386
|
+
if (suffixPattern.test(functionName)) {
|
|
387
|
+
return { isValid: true, verb: 'morphological', reason: null };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Strategy 3: Check for verb prefixes (re-, un-, pre-, de-, dis-)
|
|
392
|
+
const verbPrefixes = ['re', 'un', 'pre', 'de', 'dis', 'over', 'under', 'out', 'up'];
|
|
393
|
+
for (const prefix of verbPrefixes) {
|
|
394
|
+
// Must be followed by a known verb or capital letter
|
|
395
|
+
const prefixPattern = new RegExp(`^${prefix}[A-Z]`, 'i');
|
|
396
|
+
if (prefixPattern.test(functionName)) {
|
|
397
|
+
// Check if the part after prefix is a known verb
|
|
398
|
+
const remainder = functionName.substring(prefix.length);
|
|
399
|
+
const remainderVerb = this.extractVerbFromName(remainder);
|
|
400
|
+
if (remainderVerb) {
|
|
401
|
+
return { isValid: true, verb: `${prefix}${remainderVerb}`, reason: null };
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// No verb pattern found
|
|
407
|
+
return { isValid: false, verb: null, reason: 'no_verb' };
|
|
314
408
|
}
|
|
315
409
|
|
|
316
410
|
/**
|
|
@@ -323,29 +417,29 @@ class SmartC006Analyzer {
|
|
|
323
417
|
requiredPatterns: [],
|
|
324
418
|
suggestions: []
|
|
325
419
|
};
|
|
326
|
-
|
|
420
|
+
|
|
327
421
|
// React components have different naming conventions
|
|
328
422
|
if (architecturalContext.isReactComponent) {
|
|
329
423
|
rules.allowedPatterns.push(/^[A-Z][a-zA-Z]*$/); // PascalCase components
|
|
330
424
|
rules.allowedPatterns.push(/^use[A-Z][a-zA-Z]*$/); // React hooks
|
|
331
425
|
rules.allowedPatterns.push(/^handle[A-Z][a-zA-Z]*$/); // Event handlers
|
|
332
426
|
}
|
|
333
|
-
|
|
427
|
+
|
|
334
428
|
// Test files have different patterns
|
|
335
429
|
if (architecturalContext.isTestFile) {
|
|
336
430
|
rules.allowedPatterns.push(/^(test|it|describe|should|expect)[A-Z]?/);
|
|
337
431
|
}
|
|
338
|
-
|
|
432
|
+
|
|
339
433
|
// Data layer functions often have CRUD patterns
|
|
340
434
|
if (architecturalContext.layer === 'DATA') {
|
|
341
|
-
rules.suggestions.push('Consider CRUD verbs: create, read, update, delete');
|
|
435
|
+
rules.suggestions.push('Consider CRUD verbs: create, read, update, delete, fetch, save');
|
|
342
436
|
}
|
|
343
|
-
|
|
437
|
+
|
|
344
438
|
// UI layer functions often have interaction verbs
|
|
345
439
|
if (architecturalContext.layer === 'UI') {
|
|
346
|
-
rules.suggestions.push('Consider UI verbs: show, hide, toggle, render, display');
|
|
440
|
+
rules.suggestions.push('Consider UI verbs: show, hide, toggle, render, display, handle');
|
|
347
441
|
}
|
|
348
|
-
|
|
442
|
+
|
|
349
443
|
return rules;
|
|
350
444
|
}
|
|
351
445
|
|
|
@@ -358,88 +452,103 @@ class SmartC006Analyzer {
|
|
|
358
452
|
if (this.isSpecialFunction(functionName, architecturalContext)) {
|
|
359
453
|
return { isViolation: false };
|
|
360
454
|
}
|
|
361
|
-
|
|
455
|
+
|
|
362
456
|
// Get semantic intent
|
|
363
457
|
const semanticIntent = this.analyzeSemanticIntent(functionNode, sourceFile, content);
|
|
364
|
-
|
|
458
|
+
|
|
365
459
|
// Get context-specific rules
|
|
366
460
|
const contextRules = this.getContextSpecificRules(architecturalContext, semanticIntent);
|
|
367
|
-
|
|
461
|
+
|
|
368
462
|
// Check if allowed by context-specific patterns
|
|
369
463
|
if (contextRules.allowedPatterns.some(pattern => pattern.test(functionName))) {
|
|
370
464
|
return { isViolation: false };
|
|
371
465
|
}
|
|
372
|
-
|
|
466
|
+
|
|
373
467
|
// Check if name follows verb-noun pattern
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
if (
|
|
468
|
+
const verbCheck = this.isVerbNounPattern(functionName);
|
|
469
|
+
|
|
470
|
+
if (verbCheck.isValid) {
|
|
377
471
|
return { isViolation: false };
|
|
378
472
|
}
|
|
379
|
-
|
|
473
|
+
|
|
380
474
|
// 🧮 CONFIDENCE CALCULATION
|
|
381
475
|
let confidence = 0.5; // Base confidence
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
476
|
+
let violationType = 'no_verb';
|
|
477
|
+
|
|
478
|
+
if (verbCheck.reason === 'vague_verb') {
|
|
479
|
+
violationType = 'vague_verb';
|
|
480
|
+
confidence = 0.9; // High confidence for vague verbs
|
|
481
|
+
} else if (verbCheck.reason === 'no_verb') {
|
|
482
|
+
violationType = 'no_verb';
|
|
483
|
+
|
|
484
|
+
// Check for obviously wrong patterns
|
|
485
|
+
const commonNounPatterns = [
|
|
486
|
+
/^(user|data|info|item|list|config|settings|options|params|args|props|state|value|result|response|request|error|message|event|callback|handler)([A-Z][a-z]*)*$/i,
|
|
487
|
+
/^[a-z]+$/, // Simple lowercase words
|
|
488
|
+
/^[a-z]+[A-Z][a-z]+$/ // Simple camelCase nouns
|
|
489
|
+
];
|
|
490
|
+
|
|
491
|
+
if (commonNounPatterns.some(pattern => pattern.test(functionName))) {
|
|
492
|
+
confidence = 0.8; // High confidence for obvious nouns
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Boost confidence for clearly generic patterns
|
|
496
|
+
const vagueFunctionNames = [
|
|
497
|
+
'something', 'stuff', 'thing', 'data', 'info', 'item',
|
|
498
|
+
'work', 'task', 'job', 'action', 'operation'
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
if (vagueFunctionNames.some(vague => functionName.toLowerCase().includes(vague.toLowerCase()))) {
|
|
502
|
+
confidence = 0.95; // Very high confidence
|
|
503
|
+
}
|
|
401
504
|
}
|
|
402
|
-
|
|
403
|
-
// Reduce confidence for complex names (might have hidden
|
|
404
|
-
if (functionName.length >
|
|
405
|
-
confidence -= 0.
|
|
505
|
+
|
|
506
|
+
// Reduce confidence for complex names (might have hidden patterns)
|
|
507
|
+
if (functionName.length > 20) {
|
|
508
|
+
confidence -= 0.15;
|
|
406
509
|
}
|
|
407
|
-
|
|
510
|
+
|
|
408
511
|
// Reduce confidence for utils/helpers (more flexible naming)
|
|
409
512
|
if (architecturalContext.layer === 'UTILS') {
|
|
410
513
|
confidence -= 0.2;
|
|
411
514
|
}
|
|
412
|
-
|
|
515
|
+
|
|
413
516
|
// Reduce confidence for test files
|
|
414
517
|
if (architecturalContext.isTestFile) {
|
|
415
518
|
confidence -= 0.3;
|
|
416
519
|
}
|
|
417
|
-
|
|
520
|
+
|
|
418
521
|
// Cap confidence
|
|
419
522
|
confidence = Math.min(Math.max(confidence, 0.1), 1.0);
|
|
420
|
-
|
|
523
|
+
|
|
421
524
|
// 💬 INTELLIGENT MESSAGING
|
|
422
525
|
const context = {
|
|
423
526
|
layer: architecturalContext.layer,
|
|
424
527
|
intent: semanticIntent,
|
|
425
528
|
isReactComponent: architecturalContext.isReactComponent,
|
|
426
|
-
isTestFile: architecturalContext.isTestFile
|
|
529
|
+
isTestFile: architecturalContext.isTestFile,
|
|
530
|
+
violationType
|
|
427
531
|
};
|
|
428
|
-
|
|
429
|
-
let reason =
|
|
430
|
-
let suggestion =
|
|
532
|
+
|
|
533
|
+
let reason = '';
|
|
534
|
+
let suggestion = '';
|
|
535
|
+
|
|
536
|
+
if (violationType === 'vague_verb') {
|
|
537
|
+
reason = `Function '${functionName}' uses vague verb. Use more specific action verbs (see C006 accepted verbs list)`;
|
|
538
|
+
suggestion = this.generateSmartSuggestion(functionName, semanticIntent, architecturalContext, true);
|
|
539
|
+
} else {
|
|
540
|
+
reason = `Function '${functionName}' must start with a verb (verb-noun pattern required by C006)`;
|
|
541
|
+
suggestion = this.generateSmartSuggestion(functionName, semanticIntent, architecturalContext, false);
|
|
542
|
+
}
|
|
431
543
|
|
|
432
544
|
if (architecturalContext.layer !== 'UNKNOWN') {
|
|
433
545
|
reason += ` (${architecturalContext.layer} layer)`;
|
|
434
546
|
}
|
|
435
547
|
|
|
436
|
-
// Add helpful note about accepted verbs
|
|
437
|
-
reason += `. Accepted verbs: get, set, create, update, delete, validate, check, compare, etc. See: https://coding-standards.sun-asterisk.vn/rules/rule/C006/`;
|
|
438
|
-
|
|
439
548
|
return {
|
|
440
549
|
isViolation: true,
|
|
441
550
|
reason,
|
|
442
|
-
type:
|
|
551
|
+
type: `smart_naming_violation_${violationType}`,
|
|
443
552
|
confidence,
|
|
444
553
|
suggestion,
|
|
445
554
|
context
|
|
@@ -449,27 +558,51 @@ class SmartC006Analyzer {
|
|
|
449
558
|
/**
|
|
450
559
|
* 💡 SMART SUGGESTION GENERATOR
|
|
451
560
|
*/
|
|
452
|
-
generateSmartSuggestion(functionName, semanticIntent, architecturalContext) {
|
|
453
|
-
|
|
454
|
-
|
|
561
|
+
generateSmartSuggestion(functionName, semanticIntent, architecturalContext, isVagueVerb) {
|
|
562
|
+
// If it's a vague verb, extract the noun part
|
|
563
|
+
let baseNoun = functionName;
|
|
564
|
+
|
|
565
|
+
if (isVagueVerb) {
|
|
566
|
+
// Try to extract the noun part after the vague verb
|
|
567
|
+
for (const vagueVerb of this.vagueVerbs) {
|
|
568
|
+
const pattern = new RegExp(`^${vagueVerb}([A-Z].*)$`, 'i');
|
|
569
|
+
const match = functionName.match(pattern);
|
|
570
|
+
if (match) {
|
|
571
|
+
baseNoun = match[1];
|
|
572
|
+
break;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Ensure first letter is capitalized for camelCase
|
|
578
|
+
baseNoun = baseNoun.charAt(0).toUpperCase() + baseNoun.slice(1);
|
|
579
|
+
|
|
580
|
+
const suggestions = [];
|
|
581
|
+
|
|
455
582
|
switch (semanticIntent) {
|
|
456
583
|
case 'GETTER':
|
|
457
|
-
|
|
584
|
+
suggestions.push(`get${baseNoun}()`, `fetch${baseNoun}()`, `retrieve${baseNoun}()`);
|
|
585
|
+
break;
|
|
458
586
|
case 'SETTER':
|
|
459
|
-
|
|
587
|
+
suggestions.push(`set${baseNoun}()`, `update${baseNoun}()`, `save${baseNoun}()`);
|
|
588
|
+
break;
|
|
460
589
|
case 'CHECKER':
|
|
461
|
-
|
|
590
|
+
suggestions.push(`is${baseNoun}()`, `has${baseNoun}()`, `can${baseNoun}()`, `should${baseNoun}()`);
|
|
591
|
+
break;
|
|
462
592
|
case 'ACTION':
|
|
463
|
-
|
|
593
|
+
suggestions.push(`process${baseNoun}()`, `handle${baseNoun}()`, `execute${baseNoun}()`);
|
|
594
|
+
break;
|
|
464
595
|
default:
|
|
465
596
|
if (architecturalContext.layer === 'DATA') {
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
|
|
597
|
+
suggestions.push(`fetch${baseNoun}()`, `create${baseNoun}()`, `save${baseNoun}()`, `load${baseNoun}()`);
|
|
598
|
+
} else if (architecturalContext.layer === 'UI') {
|
|
599
|
+
suggestions.push(`render${baseNoun}()`, `show${baseNoun}()`, `display${baseNoun}()`, `draw${baseNoun}()`);
|
|
600
|
+
} else {
|
|
601
|
+
suggestions.push(`get${baseNoun}()`, `set${baseNoun}()`, `process${baseNoun}()`, `handle${baseNoun}()`);
|
|
470
602
|
}
|
|
471
|
-
return `get${baseNoun}() or process${baseNoun}()`;
|
|
472
603
|
}
|
|
604
|
+
|
|
605
|
+
return suggestions.slice(0, 3).join(' or ');
|
|
473
606
|
}
|
|
474
607
|
|
|
475
608
|
/**
|
|
@@ -489,22 +622,22 @@ class SmartC006Analyzer {
|
|
|
489
622
|
if (specialFunctions.includes(name) || name.startsWith('_') || name.startsWith('$')) {
|
|
490
623
|
return true;
|
|
491
624
|
}
|
|
492
|
-
|
|
625
|
+
|
|
493
626
|
// React component names (PascalCase)
|
|
494
627
|
if (architecturalContext.isReactComponent && /^[A-Z][a-zA-Z]*$/.test(name)) {
|
|
495
628
|
return true;
|
|
496
629
|
}
|
|
497
|
-
|
|
630
|
+
|
|
498
631
|
// React hooks
|
|
499
632
|
if (name.startsWith('use') && /^use[A-Z]/.test(name)) {
|
|
500
633
|
return true;
|
|
501
634
|
}
|
|
502
|
-
|
|
635
|
+
|
|
503
636
|
// Test function names
|
|
504
637
|
if (architecturalContext.isTestFile && /^(test|it|describe|should|expect)/.test(name)) {
|
|
505
638
|
return true;
|
|
506
639
|
}
|
|
507
|
-
|
|
640
|
+
|
|
508
641
|
return false;
|
|
509
642
|
}
|
|
510
643
|
|
|
@@ -515,11 +648,11 @@ class SmartC006Analyzer {
|
|
|
515
648
|
const importRegex = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
|
|
516
649
|
const imports = [];
|
|
517
650
|
let match;
|
|
518
|
-
|
|
651
|
+
|
|
519
652
|
while ((match = importRegex.exec(content)) !== null) {
|
|
520
653
|
imports.push(match[1]);
|
|
521
654
|
}
|
|
522
|
-
|
|
655
|
+
|
|
523
656
|
return imports;
|
|
524
657
|
}
|
|
525
658
|
|
|
@@ -845,7 +845,8 @@ class C067SymbolBasedAnalyzer {
|
|
|
845
845
|
/\.config\.(ts|js)$/,
|
|
846
846
|
/\.env$/,
|
|
847
847
|
/\.env\./,
|
|
848
|
-
/constants
|
|
848
|
+
/constants?\.(ts|js)$/, // constants.ts, constant.ts
|
|
849
|
+
/\.constants?\.(ts|js)$/, // app.constants.ts, common.constant.ts
|
|
849
850
|
/settings\.(ts|js)$/,
|
|
850
851
|
/defaults\.(ts|js)$/,
|
|
851
852
|
];
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* S031 Main Analyzer - Set Secure flag for Session Cookies
|
|
3
|
-
*
|
|
4
|
-
* Fallback: Regex-based for all other cases
|
|
3
|
+
* Uses symbol-based analysis only (regex-based removed)
|
|
5
4
|
* Command: node cli.js --rule=S031 --input=examples/rule-test-fixtures/rules/S031_secure_session_cookies --engine=heuristic
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
7
|
const S031SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
|
|
9
|
-
const S031RegexBasedAnalyzer = require("./regex-based-analyzer.js");
|
|
10
8
|
|
|
11
9
|
class S031Analyzer {
|
|
12
10
|
constructor(options = {}) {
|
|
@@ -26,14 +24,7 @@ class S031Analyzer {
|
|
|
26
24
|
this.semanticEngine = options.semanticEngine || null;
|
|
27
25
|
this.verbose = options.verbose || false;
|
|
28
26
|
|
|
29
|
-
//
|
|
30
|
-
this.config = {
|
|
31
|
-
useSymbolBased: true, // Primary approach
|
|
32
|
-
fallbackToRegex: true, // Secondary approach
|
|
33
|
-
regexBasedOnly: false, // Can be set to true for pure mode
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// Initialize analyzers
|
|
27
|
+
// Initialize symbol analyzer only
|
|
37
28
|
try {
|
|
38
29
|
this.symbolAnalyzer = new S031SymbolBasedAnalyzer(this.semanticEngine);
|
|
39
30
|
if (process.env.SUNLINT_DEBUG) {
|
|
@@ -43,48 +34,39 @@ class S031Analyzer {
|
|
|
43
34
|
console.error(`🔧 [S031] Error creating symbol analyzer:`, error);
|
|
44
35
|
}
|
|
45
36
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
49
|
-
console.log(`🔧 [S031] Regex analyzer created successfully`);
|
|
50
|
-
}
|
|
51
|
-
} catch (error) {
|
|
52
|
-
console.error(`🔧 [S031] Error creating regex analyzer:`, error);
|
|
37
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
38
|
+
console.log(`🔧 [S031] Constructor completed`);
|
|
53
39
|
}
|
|
54
40
|
}
|
|
55
41
|
|
|
56
42
|
/**
|
|
57
43
|
* Initialize analyzer with semantic engine
|
|
58
|
-
|
|
59
44
|
*/
|
|
60
|
-
async initialize(semanticEngine) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
64
|
-
console.log(`🔧 [S031] Main analyzer initializing...`);
|
|
45
|
+
async initialize(semanticEngine = null) {
|
|
46
|
+
if (semanticEngine) {
|
|
47
|
+
this.semanticEngine = semanticEngine;
|
|
65
48
|
}
|
|
49
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
66
50
|
|
|
67
|
-
// Initialize
|
|
51
|
+
// Initialize symbol analyzer
|
|
68
52
|
if (this.symbolAnalyzer) {
|
|
69
53
|
await this.symbolAnalyzer.initialize?.(semanticEngine);
|
|
70
54
|
}
|
|
71
|
-
if (this.regexAnalyzer) {
|
|
72
|
-
await this.regexAnalyzer.initialize?.(semanticEngine);
|
|
73
|
-
}
|
|
74
55
|
|
|
75
|
-
//
|
|
76
|
-
if (this.
|
|
77
|
-
this.
|
|
56
|
+
// Ensure verbose flag is propagated
|
|
57
|
+
if (this.symbolAnalyzer) {
|
|
58
|
+
this.symbolAnalyzer.verbose = this.verbose;
|
|
78
59
|
}
|
|
79
60
|
|
|
80
|
-
if (
|
|
81
|
-
console.log(
|
|
61
|
+
if (this.verbose) {
|
|
62
|
+
console.log(
|
|
63
|
+
`🔧 [S031] Analyzer initialized - verbose: ${this.verbose}`
|
|
64
|
+
);
|
|
82
65
|
}
|
|
83
66
|
}
|
|
84
67
|
|
|
85
68
|
/**
|
|
86
69
|
* Single file analysis method for testing
|
|
87
|
-
|
|
88
70
|
*/
|
|
89
71
|
analyzeSingle(filePath, options = {}) {
|
|
90
72
|
if (process.env.SUNLINT_DEBUG) {
|
|
@@ -141,122 +123,95 @@ class S031Analyzer {
|
|
|
141
123
|
// Create a Map to track unique violations and prevent duplicates
|
|
142
124
|
const violationMap = new Map();
|
|
143
125
|
|
|
144
|
-
//
|
|
145
|
-
if (
|
|
146
|
-
this.config.useSymbolBased &&
|
|
147
|
-
this.semanticEngine?.project &&
|
|
148
|
-
this.semanticEngine?.initialized
|
|
149
|
-
) {
|
|
126
|
+
// Symbol-based analysis only
|
|
127
|
+
if (this.semanticEngine?.project && this.semanticEngine?.initialized) {
|
|
150
128
|
try {
|
|
151
129
|
if (process.env.SUNLINT_DEBUG) {
|
|
152
|
-
console.log(`🔧 [S031]
|
|
130
|
+
console.log(`🔧 [S031] Running symbol-based analysis...`);
|
|
153
131
|
}
|
|
154
132
|
const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
155
133
|
if (sourceFile) {
|
|
156
134
|
if (process.env.SUNLINT_DEBUG) {
|
|
157
|
-
console.log(
|
|
135
|
+
console.log(
|
|
136
|
+
`🔧 [S031] Source file found, analyzing with symbol-based...`
|
|
137
|
+
);
|
|
158
138
|
}
|
|
159
|
-
|
|
139
|
+
|
|
140
|
+
const violations = await this.symbolAnalyzer.analyze(
|
|
160
141
|
sourceFile,
|
|
161
142
|
filePath
|
|
162
143
|
);
|
|
163
144
|
|
|
164
|
-
// Add to
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const cookieName = this.extractCookieName(violation.message) || "";
|
|
168
|
-
const key = `cookie:${cookieName}:line:${violation.line}:secure`;
|
|
145
|
+
// Add violations to map to deduplicate and add filePath
|
|
146
|
+
violations.forEach((v) => {
|
|
147
|
+
const key = `${v.line}:${v.column}:${v.message}`;
|
|
169
148
|
if (!violationMap.has(key)) {
|
|
170
|
-
|
|
149
|
+
v.analysisStrategy = "symbol-based";
|
|
150
|
+
v.filePath = filePath;
|
|
151
|
+
v.file = filePath; // Also add 'file' for compatibility
|
|
152
|
+
violationMap.set(key, v);
|
|
171
153
|
}
|
|
172
154
|
});
|
|
173
155
|
|
|
174
156
|
if (process.env.SUNLINT_DEBUG) {
|
|
175
157
|
console.log(
|
|
176
|
-
|
|
158
|
+
`✅ [S031] Symbol-based analysis: ${violations.length} violations`
|
|
177
159
|
);
|
|
178
160
|
}
|
|
161
|
+
|
|
162
|
+
const finalViolations = Array.from(violationMap.values());
|
|
163
|
+
return finalViolations; // Return deduplicated violations with filePath
|
|
179
164
|
} else {
|
|
180
165
|
if (process.env.SUNLINT_DEBUG) {
|
|
181
|
-
console.log(
|
|
166
|
+
console.log(`⚠️ [S031] Source file not found in project`);
|
|
182
167
|
}
|
|
183
168
|
}
|
|
184
169
|
} catch (error) {
|
|
185
|
-
console.warn(
|
|
170
|
+
console.warn(`⚠️ [S031] Symbol analysis failed: ${error.message}`);
|
|
186
171
|
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
regexViolations.forEach((violation) => {
|
|
199
|
-
// Create a cookie-specific key to allow multiple violations for same cookie at different locations
|
|
200
|
-
const cookieName = this.extractCookieName(violation.message) || "";
|
|
201
|
-
const key = `cookie:${cookieName}:line:${violation.line}:secure`;
|
|
202
|
-
if (!violationMap.has(key)) {
|
|
203
|
-
violationMap.set(key, violation);
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
208
|
-
console.log(
|
|
209
|
-
`🔧 [S031] Regex analysis completed: ${regexViolations.length} violations`
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
} catch (error) {
|
|
213
|
-
console.warn(`⚠ [S031] Regex analysis failed:`, error.message);
|
|
172
|
+
} else {
|
|
173
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
174
|
+
console.log(`🔄 [S031] Symbol analysis conditions check:`);
|
|
175
|
+
console.log(` - semanticEngine: ${!!this.semanticEngine}`);
|
|
176
|
+
console.log(
|
|
177
|
+
` - semanticEngine.project: ${!!this.semanticEngine?.project}`
|
|
178
|
+
);
|
|
179
|
+
console.log(
|
|
180
|
+
` - semanticEngine.initialized: ${this.semanticEngine?.initialized}`
|
|
181
|
+
);
|
|
182
|
+
console.log(`🔄 [S031] Symbol analysis unavailable`);
|
|
214
183
|
}
|
|
215
184
|
}
|
|
216
185
|
|
|
217
|
-
// Convert Map values to array and add filePath to each violation
|
|
218
|
-
const finalViolations = Array.from(violationMap.values()).map(
|
|
219
|
-
(violation) => ({
|
|
220
|
-
...violation,
|
|
221
|
-
filePath: filePath,
|
|
222
|
-
file: filePath, // Also add 'file' for compatibility
|
|
223
|
-
})
|
|
224
|
-
);
|
|
225
|
-
|
|
226
186
|
if (process.env.SUNLINT_DEBUG) {
|
|
227
|
-
console.log(
|
|
228
|
-
`🔧 [S031] File analysis completed: ${finalViolations.length} unique violations`
|
|
229
|
-
);
|
|
187
|
+
console.log(`🔧 [S031] Analysis completed: ${violationMap.size} violations`);
|
|
230
188
|
}
|
|
231
|
-
|
|
232
|
-
return finalViolations;
|
|
189
|
+
return Array.from(violationMap.values());
|
|
233
190
|
}
|
|
234
191
|
|
|
235
192
|
/**
|
|
236
|
-
*
|
|
193
|
+
* Methods for compatibility with different engine invocation patterns
|
|
237
194
|
*/
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const match = message.match(
|
|
241
|
-
/Session cookie "([^"]+)"|Session cookie from "([^"]+)"/
|
|
242
|
-
);
|
|
243
|
-
return match ? match[1] || match[2] : "";
|
|
244
|
-
} catch (error) {
|
|
245
|
-
return "";
|
|
246
|
-
}
|
|
195
|
+
async analyzeFileWithSymbols(filePath, options = {}) {
|
|
196
|
+
return this.analyzeFile(filePath, options);
|
|
247
197
|
}
|
|
248
198
|
|
|
249
|
-
|
|
250
|
-
|
|
199
|
+
async analyzeWithSemantics(filePath, options = {}) {
|
|
200
|
+
return this.analyzeFile(filePath, options);
|
|
201
|
+
}
|
|
251
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Get analyzer metadata
|
|
252
205
|
*/
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
206
|
+
getMetadata() {
|
|
207
|
+
return {
|
|
208
|
+
rule: "S031",
|
|
209
|
+
name: "Set Secure flag for Session Cookies",
|
|
210
|
+
category: "security",
|
|
211
|
+
type: "symbol-based",
|
|
212
|
+
description:
|
|
213
|
+
"Uses symbol-based analysis to detect session cookies missing Secure flag",
|
|
214
|
+
};
|
|
260
215
|
}
|
|
261
216
|
}
|
|
262
217
|
|
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S031 Regex-Based Analyzer - Set Secure flag for Session Cookies
|
|
3
|
-
* Fallback analysis using regex patterns
|
|
4
|
-
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const fs = require("fs");
|
|
8
|
-
|
|
9
|
-
class S031RegexBasedAnalyzer {
|
|
10
|
-
constructor(semanticEngine = null) {
|
|
11
|
-
this.semanticEngine = semanticEngine;
|
|
12
|
-
this.ruleId = "S031";
|
|
13
|
-
this.category = "security";
|
|
14
|
-
|
|
15
|
-
// Session cookie indicators
|
|
16
|
-
this.sessionIndicators = [
|
|
17
|
-
"session",
|
|
18
|
-
"sessionid",
|
|
19
|
-
"sessid",
|
|
20
|
-
"jsessionid",
|
|
21
|
-
"phpsessid",
|
|
22
|
-
"asp.net_sessionid",
|
|
23
|
-
"connect.sid",
|
|
24
|
-
"auth",
|
|
25
|
-
"token",
|
|
26
|
-
"jwt",
|
|
27
|
-
"csrf",
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
// Regex patterns for cookie detection
|
|
31
|
-
this.cookiePatterns = [
|
|
32
|
-
// Express/Node.js patterns
|
|
33
|
-
/res\.cookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,([^)]+)\)/gi,
|
|
34
|
-
/response\.cookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,([^)]+)\)/gi,
|
|
35
|
-
/\.setCookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,([^)]+)\)/gi,
|
|
36
|
-
|
|
37
|
-
// Set-Cookie header patterns
|
|
38
|
-
/setHeader\s*\(\s*['"`]Set-Cookie['"`]\s*,\s*['"`]([^'"`]+)['"`]\s*\)/gi,
|
|
39
|
-
/writeHead\s*\([^,]*,\s*{[^}]*['"`]Set-Cookie['"`]\s*:\s*['"`]([^'"`]+)['"`]/gi,
|
|
40
|
-
|
|
41
|
-
// Document.cookie assignments
|
|
42
|
-
/document\.cookie\s*=\s*['"`]([^'"`]+)['"`]/gi,
|
|
43
|
-
|
|
44
|
-
// Session middleware patterns
|
|
45
|
-
/session\s*\(\s*{([^}]+)}/gi,
|
|
46
|
-
/\.use\s*\(\s*session\s*\(\s*{([^}]+)}/gi,
|
|
47
|
-
];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Initialize analyzer
|
|
52
|
-
|
|
53
|
-
*/
|
|
54
|
-
async initialize(semanticEngine) {
|
|
55
|
-
this.semanticEngine = semanticEngine;
|
|
56
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
57
|
-
console.log(`🔧 [S031] Regex-based analyzer initialized`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Check if file should be skipped (test files)
|
|
63
|
-
*/
|
|
64
|
-
shouldSkipFile(filePath) {
|
|
65
|
-
const testPatterns = [
|
|
66
|
-
/\.test\.(ts|tsx|js|jsx)$/,
|
|
67
|
-
/\.spec\.(ts|tsx|js|jsx)$/,
|
|
68
|
-
/__tests__\//,
|
|
69
|
-
/__mocks__\//,
|
|
70
|
-
/\/tests?\//,
|
|
71
|
-
/\/fixtures?\//,
|
|
72
|
-
];
|
|
73
|
-
return testPatterns.some((pattern) => pattern.test(filePath));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Analyze file content using regex patterns
|
|
78
|
-
|
|
79
|
-
*/
|
|
80
|
-
async analyze(filePath) {
|
|
81
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
82
|
-
console.log(`🔍 [S031] Regex-based analysis for: ${filePath}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Skip test files
|
|
86
|
-
if (this.shouldSkipFile(filePath)) {
|
|
87
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
88
|
-
console.log(`⏭ [S031] Skipping test file: ${filePath}`);
|
|
89
|
-
}
|
|
90
|
-
return [];
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
let content;
|
|
94
|
-
try {
|
|
95
|
-
content = fs.readFileSync(filePath, "utf8");
|
|
96
|
-
} catch (error) {
|
|
97
|
-
if (process.env.SUNLINT_DEBUG) {
|
|
98
|
-
console.error(`❌ [S031] File read error:`, error);
|
|
99
|
-
}
|
|
100
|
-
throw error;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const violations = [];
|
|
104
|
-
const lines = content.split("\n");
|
|
105
|
-
|
|
106
|
-
// Check each pattern
|
|
107
|
-
for (const pattern of this.cookiePatterns) {
|
|
108
|
-
this.checkPattern(pattern, content, lines, violations, filePath);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Check for custom cookie utilities (e.g., StorageUtils.setCookie)
|
|
112
|
-
this.checkCustomCookieUtilities(content, lines, violations, filePath);
|
|
113
|
-
|
|
114
|
-
return violations;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Check specific regex pattern for violations
|
|
119
|
-
|
|
120
|
-
*/
|
|
121
|
-
checkPattern(pattern, content, lines, violations, filePath) {
|
|
122
|
-
let match;
|
|
123
|
-
pattern.lastIndex = 0; // Reset regex state
|
|
124
|
-
|
|
125
|
-
while ((match = pattern.exec(content)) !== null) {
|
|
126
|
-
const matchText = match[0];
|
|
127
|
-
const cookieName = match[1] || "";
|
|
128
|
-
const cookieOptions = match[2] || match[1] || "";
|
|
129
|
-
|
|
130
|
-
// Check if this is a session cookie
|
|
131
|
-
if (!this.isSessionCookie(cookieName, matchText)) {
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Check if secure flag is present
|
|
136
|
-
if (!this.hasSecureFlag(cookieOptions, matchText)) {
|
|
137
|
-
const lineNumber = this.getLineNumber(content, match.index);
|
|
138
|
-
|
|
139
|
-
this.addViolation(
|
|
140
|
-
matchText,
|
|
141
|
-
lineNumber,
|
|
142
|
-
violations,
|
|
143
|
-
`Session cookie "${cookieName || "unknown"}" missing Secure flag`
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Check if cookie name or context indicates session cookie
|
|
151
|
-
|
|
152
|
-
*/
|
|
153
|
-
isSessionCookie(cookieName, matchText) {
|
|
154
|
-
const textToCheck = (cookieName + " " + matchText).toLowerCase();
|
|
155
|
-
return this.sessionIndicators.some((indicator) =>
|
|
156
|
-
textToCheck.includes(indicator.toLowerCase())
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Check if secure flag is present in cookie options
|
|
162
|
-
*/
|
|
163
|
-
hasSecureFlag(cookieOptions, fullMatch) {
|
|
164
|
-
const textToCheck = cookieOptions + " " + fullMatch;
|
|
165
|
-
|
|
166
|
-
// Check for secure config references (likely safe)
|
|
167
|
-
const secureConfigPatterns = [
|
|
168
|
-
/\bcookieConfig\b/i,
|
|
169
|
-
/\bsecureConfig\b/i,
|
|
170
|
-
/\bsafeConfig\b/i,
|
|
171
|
-
/\bdefaultConfig\b/i,
|
|
172
|
-
/\.\.\..*config/i, // spread operator with config
|
|
173
|
-
/config.*secure/i,
|
|
174
|
-
];
|
|
175
|
-
|
|
176
|
-
// If using a secure config reference, assume it's safe
|
|
177
|
-
if (secureConfigPatterns.some((pattern) => pattern.test(textToCheck))) {
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Check for various secure flag patterns
|
|
182
|
-
const securePatterns = [
|
|
183
|
-
/secure\s*:\s*true/i,
|
|
184
|
-
/secure\s*=\s*true/i,
|
|
185
|
-
/;\s*secure\s*[;\s]/i,
|
|
186
|
-
/;\s*secure$/i,
|
|
187
|
-
/['"`]\s*secure\s*['"`]/i,
|
|
188
|
-
/"secure"\s*:\s*true/i,
|
|
189
|
-
/'secure'\s*:\s*true/i,
|
|
190
|
-
/\bsecure\b/i, // Simple secure keyword
|
|
191
|
-
];
|
|
192
|
-
|
|
193
|
-
return securePatterns.some((pattern) => pattern.test(textToCheck));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Get line number from content position
|
|
198
|
-
|
|
199
|
-
*/
|
|
200
|
-
getLineNumber(content, position) {
|
|
201
|
-
const beforeMatch = content.substring(0, position);
|
|
202
|
-
return beforeMatch.split("\n").length;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Add violation to results
|
|
207
|
-
|
|
208
|
-
*/
|
|
209
|
-
addViolation(source, lineNumber, violations, message) {
|
|
210
|
-
violations.push({
|
|
211
|
-
ruleId: this.ruleId,
|
|
212
|
-
source: source.trim(),
|
|
213
|
-
category: this.category,
|
|
214
|
-
line: lineNumber,
|
|
215
|
-
column: 1,
|
|
216
|
-
message: `Insecure session cookie: ${message}`,
|
|
217
|
-
severity: "error",
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Check for custom cookie utility functions with variable names
|
|
223
|
-
* Handles cases like: StorageUtils.setCookie(STORAGE_KEY.ACCESS_TOKEN, value)
|
|
224
|
-
*/
|
|
225
|
-
checkCustomCookieUtilities(content, lines, violations, filePath) {
|
|
226
|
-
// Pattern to match custom cookie utilities with variable references
|
|
227
|
-
// Matches: Utils.setCookie(VARIABLE_NAME, value) or Utils.setCookie(VARIABLE_NAME, value, options)
|
|
228
|
-
const customCookiePattern = /(\w+\.setCookie)\s*\(\s*([A-Z_][A-Z0-9_.]*)\s*,\s*([^,)]+)(?:\s*,\s*([^)]*))?\s*\)/gi;
|
|
229
|
-
|
|
230
|
-
let match;
|
|
231
|
-
customCookiePattern.lastIndex = 0;
|
|
232
|
-
|
|
233
|
-
while ((match = customCookiePattern.exec(content)) !== null) {
|
|
234
|
-
const methodCall = match[1]; // e.g., "StorageUtils.setCookie"
|
|
235
|
-
const cookieNameVar = match[2]; // e.g., "STORAGE_KEY.ACCESS_TOKEN"
|
|
236
|
-
const cookieValue = match[3]; // e.g., "response.user?.access_token || ''"
|
|
237
|
-
const cookieOptions = match[4] || ""; // e.g., options object if present
|
|
238
|
-
const matchText = match[0];
|
|
239
|
-
|
|
240
|
-
// Check if this looks like a session cookie based on variable name or value
|
|
241
|
-
if (!this.isSessionCookieLikely(cookieNameVar, cookieValue, matchText)) {
|
|
242
|
-
continue;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Check if secure flag is present in options
|
|
246
|
-
if (!this.hasSecureFlag(cookieOptions, matchText)) {
|
|
247
|
-
const lineNumber = this.getLineNumber(content, match.index);
|
|
248
|
-
|
|
249
|
-
// Extract a friendly cookie name from the variable
|
|
250
|
-
const friendlyCookieName = this.extractFriendlyCookieName(cookieNameVar);
|
|
251
|
-
|
|
252
|
-
this.addViolation(
|
|
253
|
-
matchText,
|
|
254
|
-
lineNumber,
|
|
255
|
-
violations,
|
|
256
|
-
`Session cookie from "${friendlyCookieName}" missing Secure flag - add secure flag to options`
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Check if cookie variable name or value suggests it's a session cookie
|
|
264
|
-
*/
|
|
265
|
-
isSessionCookieLikely(varName, value, matchText) {
|
|
266
|
-
const textToCheck = (varName + " " + value + " " + matchText).toLowerCase();
|
|
267
|
-
|
|
268
|
-
// Check against session indicators
|
|
269
|
-
const isSession = this.sessionIndicators.some((indicator) =>
|
|
270
|
-
textToCheck.includes(indicator.toLowerCase())
|
|
271
|
-
);
|
|
272
|
-
|
|
273
|
-
// Also check for common patterns like ACCESS_TOKEN, REFRESH_TOKEN, etc.
|
|
274
|
-
const tokenPatterns = [
|
|
275
|
-
/access[_-]?token/i,
|
|
276
|
-
/refresh[_-]?token/i,
|
|
277
|
-
/auth[_-]?token/i,
|
|
278
|
-
/id[_-]?token/i,
|
|
279
|
-
/session/i,
|
|
280
|
-
];
|
|
281
|
-
|
|
282
|
-
return isSession || tokenPatterns.some(pattern => pattern.test(textToCheck));
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Extract a friendly cookie name from variable reference
|
|
287
|
-
* e.g., "STORAGE_KEY.ACCESS_TOKEN" -> "ACCESS_TOKEN"
|
|
288
|
-
*/
|
|
289
|
-
extractFriendlyCookieName(varName) {
|
|
290
|
-
// If it has a dot, take the last part
|
|
291
|
-
const parts = varName.split(".");
|
|
292
|
-
return parts[parts.length - 1] || varName;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
module.exports = S031RegexBasedAnalyzer;
|