@sun-asterisk/sunlint 1.3.7 → 1.3.8
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/CHANGELOG.md +38 -0
- package/config/defaults/default.json +2 -1
- package/config/rule-analysis-strategies.js +20 -0
- package/config/rules/enhanced-rules-registry.json +190 -35
- package/core/file-targeting-service.js +83 -7
- package/package.json +1 -1
- package/rules/common/C065_one_behavior_per_test/analyzer.js +851 -0
- package/rules/common/C065_one_behavior_per_test/config.json +95 -0
- package/rules/security/S037_cache_headers/README.md +128 -0
- package/rules/security/S037_cache_headers/analyzer.js +263 -0
- package/rules/security/S037_cache_headers/config.json +50 -0
- package/rules/security/S037_cache_headers/regex-based-analyzer.js +463 -0
- package/rules/security/S037_cache_headers/symbol-based-analyzer.js +546 -0
- package/rules/security/S038_no_version_headers/README.md +234 -0
- package/rules/security/S038_no_version_headers/analyzer.js +262 -0
- package/rules/security/S038_no_version_headers/config.json +49 -0
- package/rules/security/S038_no_version_headers/regex-based-analyzer.js +339 -0
- package/rules/security/S038_no_version_headers/symbol-based-analyzer.js +375 -0
- package/rules/security/S039_no_session_tokens_in_url/README.md +198 -0
- package/rules/security/S039_no_session_tokens_in_url/analyzer.js +262 -0
- package/rules/security/S039_no_session_tokens_in_url/config.json +92 -0
- package/rules/security/S039_no_session_tokens_in_url/regex-based-analyzer.js +337 -0
- package/rules/security/S039_no_session_tokens_in_url/symbol-based-analyzer.js +436 -0
- package/rules/security/S049_short_validity_tokens/analyzer.js +175 -0
- package/rules/security/S049_short_validity_tokens/config.json +124 -0
- package/rules/security/S049_short_validity_tokens/regex-based-analyzer.js +295 -0
- package/rules/security/S049_short_validity_tokens/symbol-based-analyzer.js +389 -0
- package/rules/security/S051_password_length_policy/analyzer.js +410 -0
- package/rules/security/S051_password_length_policy/config.json +83 -0
- package/rules/security/S052_weak_otp_entropy/analyzer.js +403 -0
- package/rules/security/S052_weak_otp_entropy/config.json +57 -0
- package/rules/security/S054_no_default_accounts/README.md +129 -0
- package/rules/security/S054_no_default_accounts/analyzer.js +792 -0
- package/rules/security/S054_no_default_accounts/config.json +101 -0
- package/rules/security/S056_log_injection_protection/analyzer.js +242 -0
- package/rules/security/S056_log_injection_protection/config.json +148 -0
- package/rules/security/S056_log_injection_protection/regex-based-analyzer.js +120 -0
- package/rules/security/S056_log_injection_protection/symbol-based-analyzer.js +287 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ruleId": "C065",
|
|
3
|
+
"name": "One Behavior per Test (AAA Pattern)",
|
|
4
|
+
"description": "Each test case should focus on testing a single behavior to improve clarity and maintainability",
|
|
5
|
+
"category": "common",
|
|
6
|
+
"severity": "warn",
|
|
7
|
+
"languages": ["typescript", "javascript", "java", "csharp", "swift", "kotlin"],
|
|
8
|
+
"options": {
|
|
9
|
+
"assertApis": {
|
|
10
|
+
"javascript": ["expect\\(", "assert\\.", "should\\.", "chai\\.expect"],
|
|
11
|
+
"typescript": ["expect\\(", "assert\\.", "should\\.", "chai\\.expect"],
|
|
12
|
+
"java": ["assertThat\\(", "assertEquals\\(", "assertTrue\\(", "assertFalse\\(", "assertNull\\(", "assertNotNull\\("],
|
|
13
|
+
"csharp": ["Assert\\.", "Xunit\\.Assert", "NUnit\\.Framework\\.Assert"],
|
|
14
|
+
"swift": ["XCTAssert", "XCTAssertEqual", "XCTAssertTrue", "XCTAssertFalse"],
|
|
15
|
+
"kotlin": ["assertThat\\(", "assertEquals\\(", "assertTrue\\(", "assertFalse\\("]
|
|
16
|
+
},
|
|
17
|
+
"actHeuristics": {
|
|
18
|
+
"common": ["sut\\.", "\\.create\\(", "\\.update\\(", "\\.delete\\(", "\\.save\\(", "\\.execute\\(", "\\.handle\\(", "\\.run\\(", "\\.call\\(", "\\.process\\(", "service\\.", "repository\\.", "await\\s+\\w+\\."],
|
|
19
|
+
"javascript": ["\\.mockReturnValue\\(", "\\.mockResolvedValue\\(", "render\\(", "fireEvent\\.(click|submit|keyPress|keyDown|keyUp)", "userService\\.", "api\\.", "fetch\\(", "user\\.click\\(", "user\\.type\\(", "user\\.clear\\("],
|
|
20
|
+
"typescript": ["\\.mockReturnValue\\(", "\\.mockResolvedValue\\(", "render\\(", "fireEvent\\.(click|submit|keyPress|keyDown|keyUp)", "userService\\.", "api\\.", "fetch\\(", "user\\.click\\(", "user\\.type\\(", "user\\.clear\\("],
|
|
21
|
+
"java": ["\\.when\\(", "\\.thenReturn\\(", "\\.doReturn\\(", "\\.verify\\("],
|
|
22
|
+
"csharp": ["\\.Setup\\(", "\\.Returns\\(", "\\.Verify\\("],
|
|
23
|
+
"swift": ["given\\(", "when\\(", "then\\("],
|
|
24
|
+
"kotlin": ["every\\s*\\{", "verify\\s*\\{"]
|
|
25
|
+
},
|
|
26
|
+
"controlFlow": ["\\bif\\s*\\(", "\\bswitch\\s*\\(", "\\bfor\\s*\\(", "\\bwhile\\s*\\(", "\\btry\\s*\\{"],
|
|
27
|
+
"testPatterns": {
|
|
28
|
+
"javascript": ["\\bit\\s*\\(", "\\btest\\s*\\(", "\\bdescribe\\s*\\("],
|
|
29
|
+
"typescript": ["\\bit\\s*\\(", "\\btest\\s*\\(", "\\bdescribe\\s*\\("],
|
|
30
|
+
"java": ["@Test", "@ParameterizedTest"],
|
|
31
|
+
"csharp": ["\\[Test\\]", "\\[Fact\\]", "\\[Theory\\]"],
|
|
32
|
+
"swift": ["func\\s+test", "XCTestCase"],
|
|
33
|
+
"kotlin": ["@Test", "fun\\s+test"]
|
|
34
|
+
},
|
|
35
|
+
"parameterizedHints": ["test\\.each", "describe\\.each", "@ParameterizedTest", "where\\s*:", "\\[TestCase"],
|
|
36
|
+
"thresholds": {
|
|
37
|
+
"maxActsPerTest": 3,
|
|
38
|
+
"maxUnrelatedExpects": 2,
|
|
39
|
+
"maxControlFlowStatements": 0,
|
|
40
|
+
"maxTestMethodsPerFunction": 1
|
|
41
|
+
},
|
|
42
|
+
"flags": {
|
|
43
|
+
"flagControlFlowInTest": true,
|
|
44
|
+
"treatSnapshotAsSingleAssert": true,
|
|
45
|
+
"allowMultipleAssertsForSameObject": true,
|
|
46
|
+
"allowSetupAssertions": true,
|
|
47
|
+
"allowMultipleUIActions": true
|
|
48
|
+
},
|
|
49
|
+
"whitelist": {
|
|
50
|
+
"setupMethods": ["beforeEach", "setUp", "before", "Given"],
|
|
51
|
+
"teardownMethods": ["afterEach", "tearDown", "after"],
|
|
52
|
+
"helperMethods": ["helper", "util", "mock", "stub", "spy"]
|
|
53
|
+
},
|
|
54
|
+
"allowlist": {
|
|
55
|
+
"paths": ["test/", "tests/", "__tests__/", "spec/", "specs/", "*.test.*", "*.spec.*"],
|
|
56
|
+
"filePatterns": ["\\.test\\.", "\\.spec\\.", "Test\\.java$", "Tests\\.cs$", "Test\\.swift$", "Test\\.kt$"],
|
|
57
|
+
"formInteractionSequences": [
|
|
58
|
+
"fireEvent\\.change.*fireEvent\\.blur",
|
|
59
|
+
"fireEvent\\.change.*fireEvent\\.focus",
|
|
60
|
+
"user\\.type.*user\\.tab",
|
|
61
|
+
"user\\.type.*user\\.clear",
|
|
62
|
+
"render.*fireEvent\\.change.*fireEvent\\.blur"
|
|
63
|
+
],
|
|
64
|
+
"uiInteractionWorkflows": [
|
|
65
|
+
"fireEvent\\.click.*expect\\(",
|
|
66
|
+
"fireEvent\\.click.*fireEvent\\.click",
|
|
67
|
+
"render.*fireEvent\\.click.*expect\\(",
|
|
68
|
+
"getByRole.*fireEvent\\.click.*expect\\(",
|
|
69
|
+
"queryByText.*fireEvent\\.click",
|
|
70
|
+
"user\\.click.*expect\\(",
|
|
71
|
+
"user\\.click.*user\\.click"
|
|
72
|
+
],
|
|
73
|
+
"setupActionPatterns": [
|
|
74
|
+
"render\\(",
|
|
75
|
+
"fireEvent\\.(change|blur|focus|mouseEnter|mouseLeave)",
|
|
76
|
+
"user\\.(hover|focus|tab|clear)",
|
|
77
|
+
"\\.mockReturnValue\\(",
|
|
78
|
+
"\\.mockResolvedValue\\("
|
|
79
|
+
],
|
|
80
|
+
"uiSetupPatterns": [
|
|
81
|
+
"getByRole\\(",
|
|
82
|
+
"queryByText\\(",
|
|
83
|
+
"queryAllByText\\(",
|
|
84
|
+
"getByText\\(",
|
|
85
|
+
"findByRole\\(",
|
|
86
|
+
"act\\(.*render"
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"overrideExclude": {
|
|
90
|
+
"enabled": true,
|
|
91
|
+
"reason": "C065 needs to analyze test files that are typically excluded by project configs",
|
|
92
|
+
"removePatterns": ["**/*.test.*", "**/*.spec.*", "src/**/*.test.{ts,tsx}", "src/**/*.spec.{ts,tsx}"]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# S037 - Configure Comprehensive Cache Headers to Prevent Sensitive Data Leakage
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Rule S037 ensures that sensitive HTTP responses explicitly disable caching using a complete set of anti-caching headers:
|
|
6
|
+
|
|
7
|
+
- `Cache-Control: no-store, no-cache, must-revalidate`
|
|
8
|
+
- `Pragma: no-cache`
|
|
9
|
+
- `Expires: 0`
|
|
10
|
+
|
|
11
|
+
These prevent browsers and intermediate proxies from persisting sensitive content (e.g., authenticated pages, profile data, tokens) in cache storage where it could later be exposed.
|
|
12
|
+
|
|
13
|
+
## Framework Support
|
|
14
|
+
|
|
15
|
+
This rule supports multiple web frameworks:
|
|
16
|
+
|
|
17
|
+
- **Express.js**: Route handlers using `app.get()`, `router.post()`, etc.
|
|
18
|
+
- **Next.js**: API routes (`export default function handler`) and App Router (`export async function GET`)
|
|
19
|
+
- **NestJS**: Controller methods with decorators (`@Get()`, `@Post()`, etc.)
|
|
20
|
+
- **Nuxt.js**: Server routes (`export default defineEventHandler`)
|
|
21
|
+
|
|
22
|
+
## Security Impact
|
|
23
|
+
|
|
24
|
+
Without proper cache headers, authenticated or confidential data may remain in browser cache or be served to other users (in shared environments, kiosk systems, or via back/forward navigation). Attackers may retrieve cached responses, exposing session data or personal information.
|
|
25
|
+
|
|
26
|
+
## Rule Details
|
|
27
|
+
|
|
28
|
+
### Required Header Set
|
|
29
|
+
|
|
30
|
+
All of the following must be present for sensitive responses:
|
|
31
|
+
|
|
32
|
+
| Header | Required Value / Directives |
|
|
33
|
+
| ------------- | --------------------------------------------------------------------------- |
|
|
34
|
+
| Cache-Control | `no-store, no-cache, must-revalidate` (order flexible, directives required) |
|
|
35
|
+
| Pragma | `no-cache` |
|
|
36
|
+
| Expires | `0` (or a past date) |
|
|
37
|
+
|
|
38
|
+
### Sensitive Response Indicators (heuristic)
|
|
39
|
+
|
|
40
|
+
The rule assumes a response is sensitive when code references identifiers like: `session`, `auth`, `token`, `jwt`, `csrf`, `user`, `profile`, `payment`, `account`.
|
|
41
|
+
|
|
42
|
+
### Detected Patterns
|
|
43
|
+
|
|
44
|
+
✅ **Compliant Examples:**
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
48
|
+
res.setHeader("Pragma", "no-cache");
|
|
49
|
+
res.setHeader("Expires", "0");
|
|
50
|
+
res.json({ user: profile });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
response.set({
|
|
55
|
+
"Cache-Control": "no-store, no-cache, must-revalidate",
|
|
56
|
+
Pragma: "no-cache",
|
|
57
|
+
Expires: "0",
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
❌ **Violation Examples:**
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// Missing Cache-Control directives
|
|
65
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
66
|
+
res.setHeader("Pragma", "no-cache");
|
|
67
|
+
res.setHeader("Expires", "0");
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// Missing Pragma and Expires
|
|
72
|
+
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
// Missing all anti-cache headers
|
|
77
|
+
res.json({ auth: token, profile });
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Partial Cache-Control Values
|
|
81
|
+
|
|
82
|
+
The rule flags missing directives individually when `Cache-Control` is present but incomplete.
|
|
83
|
+
|
|
84
|
+
| Example Value | Result |
|
|
85
|
+
| ------------------------------------- | ----------------------------------------- |
|
|
86
|
+
| `no-cache` | ❌ Missing: `no-store`, `must-revalidate` |
|
|
87
|
+
| `no-store, no-cache` | ❌ Missing: `must-revalidate` |
|
|
88
|
+
| `no-store, no-cache, must-revalidate` | ✅ Valid |
|
|
89
|
+
|
|
90
|
+
## Analysis Approach
|
|
91
|
+
|
|
92
|
+
### Symbol-Based (Primary)
|
|
93
|
+
|
|
94
|
+
Parses AST to collect header setting calls across a file, then evaluates completeness.
|
|
95
|
+
|
|
96
|
+
### Regex-Based (Fallback)
|
|
97
|
+
|
|
98
|
+
Scans for literal `setHeader`/`set` calls to approximate detection when semantic engine unavailable.
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
See `config.json` for adjustable keys:
|
|
103
|
+
|
|
104
|
+
- `validation.required` – mandatory headers & directives
|
|
105
|
+
- `validation.sensitiveIndicators` – triggers stricter enforcement when detected
|
|
106
|
+
- `patterns.include/exclude` – file targeting
|
|
107
|
+
|
|
108
|
+
## Best Practices
|
|
109
|
+
|
|
110
|
+
1. Always send complete anti-cache headers for authenticated pages.
|
|
111
|
+
2. Never rely only on `Pragma` or only `Cache-Control`.
|
|
112
|
+
3. Include `no-store` for maximum protection (prevents any storage).
|
|
113
|
+
4. Pair with other security headers (CSP, X-Content-Type-Options, etc.).
|
|
114
|
+
5. Re-check reverse proxy / CDN behavior (may override headers).
|
|
115
|
+
|
|
116
|
+
## Related Rules
|
|
117
|
+
|
|
118
|
+
- **S031**: Secure flag for cookies
|
|
119
|
+
- **S032**: HttpOnly flag for cookies
|
|
120
|
+
- **S033**: SameSite attribute
|
|
121
|
+
- **S034**: \_\_Host- prefix
|
|
122
|
+
- **S035**: Path attribute
|
|
123
|
+
|
|
124
|
+
## References
|
|
125
|
+
|
|
126
|
+
- MDN: HTTP Caching
|
|
127
|
+
- OWASP: Sensitive Data Exposure / Cryptographic Failures
|
|
128
|
+
- RFC 7234 - HTTP/1.1 Caching
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S037 Main Analyzer - Configure comprehensive cache headers to prevent sensitive data leakage
|
|
3
|
+
* Primary: Symbol-based analysis (when available)
|
|
4
|
+
* Fallback: Regex-based for all other cases
|
|
5
|
+
* Command: node cli.js --rule=S037 --input=examples/rule-test-fixtures/rules/S037_cache_headers --engine=heuristic
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const S037SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
|
|
9
|
+
const S037RegexBasedAnalyzer = require("./regex-based-analyzer.js");
|
|
10
|
+
|
|
11
|
+
class S037Analyzer {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
14
|
+
console.log(`🔧 [S037] Constructor called with options:`, !!options);
|
|
15
|
+
console.log(
|
|
16
|
+
`🔧 [S037] Options type:`,
|
|
17
|
+
typeof options,
|
|
18
|
+
Object.keys(options || {})
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
this.ruleId = "S037";
|
|
23
|
+
this.ruleName =
|
|
24
|
+
"Configure comprehensive cache headers to prevent sensitive data leakage";
|
|
25
|
+
this.description =
|
|
26
|
+
"Configure comprehensive cache headers (Cache-Control: no-store, no-cache, must-revalidate, Pragma: no-cache, Expires: 0) for sensitive responses to avoid caching sensitive data in browsers or intermediaries.";
|
|
27
|
+
this.semanticEngine = options.semanticEngine || null;
|
|
28
|
+
this.verbose = options.verbose || false;
|
|
29
|
+
|
|
30
|
+
this.config = {
|
|
31
|
+
useSymbolBased: true,
|
|
32
|
+
fallbackToRegex: true,
|
|
33
|
+
regexBasedOnly: false,
|
|
34
|
+
prioritizeSymbolic: true, // Prefer symbol-based when available
|
|
35
|
+
fallbackToSymbol: true, // Allow symbol analysis even without semantic engine
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
this.symbolAnalyzer = new S037SymbolBasedAnalyzer(this.semanticEngine);
|
|
40
|
+
if (process.env.SUNLINT_DEBUG)
|
|
41
|
+
console.log(`🔧 [S037] Symbol analyzer created successfully`);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(`🔧 [S037] Error creating symbol analyzer:`, error);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
this.regexAnalyzer = new S037RegexBasedAnalyzer(this.semanticEngine);
|
|
48
|
+
if (process.env.SUNLINT_DEBUG)
|
|
49
|
+
console.log(`🔧 [S037] Regex analyzer created successfully`);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`🔧 [S037] Error creating regex analyzer:`, error);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async initialize(semanticEngine) {
|
|
56
|
+
this.semanticEngine = semanticEngine;
|
|
57
|
+
if (process.env.SUNLINT_DEBUG)
|
|
58
|
+
console.log(`🔧 [S037] Main analyzer initializing...`);
|
|
59
|
+
|
|
60
|
+
if (this.symbolAnalyzer)
|
|
61
|
+
await this.symbolAnalyzer.initialize?.(semanticEngine);
|
|
62
|
+
if (this.regexAnalyzer)
|
|
63
|
+
await this.regexAnalyzer.initialize?.(semanticEngine);
|
|
64
|
+
if (this.regexAnalyzer) this.regexAnalyzer.cleanup?.();
|
|
65
|
+
|
|
66
|
+
if (process.env.SUNLINT_DEBUG)
|
|
67
|
+
console.log(`🔧 [S037] Main analyzer initialized successfully`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
analyzeSingle(filePath, options = {}) {
|
|
71
|
+
if (process.env.SUNLINT_DEBUG)
|
|
72
|
+
console.log(`🔍 [S037] analyzeSingle() called for: ${filePath}`);
|
|
73
|
+
return this.analyze([filePath], "typescript", options);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async analyze(files, language, options = {}) {
|
|
77
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
78
|
+
console.log(
|
|
79
|
+
`🔧 [S037] analyze() method called with ${files.length} files, language: ${language}`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const violations = [];
|
|
84
|
+
for (const filePath of files) {
|
|
85
|
+
try {
|
|
86
|
+
if (process.env.SUNLINT_DEBUG)
|
|
87
|
+
console.log(`🔧 [S037] Processing file: ${filePath}`);
|
|
88
|
+
const fileViolations = await this.analyzeFile(filePath, options);
|
|
89
|
+
violations.push(...fileViolations);
|
|
90
|
+
if (process.env.SUNLINT_DEBUG)
|
|
91
|
+
console.log(
|
|
92
|
+
`🔧 [S037] File ${filePath}: Found ${fileViolations.length} violations`
|
|
93
|
+
);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.warn(
|
|
96
|
+
`⚠ [S037] Analysis failed for ${filePath}:`,
|
|
97
|
+
error.message
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (process.env.SUNLINT_DEBUG)
|
|
103
|
+
console.log(`🔧 [S037] Total violations found: ${violations.length}`);
|
|
104
|
+
return violations;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async analyzeFile(filePath, options = {}) {
|
|
108
|
+
if (process.env.SUNLINT_DEBUG)
|
|
109
|
+
console.log(`🔍 [S037] analyzeFile() called for: ${filePath}`);
|
|
110
|
+
const violationMap = new Map();
|
|
111
|
+
|
|
112
|
+
// Try symbol-based analysis first when semantic engine is available OR when explicitly enabled
|
|
113
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
114
|
+
console.log(
|
|
115
|
+
`🔧 [S037] Symbol check: useSymbolBased=${
|
|
116
|
+
this.config.useSymbolBased
|
|
117
|
+
}, semanticEngine=${!!this.semanticEngine}, project=${!!this
|
|
118
|
+
.semanticEngine?.project}, initialized=${!!this.semanticEngine
|
|
119
|
+
?.initialized}`
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const canUseSymbol =
|
|
124
|
+
this.config.useSymbolBased &&
|
|
125
|
+
((this.semanticEngine?.project && this.semanticEngine?.initialized) ||
|
|
126
|
+
(!this.semanticEngine && this.config.fallbackToSymbol !== false));
|
|
127
|
+
|
|
128
|
+
if (canUseSymbol) {
|
|
129
|
+
try {
|
|
130
|
+
if (process.env.SUNLINT_DEBUG)
|
|
131
|
+
console.log(`🔧 [S037] Trying symbol-based analysis...`);
|
|
132
|
+
|
|
133
|
+
let sourceFile = null;
|
|
134
|
+
if (this.semanticEngine?.project) {
|
|
135
|
+
sourceFile = this.semanticEngine.project.getSourceFile(filePath);
|
|
136
|
+
if (process.env.SUNLINT_DEBUG) {
|
|
137
|
+
console.log(
|
|
138
|
+
`🔧 [S037] Checked existing semantic engine project: sourceFile=${!!sourceFile}`
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!sourceFile) {
|
|
144
|
+
// Create a minimal ts-morph project for this analysis
|
|
145
|
+
if (process.env.SUNLINT_DEBUG)
|
|
146
|
+
console.log(
|
|
147
|
+
`🔧 [S037] Creating temporary ts-morph project for: ${filePath}`
|
|
148
|
+
);
|
|
149
|
+
try {
|
|
150
|
+
const fs = require("fs");
|
|
151
|
+
const path = require("path");
|
|
152
|
+
const { Project } = require("ts-morph");
|
|
153
|
+
|
|
154
|
+
// Check if file exists and read content
|
|
155
|
+
if (!fs.existsSync(filePath)) {
|
|
156
|
+
throw new Error(`File not found: ${filePath}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const fileContent = fs.readFileSync(filePath, "utf8");
|
|
160
|
+
const fileName = path.basename(filePath);
|
|
161
|
+
|
|
162
|
+
const tempProject = new Project({
|
|
163
|
+
useInMemoryFileSystem: true,
|
|
164
|
+
compilerOptions: {
|
|
165
|
+
allowJs: true,
|
|
166
|
+
allowSyntheticDefaultImports: true,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// Add file content to in-memory project
|
|
171
|
+
sourceFile = tempProject.createSourceFile(fileName, fileContent);
|
|
172
|
+
if (process.env.SUNLINT_DEBUG)
|
|
173
|
+
console.log(
|
|
174
|
+
`🔧 [S037] Temporary project created successfully with file: ${fileName}`
|
|
175
|
+
);
|
|
176
|
+
} catch (projectError) {
|
|
177
|
+
if (process.env.SUNLINT_DEBUG)
|
|
178
|
+
console.log(
|
|
179
|
+
`🔧 [S037] Failed to create temporary project:`,
|
|
180
|
+
projectError.message
|
|
181
|
+
);
|
|
182
|
+
throw projectError;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (sourceFile) {
|
|
187
|
+
const symbolViolations = await this.symbolAnalyzer.analyze(
|
|
188
|
+
sourceFile,
|
|
189
|
+
filePath
|
|
190
|
+
);
|
|
191
|
+
symbolViolations.forEach((v) => {
|
|
192
|
+
const key = `${v.line}:${v.column}:${v.message}`;
|
|
193
|
+
if (!violationMap.has(key)) violationMap.set(key, v);
|
|
194
|
+
});
|
|
195
|
+
if (process.env.SUNLINT_DEBUG)
|
|
196
|
+
console.log(
|
|
197
|
+
`🔧 [S037] Symbol analysis completed: ${symbolViolations.length} violations`
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// If symbol-based found violations or prioritizeSymbolic is true, skip regex
|
|
201
|
+
if (this.config.prioritizeSymbolic && symbolViolations.length >= 0) {
|
|
202
|
+
const finalViolations = Array.from(violationMap.values()).map(
|
|
203
|
+
(v) => ({
|
|
204
|
+
...v,
|
|
205
|
+
filePath,
|
|
206
|
+
file: filePath,
|
|
207
|
+
})
|
|
208
|
+
);
|
|
209
|
+
if (process.env.SUNLINT_DEBUG)
|
|
210
|
+
console.log(
|
|
211
|
+
`🔧 [S037] Symbol-based analysis prioritized: ${finalViolations.length} violations`
|
|
212
|
+
);
|
|
213
|
+
return finalViolations;
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
if (process.env.SUNLINT_DEBUG)
|
|
217
|
+
console.log(
|
|
218
|
+
`🔧 [S037] No source file found, skipping symbol analysis`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
} catch (error) {
|
|
222
|
+
console.warn(`⚠ [S037] Symbol analysis failed:`, error.message);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Fallback to regex-based analysis
|
|
227
|
+
if (this.config.fallbackToRegex || this.config.regexBasedOnly) {
|
|
228
|
+
try {
|
|
229
|
+
if (process.env.SUNLINT_DEBUG)
|
|
230
|
+
console.log(`🔧 [S037] Trying regex-based analysis...`);
|
|
231
|
+
const regexViolations = await this.regexAnalyzer.analyze(filePath);
|
|
232
|
+
regexViolations.forEach((v) => {
|
|
233
|
+
const key = `${v.line}:${v.column}:${v.message}`;
|
|
234
|
+
if (!violationMap.has(key)) violationMap.set(key, v);
|
|
235
|
+
});
|
|
236
|
+
if (process.env.SUNLINT_DEBUG)
|
|
237
|
+
console.log(
|
|
238
|
+
`🔧 [S037] Regex analysis completed: ${regexViolations.length} violations`
|
|
239
|
+
);
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.warn(`⚠ [S037] Regex analysis failed:`, error.message);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const finalViolations = Array.from(violationMap.values()).map((v) => ({
|
|
246
|
+
...v,
|
|
247
|
+
filePath,
|
|
248
|
+
file: filePath,
|
|
249
|
+
}));
|
|
250
|
+
if (process.env.SUNLINT_DEBUG)
|
|
251
|
+
console.log(
|
|
252
|
+
`🔧 [S037] File analysis completed: ${finalViolations.length} unique violations`
|
|
253
|
+
);
|
|
254
|
+
return finalViolations;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
cleanup() {
|
|
258
|
+
if (this.symbolAnalyzer?.cleanup) this.symbolAnalyzer.cleanup();
|
|
259
|
+
if (this.regexAnalyzer?.cleanup) this.regexAnalyzer.cleanup();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
module.exports = S037Analyzer;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "S037",
|
|
3
|
+
"name": "Configure comprehensive cache headers to prevent sensitive data leakage",
|
|
4
|
+
"category": "security",
|
|
5
|
+
"description": "S037 - Configure comprehensive cache headers (Cache-Control: no-store, no-cache, must-revalidate, Pragma: no-cache, Expires: 0) for sensitive responses to avoid caching sensitive data in browsers or intermediaries.",
|
|
6
|
+
"severity": "warning",
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"semantic": {
|
|
9
|
+
"enabled": true,
|
|
10
|
+
"priority": "medium",
|
|
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
|
+
"headerSetters": ["setHeader", "set", "header"],
|
|
33
|
+
"required": {
|
|
34
|
+
"Cache-Control": ["no-store", "no-cache", "must-revalidate"],
|
|
35
|
+
"Pragma": ["no-cache"],
|
|
36
|
+
"Expires": ["0"]
|
|
37
|
+
},
|
|
38
|
+
"sensitiveIndicators": [
|
|
39
|
+
"session",
|
|
40
|
+
"auth",
|
|
41
|
+
"token",
|
|
42
|
+
"jwt",
|
|
43
|
+
"csrf",
|
|
44
|
+
"user",
|
|
45
|
+
"profile",
|
|
46
|
+
"payment",
|
|
47
|
+
"account"
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
}
|