@sun-asterisk/sunlint 1.3.16 → 1.3.17

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 (50) hide show
  1. package/config/rule-analysis-strategies.js +3 -3
  2. package/config/rules/enhanced-rules-registry.json +40 -20
  3. package/core/cli-action-handler.js +2 -2
  4. package/core/config-merger.js +28 -6
  5. package/core/constants/defaults.js +1 -1
  6. package/core/file-targeting-service.js +72 -4
  7. package/core/output-service.js +21 -4
  8. package/engines/heuristic-engine.js +5 -0
  9. package/package.json +1 -1
  10. package/rules/common/C002_no_duplicate_code/README.md +115 -0
  11. package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
  12. package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
  13. package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
  14. package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
  15. package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
  16. package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
  17. package/rules/common/C008/analyzer.js +40 -0
  18. package/rules/common/C008/config.json +20 -0
  19. package/rules/common/C008/ts-morph-analyzer.js +1067 -0
  20. package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
  21. package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
  22. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
  23. package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
  24. package/rules/common/C033_separate_service_repository/README.md +131 -20
  25. package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
  26. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
  27. package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
  28. package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
  29. package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
  30. package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
  31. package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
  32. package/rules/docs/C002_no_duplicate_code.md +276 -11
  33. package/rules/index.js +5 -1
  34. package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
  35. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
  36. package/rules/security/S010_no_insecure_encryption/README.md +78 -0
  37. package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
  38. package/rules/security/S013_tls_enforcement/README.md +51 -0
  39. package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
  40. package/rules/security/S013_tls_enforcement/config.json +41 -0
  41. package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
  42. package/rules/security/S014_tls_version_enforcement/README.md +354 -0
  43. package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
  44. package/rules/security/S014_tls_version_enforcement/config.json +56 -0
  45. package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
  46. package/rules/security/S055_content_type_validation/analyzer.js +121 -279
  47. package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
  48. package/rules/tests/C002_no_duplicate_code.test.js +111 -22
  49. package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
  50. package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +0 -296
@@ -0,0 +1,51 @@
1
+ ````markdown
2
+ # S013 - Enforce TLS for all connections
3
+
4
+ ## Overview
5
+
6
+ Rule S013 ensures that applications only create or call secure (HTTPS/TLS) endpoints. It detects insecure server bindings (http.createServer, express app.listen without HTTPS), hard-coded http:// URLs in client calls, and framework-specific misconfigurations that disable TLS.
7
+
8
+ ## Framework Support
9
+
10
+ - Express.js: detect `http.createServer`, `app.listen` used without TLS wrapper, `app.set('trust proxy', false)` combined with http
11
+ - Next.js: detect API routes calling `fetch('http://...')` or new Response created from http sources
12
+ - Nuxt.js: detect server config `server: { https: false }` or HTTP public assets
13
+ - NestJS: detect direct use of http server via `createServer` or `app.listen` without TLS
14
+
15
+ ## Security Impact
16
+
17
+ Serving over HTTP or making unencrypted client calls can expose credentials, session tokens, and sensitive data to passive observers. TLS must be enforced for all communication in production.
18
+
19
+ ## Examples
20
+
21
+ ❌ Violation (Express server using http):
22
+
23
+ ```js
24
+ const http = require("http");
25
+ const server = http.createServer(app);
26
+ server.listen(80);
27
+ ```
28
+
29
+ ❌ Violation (client fetch to http):
30
+
31
+ ```js
32
+ await fetch("http://internal-api.local/auth");
33
+ ```
34
+
35
+ ✅ Compliant (HTTPS enforced):
36
+
37
+ ```js
38
+ const https = require("https");
39
+ const fs = require("fs");
40
+ const server = https.createServer(
41
+ { key: fs.readFileSync("key"), cert: fs.readFileSync("cert") },
42
+ app
43
+ );
44
+ server.listen(443);
45
+ ```
46
+
47
+ ## Analysis Approach
48
+
49
+ - Symbol-based (primary): analyze AST for server creation calls, imports of `http`, usages of `fetch`/`axios`/`request` with `http://` literals, and framework-specific config objects.
50
+ - Regex-based (fallback): scan lines for `http.createServer`, `fetch('http://`, `app.listen(` with numeric port 80, `server: { https: false }`, etc.
51
+ ````
@@ -0,0 +1,99 @@
1
+ // Command: node cli.js --rule=S013 --input=examples/rule-test-fixtures/rules/S013_tls_enforcement --engine=heuristic
2
+
3
+ const S013SymbolBasedAnalyzer = require("./symbol-based-analyzer");
4
+
5
+ class S013Analyzer {
6
+ constructor(options = {}) {
7
+ this.ruleId = "S013";
8
+ this.semanticEngine = options.semanticEngine || null;
9
+ this.verbose = options.verbose || false;
10
+
11
+ try {
12
+ this.symbolAnalyzer = new S013SymbolBasedAnalyzer(this.semanticEngine);
13
+ } catch (e) {
14
+ console.warn(`⚠ [S013] Failed to create symbol analyzer: ${e.message}`);
15
+ }
16
+ }
17
+
18
+ async initialize(semanticEngine) {
19
+ this.semanticEngine = semanticEngine;
20
+ if (this.symbolAnalyzer && this.symbolAnalyzer.initialize) {
21
+ await this.symbolAnalyzer.initialize(semanticEngine);
22
+ }
23
+ }
24
+
25
+ analyzeSingle(filePath, options = {}) {
26
+ return this.analyze([filePath], "typescript", options);
27
+ }
28
+
29
+ async analyze(files, language, options = {}) {
30
+ const violations = [];
31
+ for (const filePath of files) {
32
+ try {
33
+ const vs = await this.analyzeFile(filePath, options);
34
+ violations.push(...vs);
35
+ } catch (e) {
36
+ console.warn(`⚠ [S013] Analysis error for ${filePath}: ${e.message}`);
37
+ }
38
+ }
39
+ return violations;
40
+ }
41
+
42
+ async analyzeFile(filePath, options = {}) {
43
+ const violationMap = new Map();
44
+
45
+ if (!this.symbolAnalyzer) {
46
+ return [];
47
+ }
48
+
49
+ try {
50
+ let sourceFile = null;
51
+ if (this.semanticEngine?.project) {
52
+ sourceFile = this.semanticEngine.project.getSourceFile(filePath);
53
+ }
54
+
55
+ if (!sourceFile) {
56
+ // create temporary ts-morph source file
57
+ const fs = require("fs");
58
+ const path = require("path");
59
+ const { Project } = require("ts-morph");
60
+ if (!fs.existsSync(filePath)) {
61
+ throw new Error(`File not found: ${filePath}`);
62
+ }
63
+ const content = fs.readFileSync(filePath, "utf8");
64
+ const tmp = new Project({
65
+ useInMemoryFileSystem: true,
66
+ compilerOptions: { allowJs: true },
67
+ });
68
+ sourceFile = tmp.createSourceFile(path.basename(filePath), content);
69
+ }
70
+
71
+ if (sourceFile) {
72
+ const symbolViolations = await this.symbolAnalyzer.analyze(
73
+ sourceFile,
74
+ filePath
75
+ );
76
+ symbolViolations.forEach((v) => {
77
+ const key = `${v.line}:${v.column}:${v.message}`;
78
+ if (!violationMap.has(key)) violationMap.set(key, v);
79
+ });
80
+ }
81
+ } catch (e) {
82
+ console.warn(`⚠ [S013] Symbol analysis failed: ${e.message}`);
83
+ }
84
+
85
+ return Array.from(violationMap.values()).map((v) => ({
86
+ ...v,
87
+ filePath,
88
+ file: filePath,
89
+ }));
90
+ }
91
+
92
+ cleanup() {
93
+ if (this.symbolAnalyzer?.cleanup) {
94
+ this.symbolAnalyzer.cleanup();
95
+ }
96
+ }
97
+ }
98
+
99
+ module.exports = S013Analyzer;
@@ -0,0 +1,41 @@
1
+ {
2
+ "id": "S013",
3
+ "name": "Enforce TLS for all connections",
4
+ "category": "security",
5
+ "description": "S013 - Ensure TLS (HTTPS) is always used for network connections and server bindings. Detects unsecured HTTP server creation, use of http:// endpoints in client calls, and framework-specific configurations that disable HTTPS.",
6
+ "severity": "error",
7
+ "enabled": true,
8
+ "semantic": {
9
+ "enabled": true,
10
+ "priority": "high",
11
+ "fallback": "heuristic"
12
+ },
13
+ "patterns": {
14
+ "include": ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx"],
15
+ "exclude": [
16
+ "**/*.test.js",
17
+ "**/*.test.ts",
18
+ "**/*.spec.js",
19
+ "**/*.spec.ts",
20
+ "**/node_modules/**",
21
+ "**/dist/**",
22
+ "**/build/**"
23
+ ]
24
+ },
25
+ "analysis": {
26
+ "approach": "symbol-based-primary",
27
+ "fallback": "regex-based",
28
+ "depth": 1,
29
+ "timeout": 4000
30
+ },
31
+ "validation": {
32
+ "httpIndicators": [
33
+ "http://",
34
+ "require('http')",
35
+ "require(\"http\")",
36
+ "http.createServer"
37
+ ],
38
+ "httpsModules": ["https", "tls"],
39
+ "frameworks": ["express", "nextjs", "nuxtjs", "nestjs"]
40
+ }
41
+ }
@@ -0,0 +1,339 @@
1
+ /**
2
+ * S013 Symbol-Based Analyzer - Enforce TLS for all connections
3
+ */
4
+
5
+ class S013SymbolBasedAnalyzer {
6
+ constructor(semanticEngine) {
7
+ this.ruleId = "S013";
8
+ this.semanticEngine = semanticEngine;
9
+ this.httpIndicators = [
10
+ /http:\/\//i,
11
+ /ws:\/\//i, // WebSocket insecure protocol
12
+ /http\.createServer/,
13
+ /require\(['\"]http['\"]\)/,
14
+ ];
15
+ this.tlsModules = ["https", "tls"];
16
+ this.clientCallMethods = [
17
+ "fetch",
18
+ "axios",
19
+ "request",
20
+ "http.request",
21
+ "http.get",
22
+ ];
23
+ }
24
+
25
+ async initialize() {}
26
+
27
+ analyze(sourceFile, filePath) {
28
+ const violations = [];
29
+ try {
30
+ const { SyntaxKind } = require("ts-morph");
31
+
32
+ // Check for imports/requires of 'http' module
33
+ const importDecls = sourceFile.getImportDeclarations();
34
+ for (const imp of importDecls) {
35
+ const moduleName = imp.getModuleSpecifierValue();
36
+ if (moduleName === "http") {
37
+ const startLine = imp.getStartLineNumber();
38
+ violations.push({
39
+ ruleId: this.ruleId,
40
+ message: `Import of insecure 'http' module detected - prefer 'https' or TLS-wrapped server`,
41
+ severity: "error",
42
+ line: startLine,
43
+ column: 1,
44
+ });
45
+ }
46
+ }
47
+
48
+ // Check for require('http') style and variable assignments with insecure URLs
49
+ const varDecls = sourceFile.getDescendantsOfKind(
50
+ SyntaxKind.VariableDeclaration
51
+ );
52
+ for (const v of varDecls) {
53
+ const init = v.getInitializer();
54
+ if (init && init.getText) {
55
+ const initText = init.getText();
56
+
57
+ // Check for require('http')
58
+ if (/require\(['\"]http['\"]\)/.test(initText)) {
59
+ violations.push({
60
+ ruleId: this.ruleId,
61
+ message: `CommonJS require of insecure 'http' module detected - prefer 'https'`,
62
+ severity: "error",
63
+ line: v.getStartLineNumber(),
64
+ column: 1,
65
+ });
66
+ }
67
+
68
+ // Check for template literals with http:// or ws:// in variable assignments
69
+ const kind = init.getKind && init.getKind();
70
+ if (
71
+ kind === SyntaxKind.TemplateExpression ||
72
+ kind === SyntaxKind.NoSubstitutionTemplateLiteral
73
+ ) {
74
+ if (/http:\/\//i.test(initText)) {
75
+ violations.push({
76
+ ruleId: this.ruleId,
77
+ message: `Variable assigned insecure URL in template literal - use 'https://'`,
78
+ severity: "error",
79
+ line: v.getStartLineNumber(),
80
+ column: 1,
81
+ });
82
+ } else if (/ws:\/\//i.test(initText)) {
83
+ violations.push({
84
+ ruleId: this.ruleId,
85
+ message: `Variable assigned insecure WebSocket URL in template literal - use 'wss://'`,
86
+ severity: "error",
87
+ line: v.getStartLineNumber(),
88
+ column: 1,
89
+ });
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ // Check for http.createServer or server.listen on port 80
96
+ const calls = sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression);
97
+ for (const call of calls) {
98
+ const expr = call.getExpression();
99
+ const text = expr.getText();
100
+
101
+ // Check for 'new' expressions (like new WebSocket('ws://...'))
102
+ const parent = call.getParent();
103
+ const isNewExpression =
104
+ parent &&
105
+ parent.getKind &&
106
+ parent.getKind() === SyntaxKind.NewExpression;
107
+
108
+ // http.createServer(...)
109
+ if (/http\.createServer/.test(text)) {
110
+ violations.push({
111
+ ruleId: this.ruleId,
112
+ message: `Insecure server created with http.createServer - use TLS (https.createServer)`,
113
+ severity: "error",
114
+ line: call.getStartLineNumber(),
115
+ column: 1,
116
+ });
117
+ }
118
+
119
+ // app.listen(80) or server.listen(80)
120
+ if (/\.listen/.test(text)) {
121
+ const args = call.getArguments();
122
+ if (args.length > 0) {
123
+ const first = args[0];
124
+ try {
125
+ const val = first.getText();
126
+ if (/\b80\b/.test(val) || /['\"]http:\/\//.test(val)) {
127
+ violations.push({
128
+ ruleId: this.ruleId,
129
+ message: `Server listening on insecure port or http endpoint (${val}) - configure TLS (443) or use HTTPS`,
130
+ severity: "error",
131
+ line: call.getStartLineNumber(),
132
+ column: 1,
133
+ });
134
+ }
135
+ } catch (e) {
136
+ // ignore
137
+ }
138
+ }
139
+ }
140
+
141
+ // client calls with http:// or ws:// in literal args (fetch('http://...'))
142
+ const args = call.getArguments();
143
+ for (const a of args) {
144
+ try {
145
+ const kind = a.getKind && a.getKind();
146
+
147
+ // Check StringLiteral
148
+ if (kind === SyntaxKind.StringLiteral) {
149
+ const lit = a.getLiteralValue();
150
+ if (/^http:\/\//i.test(lit)) {
151
+ const msgPrefix = isNewExpression
152
+ ? "Constructor"
153
+ : "Insecure client call";
154
+ violations.push({
155
+ ruleId: this.ruleId,
156
+ message: `${msgPrefix} to '${lit}' detected - use 'https://'`,
157
+ severity: "error",
158
+ line: call.getStartLineNumber(),
159
+ column: 1,
160
+ });
161
+ } else if (/^ws:\/\//i.test(lit)) {
162
+ violations.push({
163
+ ruleId: this.ruleId,
164
+ message: `Insecure WebSocket connection to '${lit}' detected - use 'wss://'`,
165
+ severity: "error",
166
+ line: call.getStartLineNumber(),
167
+ column: 1,
168
+ });
169
+ }
170
+ }
171
+
172
+ // Check TemplateExpression and NoSubstitutionTemplateLiteral
173
+ if (
174
+ kind === SyntaxKind.TemplateExpression ||
175
+ kind === SyntaxKind.NoSubstitutionTemplateLiteral
176
+ ) {
177
+ const text = a.getText();
178
+ if (/http:\/\//i.test(text)) {
179
+ violations.push({
180
+ ruleId: this.ruleId,
181
+ message: `Insecure URL in template literal detected - use 'https://'`,
182
+ severity: "error",
183
+ line: a.getStartLineNumber(),
184
+ column: 1,
185
+ });
186
+ } else if (/ws:\/\//i.test(text)) {
187
+ violations.push({
188
+ ruleId: this.ruleId,
189
+ message: `Insecure WebSocket in template literal detected - use 'wss://'`,
190
+ severity: "error",
191
+ line: a.getStartLineNumber(),
192
+ column: 1,
193
+ });
194
+ }
195
+ }
196
+ } catch (e) {
197
+ // ignore
198
+ }
199
+ }
200
+ }
201
+
202
+ // Check NewExpression separately for WebSocket and similar constructors
203
+ const newExprs = sourceFile.getDescendantsOfKind(
204
+ SyntaxKind.NewExpression
205
+ );
206
+ for (const newExpr of newExprs) {
207
+ try {
208
+ const args = newExpr.getArguments();
209
+ for (const a of args) {
210
+ const kind = a.getKind && a.getKind();
211
+
212
+ if (kind === SyntaxKind.StringLiteral) {
213
+ const lit = a.getLiteralValue();
214
+ if (/^ws:\/\//i.test(lit)) {
215
+ violations.push({
216
+ ruleId: this.ruleId,
217
+ message: `Insecure WebSocket connection to '${lit}' detected - use 'wss://'`,
218
+ severity: "error",
219
+ line: newExpr.getStartLineNumber(),
220
+ column: 1,
221
+ });
222
+ } else if (/^http:\/\//i.test(lit)) {
223
+ violations.push({
224
+ ruleId: this.ruleId,
225
+ message: `Constructor with insecure URL '${lit}' detected - use 'https://'`,
226
+ severity: "error",
227
+ line: newExpr.getStartLineNumber(),
228
+ column: 1,
229
+ });
230
+ }
231
+ }
232
+ }
233
+ } catch (e) {
234
+ // ignore
235
+ }
236
+ }
237
+
238
+ // Check framework config objects for https: false or tls disabled
239
+ const objLits = sourceFile.getDescendantsOfKind(
240
+ SyntaxKind.ObjectLiteralExpression
241
+ );
242
+ for (const obj of objLits) {
243
+ const text = obj.getText();
244
+ if (/https\s*:\s*false/.test(text) || /tls\s*:\s*false/.test(text)) {
245
+ violations.push({
246
+ ruleId: this.ruleId,
247
+ message: `Framework configuration disables TLS (https: false) - enable TLS for production`,
248
+ severity: "error",
249
+ line: obj.getStartLineNumber(),
250
+ column: 1,
251
+ });
252
+ }
253
+ }
254
+
255
+ // Check object property assignments for insecure URLs
256
+ const properties = sourceFile.getDescendantsOfKind(
257
+ SyntaxKind.PropertyAssignment
258
+ );
259
+ for (const prop of properties) {
260
+ try {
261
+ const initializer = prop.getInitializer();
262
+ if (initializer) {
263
+ const kind = initializer.getKind && initializer.getKind();
264
+
265
+ // Check string literal property values
266
+ if (kind === SyntaxKind.StringLiteral) {
267
+ const value = initializer.getLiteralValue();
268
+ if (/^http:\/\//i.test(value)) {
269
+ violations.push({
270
+ ruleId: this.ruleId,
271
+ message: `Object property contains insecure URL '${value}' - use 'https://'`,
272
+ severity: "error",
273
+ line: prop.getStartLineNumber(),
274
+ column: 1,
275
+ });
276
+ } else if (/^ws:\/\//i.test(value)) {
277
+ violations.push({
278
+ ruleId: this.ruleId,
279
+ message: `Object property contains insecure WebSocket URL '${value}' - use 'wss://'`,
280
+ severity: "error",
281
+ line: prop.getStartLineNumber(),
282
+ column: 1,
283
+ });
284
+ }
285
+ }
286
+ }
287
+ } catch (e) {
288
+ // ignore
289
+ }
290
+ }
291
+
292
+ // Check array literals for insecure URLs
293
+ const arrayLits = sourceFile.getDescendantsOfKind(
294
+ SyntaxKind.ArrayLiteralExpression
295
+ );
296
+ for (const arr of arrayLits) {
297
+ const elements = arr.getElements();
298
+ for (const elem of elements) {
299
+ try {
300
+ const kind = elem.getKind && elem.getKind();
301
+ if (kind === SyntaxKind.StringLiteral) {
302
+ const value = elem.getLiteralValue();
303
+ if (/^http:\/\//i.test(value)) {
304
+ violations.push({
305
+ ruleId: this.ruleId,
306
+ message: `Array element contains insecure URL '${value}' - use 'https://'`,
307
+ severity: "error",
308
+ line: elem.getStartLineNumber(),
309
+ column: 1,
310
+ });
311
+ } else if (/^ws:\/\//i.test(value)) {
312
+ violations.push({
313
+ ruleId: this.ruleId,
314
+ message: `Array element contains insecure WebSocket URL '${value}' - use 'wss://'`,
315
+ severity: "error",
316
+ line: elem.getStartLineNumber(),
317
+ column: 1,
318
+ });
319
+ }
320
+ }
321
+ } catch (e) {
322
+ // ignore
323
+ }
324
+ }
325
+ }
326
+ } catch (err) {
327
+ console.warn(
328
+ `⚠ [S013] Symbol analysis failed for ${filePath}:`,
329
+ err.message
330
+ );
331
+ }
332
+
333
+ return violations;
334
+ }
335
+
336
+ cleanup() {}
337
+ }
338
+
339
+ module.exports = S013SymbolBasedAnalyzer;