@sun-asterisk/sunlint 1.3.36 → 1.3.37
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/cli.js +33 -0
- package/config/rules/enhanced-rules-registry.json +354 -98
- package/config/rules/rules-registry-generated.json +197 -171
- package/core/architecture-integration.js +115 -17
- package/core/cli-action-handler.js +101 -27
- package/core/cli-program.js +5 -0
- package/core/github-annotate-service.js +62 -0
- package/core/impact-integration.js +31 -16
- package/core/init-command.js +227 -0
- package/core/output-service.js +53 -5
- package/core/summary-report-service.js +46 -0
- package/core/unified-rule-registry.js +2 -1
- package/engines/eslint-engine.js +6 -0
- package/engines/impact/core/detectors/database-detector.js +1 -1
- package/engines/impact/core/detectors/endpoint-detector.js +1 -1
- package/engines/impact/core/report-generator.js +235 -73
- package/origin-rules/security-en.md +470 -282
- package/package.json +1 -1
- package/rules/security/S001_backend_auth_communications/dart/analyzer.js +44 -0
- package/rules/security/S001_backend_auth_communications/index.js +87 -0
- package/rules/security/S001_backend_auth_communications/typescript/analyzer.js +164 -0
- package/rules/security/S002_os_command_injection/dart/analyzer.js +44 -0
- package/rules/security/S002_os_command_injection/index.js +87 -0
- package/rules/security/S002_os_command_injection/typescript/analyzer.js +194 -0
- package/rules/security/S008_svg_content_validation/dart/analyzer.js +44 -0
- package/rules/security/S008_svg_content_validation/index.js +87 -0
- package/rules/security/S008_svg_content_validation/typescript/analyzer.js +216 -0
- package/rules/security/S018_no_sensitive_browser_storage/dart/analyzer.js +44 -0
- package/rules/security/S018_no_sensitive_browser_storage/index.js +86 -0
- package/rules/security/S018_no_sensitive_browser_storage/typescript/analyzer.js +193 -0
- package/rules/security/S021_referrer_policy/dart/analyzer.js +44 -0
- package/rules/security/S021_referrer_policy/index.js +86 -0
- package/rules/security/S021_referrer_policy/typescript/analyzer.js +183 -0
- package/rules/security/S023_no_json_injection/config.json +133 -44
- package/rules/security/S023_no_json_injection/dart/analyzer.js +7 -6
- package/rules/security/S023_no_json_injection/typescript/analyzer.js +402 -126
- package/rules/security/S023_no_json_injection/typescript/ast-analyzer.js +571 -154
- package/rules/security/S026_tls_all_connections/config.json +30 -0
- package/rules/security/S026_tls_all_connections/typescript/analyzer.js +339 -0
- package/rules/security/S027_mtls_certificate_validation/config.json +30 -0
- package/rules/security/S027_mtls_certificate_validation/typescript/analyzer.js +225 -0
- package/rules/security/S035_separate_app_hostnames/config.json +28 -0
- package/rules/security/S035_separate_app_hostnames/typescript/analyzer.js +186 -0
- package/rules/security/S036_lfi_rfi_protection/config.json +2 -2
- package/rules/security/S039_tls_certificate_validation/config.json +29 -0
- package/rules/security/S039_tls_certificate_validation/typescript/analyzer.js +229 -0
- package/rules/security/S046_jwt_algorithm_allowlist/config.json +28 -0
- package/rules/security/S046_jwt_algorithm_allowlist/dart/analyzer.js +44 -0
- package/rules/security/S046_jwt_algorithm_allowlist/index.js +87 -0
- package/rules/security/S046_jwt_algorithm_allowlist/typescript/analyzer.js +235 -0
- package/rules/security/S047_oauth_pkce_protection/config.json +31 -0
- package/rules/security/S047_oauth_pkce_protection/dart/analyzer.js +44 -0
- package/rules/security/S047_oauth_pkce_protection/index.js +86 -0
- package/rules/security/S047_oauth_pkce_protection/typescript/analyzer.js +78 -0
- package/rules/security/S048_oauth_redirect_uri_validation/config.json +30 -0
- package/rules/security/S048_oauth_redirect_uri_validation/typescript/analyzer.js +278 -0
- package/rules/security/S049_short_validity_tokens/typescript/config.json +10 -3
- package/rules/security/S050_reference_tokens_entropy/config.json +28 -0
- package/rules/security/S050_reference_tokens_entropy/dart/analyzer.js +45 -0
- package/rules/security/S050_reference_tokens_entropy/index.js +86 -0
- package/rules/security/S050_reference_tokens_entropy/typescript/analyzer.js +74 -0
- package/rules/security/S053_generic_error_messages/config.json +28 -0
- package/rules/security/S053_generic_error_messages/dart/analyzer.js +45 -0
- package/rules/security/S053_generic_error_messages/index.js +86 -0
- package/rules/security/S053_generic_error_messages/typescript/analyzer.js +80 -0
- package/rules/security/S055_content_type_validation/typescript/symbol-based-analyzer.js +64 -2
- package/rules/security/S059_disable_debug_mode/config.json +28 -0
- package/rules/security/S059_disable_debug_mode/dart/analyzer.js +45 -0
- package/rules/security/S059_disable_debug_mode/index.js +86 -0
- package/rules/security/S059_disable_debug_mode/typescript/analyzer.js +85 -0
- package/rules/security/S060_password_minimum_length/config.json +28 -0
- package/rules/security/S060_password_minimum_length/dart/analyzer.js +45 -0
- package/rules/security/S060_password_minimum_length/index.js +86 -0
- package/rules/security/S060_password_minimum_length/typescript/analyzer.js +78 -0
- package/rules/security/S026_json_schema_validation/config.json +0 -27
- package/rules/security/S026_json_schema_validation/typescript/analyzer.js +0 -251
- package/rules/security/S027_no_hardcoded_secrets/config.json +0 -29
- package/rules/security/S027_no_hardcoded_secrets/typescript/analyzer.js +0 -309
- package/rules/security/S027_no_hardcoded_secrets/typescript/categories.json +0 -153
- package/rules/security/S035_path_session_cookies/config.json +0 -99
- package/rules/security/S035_path_session_cookies/typescript/analyzer.js +0 -316
- package/rules/security/S035_path_session_cookies/typescript/regex-based-analyzer.js +0 -724
- package/rules/security/S035_path_session_cookies/typescript/symbol-based-analyzer.js +0 -373
- package/rules/security/S039_no_session_tokens_in_url/config.json +0 -92
- package/rules/security/S039_no_session_tokens_in_url/typescript/analyzer.js +0 -262
- package/rules/security/S039_no_session_tokens_in_url/typescript/regex-based-analyzer.js +0 -337
- package/rules/security/S039_no_session_tokens_in_url/typescript/symbol-based-analyzer.js +0 -443
- package/rules/security/S048_no_current_password_in_reset/config.json +0 -48
- package/rules/security/S048_no_current_password_in_reset/typescript/analyzer.js +0 -366
- /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/dart/analyzer.js +0 -0
- /package/rules/security/{S026_json_schema_validation → S026_tls_all_connections}/index.js +0 -0
- /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/dart/analyzer.js +0 -0
- /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/index.js +0 -0
- /package/rules/security/{S027_no_hardcoded_secrets → S027_mtls_certificate_validation}/typescript/categorized-analyzer.js +0 -0
- /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/dart/analyzer.js +0 -0
- /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/index.js +0 -0
- /package/rules/security/{S035_path_session_cookies → S035_separate_app_hostnames}/typescript/README.md +0 -0
- /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/dart/analyzer.js +0 -0
- /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/index.js +0 -0
- /package/rules/security/{S039_no_session_tokens_in_url → S039_tls_certificate_validation}/typescript/README.md +0 -0
- /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/dart/analyzer.js +0 -0
- /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/index.js +0 -0
- /package/rules/security/{S048_no_current_password_in_reset → S048_oauth_redirect_uri_validation}/typescript/README.md +0 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S008 Rule Router - SVG Content Validation
|
|
3
|
+
*
|
|
4
|
+
* Routes analysis to the appropriate language-specific analyzer.
|
|
5
|
+
* Supports: TypeScript, JavaScript, Dart
|
|
6
|
+
*
|
|
7
|
+
* Rule: Ensure user-supplied SVG content is validated or sanitized
|
|
8
|
+
* to prevent script injection and other attacks.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
class S008Router {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.analyzers = new Map();
|
|
16
|
+
this.ruleId = 'S008';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getAnalyzer(language) {
|
|
20
|
+
const normalizedLang = this.normalizeLanguage(language);
|
|
21
|
+
|
|
22
|
+
if (!this.analyzers.has(normalizedLang)) {
|
|
23
|
+
try {
|
|
24
|
+
const analyzerPath = path.join(__dirname, normalizedLang, 'analyzer.js');
|
|
25
|
+
const AnalyzerClass = require(analyzerPath);
|
|
26
|
+
this.analyzers.set(normalizedLang, new AnalyzerClass());
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return this.analyzers.get(normalizedLang);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
normalizeLanguage(language) {
|
|
36
|
+
if (typeof language !== 'string') {
|
|
37
|
+
return 'typescript';
|
|
38
|
+
}
|
|
39
|
+
const languageMap = {
|
|
40
|
+
'typescript': 'typescript',
|
|
41
|
+
'javascript': 'typescript',
|
|
42
|
+
'ts': 'typescript',
|
|
43
|
+
'js': 'typescript',
|
|
44
|
+
'dart': 'dart'
|
|
45
|
+
};
|
|
46
|
+
return languageMap[language.toLowerCase()] || language.toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
supportsLanguage(language) {
|
|
50
|
+
if (typeof language !== 'string') return false;
|
|
51
|
+
const supported = ['typescript', 'javascript', 'ts', 'js', 'dart'];
|
|
52
|
+
return supported.includes(language.toLowerCase());
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getSupportedLanguages() {
|
|
56
|
+
return ['typescript', 'javascript', 'dart'];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async analyze(files, language, options = {}) {
|
|
60
|
+
const analyzer = this.getAnalyzer(language);
|
|
61
|
+
if (!analyzer) return [];
|
|
62
|
+
if (typeof analyzer.analyze === 'function') {
|
|
63
|
+
return analyzer.analyze(files, language, options);
|
|
64
|
+
}
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async initialize(semanticEngineOrLanguage = null, semanticEngine = null) {
|
|
69
|
+
let engine = semanticEngine;
|
|
70
|
+
let lang = null;
|
|
71
|
+
|
|
72
|
+
if (typeof semanticEngineOrLanguage === 'string') {
|
|
73
|
+
lang = semanticEngineOrLanguage;
|
|
74
|
+
} else if (semanticEngineOrLanguage && typeof semanticEngineOrLanguage === 'object') {
|
|
75
|
+
engine = semanticEngineOrLanguage;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (lang) {
|
|
79
|
+
const analyzer = this.getAnalyzer(lang);
|
|
80
|
+
if (analyzer && typeof analyzer.initialize === 'function') {
|
|
81
|
+
await analyzer.initialize(engine);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = new S008Router();
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S008 TypeScript Analyzer - SVG Content Validation
|
|
3
|
+
*
|
|
4
|
+
* Detects unsafe SVG handling in TypeScript/JavaScript code.
|
|
5
|
+
*
|
|
6
|
+
* Rule: Ensure user-supplied SVG content is validated or sanitized
|
|
7
|
+
* to prevent script injection and other attacks.
|
|
8
|
+
*
|
|
9
|
+
* Detects:
|
|
10
|
+
* - SVG content rendered without sanitization
|
|
11
|
+
* - dangerouslySetInnerHTML with SVG
|
|
12
|
+
* - Direct DOM insertion of SVG content
|
|
13
|
+
* - Missing DOMPurify or similar sanitization
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
|
|
18
|
+
class TypeScriptS008Analyzer {
|
|
19
|
+
constructor(config = {}) {
|
|
20
|
+
this.ruleId = 'S008';
|
|
21
|
+
this.language = 'typescript';
|
|
22
|
+
this.config = {
|
|
23
|
+
checkDangerouslySetInnerHTML: config.checkDangerouslySetInnerHTML !== false,
|
|
24
|
+
checkDirectInsertion: config.checkDirectInsertion !== false,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
getMetadata() {
|
|
29
|
+
return {
|
|
30
|
+
ruleId: 'S008',
|
|
31
|
+
name: 'SVG Content Validation',
|
|
32
|
+
language: 'typescript',
|
|
33
|
+
description: 'Ensure user-supplied SVG content is sanitized before rendering'
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getConfig() {
|
|
38
|
+
return this.config;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async analyze(files, language, options = {}) {
|
|
42
|
+
const violations = [];
|
|
43
|
+
|
|
44
|
+
for (const filePath of files) {
|
|
45
|
+
if (!/\.(ts|tsx|js|jsx)$/i.test(filePath)) continue;
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
49
|
+
const fileViolations = this.analyzeContent(content, filePath);
|
|
50
|
+
violations.push(...fileViolations);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
// Skip files that can't be read
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return violations;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
analyzeContent(content, filePath) {
|
|
60
|
+
const violations = [];
|
|
61
|
+
const lines = content.split('\n');
|
|
62
|
+
|
|
63
|
+
// Skip test files
|
|
64
|
+
if (/\.(test|spec)\.(ts|js|tsx|jsx)$/i.test(filePath) ||
|
|
65
|
+
/__(tests?|mocks?)__/i.test(filePath)) {
|
|
66
|
+
return violations;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check if file uses DOMPurify or similar sanitization
|
|
70
|
+
const hasSanitizer = /(?:DOMPurify|sanitize|xss|purify)/i.test(content);
|
|
71
|
+
|
|
72
|
+
// Track SVG-related variables
|
|
73
|
+
const svgVars = new Set();
|
|
74
|
+
const contentLower = content.toLowerCase();
|
|
75
|
+
const hasSvgContext = contentLower.includes('svg') ||
|
|
76
|
+
contentLower.includes('.svg') ||
|
|
77
|
+
contentLower.includes('image/svg');
|
|
78
|
+
|
|
79
|
+
// Only analyze if there's SVG context
|
|
80
|
+
if (!hasSvgContext) {
|
|
81
|
+
return violations;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Patterns for detecting unsafe SVG handling
|
|
85
|
+
const patterns = [
|
|
86
|
+
{
|
|
87
|
+
// dangerouslySetInnerHTML with SVG content
|
|
88
|
+
regex: /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*(\w+)/g,
|
|
89
|
+
message: 'Unsafe SVG rendering: dangerouslySetInnerHTML without sanitization',
|
|
90
|
+
checkVar: true
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
// innerHTML assignment with SVG
|
|
94
|
+
regex: /\.innerHTML\s*=\s*(\w*[Ss]vg\w*)/g,
|
|
95
|
+
message: 'Unsafe SVG insertion: innerHTML assignment with SVG content'
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
// Direct SVG string insertion
|
|
99
|
+
regex: /\.innerHTML\s*=\s*['"`]<svg/g,
|
|
100
|
+
message: 'SVG string directly inserted via innerHTML - consider using sanitization'
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
// insertAdjacentHTML with SVG
|
|
104
|
+
regex: /insertAdjacentHTML\s*\([^)]*[Ss]vg/g,
|
|
105
|
+
message: 'Unsafe SVG insertion via insertAdjacentHTML'
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
// document.write with SVG
|
|
109
|
+
regex: /document\.write\s*\([^)]*[Ss]vg/g,
|
|
110
|
+
message: 'Unsafe SVG insertion via document.write'
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
// Blob URL creation with SVG without sanitization
|
|
114
|
+
regex: /new\s+Blob\s*\(\s*\[[^\]]*[Ss]vg/g,
|
|
115
|
+
message: 'SVG Blob created - ensure content is sanitized before creating Blob'
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
// SVG data URL without sanitization
|
|
119
|
+
regex: /data:image\/svg\+xml[^'"`]*\$\{/g,
|
|
120
|
+
message: 'Dynamic SVG data URL - ensure content is sanitized'
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
// User input directly used in SVG
|
|
124
|
+
regex: /(?:req\.(?:body|query|params)|userInput|formData)\s*\.\s*\w*[Ss]vg/g,
|
|
125
|
+
message: 'User-supplied SVG content used without validation'
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
// File upload SVG without validation
|
|
129
|
+
regex: /(?:multer|upload|file)\s*\([^)]*svg/gi,
|
|
130
|
+
message: 'SVG file upload - ensure content is validated and sanitized'
|
|
131
|
+
}
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
lines.forEach((line, lineIndex) => {
|
|
135
|
+
// Skip comments
|
|
136
|
+
if (/^\s*(\/\/|\/\*|\*)/.test(line)) return;
|
|
137
|
+
|
|
138
|
+
// Skip if line has sanitization
|
|
139
|
+
if (/DOMPurify|sanitize|purify/i.test(line)) return;
|
|
140
|
+
|
|
141
|
+
for (const pattern of patterns) {
|
|
142
|
+
let match;
|
|
143
|
+
pattern.regex.lastIndex = 0;
|
|
144
|
+
while ((match = pattern.regex.exec(line)) !== null) {
|
|
145
|
+
// For dangerouslySetInnerHTML, check if the variable is SVG-related
|
|
146
|
+
if (pattern.checkVar && match[1]) {
|
|
147
|
+
const varName = match[1].toLowerCase();
|
|
148
|
+
if (!varName.includes('svg') && !svgVars.has(match[1])) {
|
|
149
|
+
// Check if variable is assigned SVG content elsewhere
|
|
150
|
+
const varAssignment = new RegExp(`${match[1]}\\s*=.*svg`, 'i');
|
|
151
|
+
if (!varAssignment.test(content)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Skip if sanitizer is used nearby (within 5 lines)
|
|
158
|
+
let hasSanitizationNearby = false;
|
|
159
|
+
for (let i = Math.max(0, lineIndex - 5); i < Math.min(lines.length, lineIndex + 5); i++) {
|
|
160
|
+
if (/DOMPurify|sanitize|purify/i.test(lines[i])) {
|
|
161
|
+
hasSanitizationNearby = true;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (hasSanitizationNearby) continue;
|
|
166
|
+
|
|
167
|
+
violations.push({
|
|
168
|
+
ruleId: 'S008',
|
|
169
|
+
file: filePath,
|
|
170
|
+
line: lineIndex + 1,
|
|
171
|
+
column: match.index + 1,
|
|
172
|
+
severity: 'warning',
|
|
173
|
+
message: pattern.message,
|
|
174
|
+
suggestion: 'Use DOMPurify or similar library to sanitize SVG content before rendering'
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// Check for SVG element creation without sanitization
|
|
181
|
+
if (!hasSanitizer) {
|
|
182
|
+
lines.forEach((line, lineIndex) => {
|
|
183
|
+
// Skip comments
|
|
184
|
+
if (/^\s*(\/\/|\/\*|\*)/.test(line)) return;
|
|
185
|
+
|
|
186
|
+
// Check for creating SVG element and setting content
|
|
187
|
+
if (/createElementNS\s*\([^)]*svg/i.test(line) ||
|
|
188
|
+
/createElement\s*\(\s*['"`]svg['"`]/i.test(line)) {
|
|
189
|
+
// Look ahead for innerHTML or textContent assignment
|
|
190
|
+
for (let i = lineIndex; i < Math.min(lines.length, lineIndex + 10); i++) {
|
|
191
|
+
if (/\.innerHTML\s*=/.test(lines[i]) && !/sanitize|purify/i.test(lines[i])) {
|
|
192
|
+
violations.push({
|
|
193
|
+
ruleId: 'S008',
|
|
194
|
+
file: filePath,
|
|
195
|
+
line: i + 1,
|
|
196
|
+
column: 1,
|
|
197
|
+
severity: 'warning',
|
|
198
|
+
message: 'SVG element content set without sanitization',
|
|
199
|
+
suggestion: 'Sanitize content before setting innerHTML on SVG elements'
|
|
200
|
+
});
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return violations;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
supportsLanguage(language) {
|
|
212
|
+
return ['typescript', 'javascript', 'ts', 'js'].includes(language.toLowerCase());
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = TypeScriptS008Analyzer;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S018 Dart Analyzer - No Sensitive Data in Browser Storage
|
|
3
|
+
*
|
|
4
|
+
* This is a JS wrapper that delegates to DartAnalyzer binary.
|
|
5
|
+
* Actual implementation: dart_analyzer/lib/rules/security/S018_no_sensitive_browser_storage.dart
|
|
6
|
+
*
|
|
7
|
+
* Rule: Do not store sensitive data in browser storage (SharedPreferences, etc.)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
class DartS018Analyzer {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.ruleId = 'S018';
|
|
13
|
+
this.language = 'dart';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getMetadata() {
|
|
17
|
+
return {
|
|
18
|
+
ruleId: 'S018',
|
|
19
|
+
name: 'No Sensitive Data in Browser Storage',
|
|
20
|
+
language: 'dart',
|
|
21
|
+
delegateTo: 'dart_analyzer',
|
|
22
|
+
description: 'Prevent sensitive data from being stored in client-side storage'
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getConfig() {
|
|
27
|
+
return {
|
|
28
|
+
checkSharedPreferences: true,
|
|
29
|
+
checkSecureStorage: true,
|
|
30
|
+
severity: 'error'
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async analyze(files, language, options) {
|
|
35
|
+
// Delegated to DartAnalyzer binary via heuristic-engine.js
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
supportsLanguage(language) {
|
|
40
|
+
return language === 'dart';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = DartS018Analyzer;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S018 Rule Router - No Sensitive Data in Browser Storage
|
|
3
|
+
*
|
|
4
|
+
* Routes analysis to the appropriate language-specific analyzer.
|
|
5
|
+
* Supports: TypeScript, JavaScript, Dart
|
|
6
|
+
*
|
|
7
|
+
* Rule: Do not store sensitive data in browser storage (localStorage, sessionStorage, IndexedDB)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
class S018Router {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.analyzers = new Map();
|
|
15
|
+
this.ruleId = 'S018';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
getAnalyzer(language) {
|
|
19
|
+
const normalizedLang = this.normalizeLanguage(language);
|
|
20
|
+
|
|
21
|
+
if (!this.analyzers.has(normalizedLang)) {
|
|
22
|
+
try {
|
|
23
|
+
const analyzerPath = path.join(__dirname, normalizedLang, 'analyzer.js');
|
|
24
|
+
const AnalyzerClass = require(analyzerPath);
|
|
25
|
+
this.analyzers.set(normalizedLang, new AnalyzerClass());
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return this.analyzers.get(normalizedLang);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
normalizeLanguage(language) {
|
|
35
|
+
if (typeof language !== 'string') {
|
|
36
|
+
return 'typescript';
|
|
37
|
+
}
|
|
38
|
+
const languageMap = {
|
|
39
|
+
'typescript': 'typescript',
|
|
40
|
+
'javascript': 'typescript',
|
|
41
|
+
'ts': 'typescript',
|
|
42
|
+
'js': 'typescript',
|
|
43
|
+
'dart': 'dart'
|
|
44
|
+
};
|
|
45
|
+
return languageMap[language.toLowerCase()] || language.toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
supportsLanguage(language) {
|
|
49
|
+
if (typeof language !== 'string') return false;
|
|
50
|
+
const supported = ['typescript', 'javascript', 'ts', 'js', 'dart'];
|
|
51
|
+
return supported.includes(language.toLowerCase());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
getSupportedLanguages() {
|
|
55
|
+
return ['typescript', 'javascript', 'dart'];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async analyze(files, language, options = {}) {
|
|
59
|
+
const analyzer = this.getAnalyzer(language);
|
|
60
|
+
if (!analyzer) return [];
|
|
61
|
+
if (typeof analyzer.analyze === 'function') {
|
|
62
|
+
return analyzer.analyze(files, language, options);
|
|
63
|
+
}
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async initialize(semanticEngineOrLanguage = null, semanticEngine = null) {
|
|
68
|
+
let engine = semanticEngine;
|
|
69
|
+
let lang = null;
|
|
70
|
+
|
|
71
|
+
if (typeof semanticEngineOrLanguage === 'string') {
|
|
72
|
+
lang = semanticEngineOrLanguage;
|
|
73
|
+
} else if (semanticEngineOrLanguage && typeof semanticEngineOrLanguage === 'object') {
|
|
74
|
+
engine = semanticEngineOrLanguage;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (lang) {
|
|
78
|
+
const analyzer = this.getAnalyzer(lang);
|
|
79
|
+
if (analyzer && typeof analyzer.initialize === 'function') {
|
|
80
|
+
await analyzer.initialize(engine);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = new S018Router();
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S018 TypeScript Analyzer - No Sensitive Data in Browser Storage
|
|
3
|
+
*
|
|
4
|
+
* Detects sensitive data stored in browser storage mechanisms.
|
|
5
|
+
*
|
|
6
|
+
* Rule: Do not store sensitive data in localStorage, sessionStorage, or IndexedDB.
|
|
7
|
+
* Exception: Session tokens may be stored with proper safeguards.
|
|
8
|
+
*
|
|
9
|
+
* Detects:
|
|
10
|
+
* - Passwords/credentials in browser storage
|
|
11
|
+
* - PII (personal identifiable information) in storage
|
|
12
|
+
* - API keys or secrets in localStorage/sessionStorage
|
|
13
|
+
* - Financial data in client-side storage
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
|
|
18
|
+
class TypeScriptS018Analyzer {
|
|
19
|
+
constructor(config = {}) {
|
|
20
|
+
this.ruleId = 'S018';
|
|
21
|
+
this.language = 'typescript';
|
|
22
|
+
this.config = {
|
|
23
|
+
checkLocalStorage: config.checkLocalStorage !== false,
|
|
24
|
+
checkSessionStorage: config.checkSessionStorage !== false,
|
|
25
|
+
checkIndexedDB: config.checkIndexedDB !== false,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
getMetadata() {
|
|
30
|
+
return {
|
|
31
|
+
ruleId: 'S018',
|
|
32
|
+
name: 'No Sensitive Data in Browser Storage',
|
|
33
|
+
language: 'typescript',
|
|
34
|
+
description: 'Prevent sensitive data from being stored in browser storage'
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getConfig() {
|
|
39
|
+
return this.config;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async analyze(files, language, options = {}) {
|
|
43
|
+
const violations = [];
|
|
44
|
+
|
|
45
|
+
for (const filePath of files) {
|
|
46
|
+
if (!/\.(ts|tsx|js|jsx)$/i.test(filePath)) continue;
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
50
|
+
const fileViolations = this.analyzeContent(content, filePath);
|
|
51
|
+
violations.push(...fileViolations);
|
|
52
|
+
} catch (error) {
|
|
53
|
+
// Skip files that can't be read
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return violations;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
analyzeContent(content, filePath) {
|
|
61
|
+
const violations = [];
|
|
62
|
+
const lines = content.split('\n');
|
|
63
|
+
|
|
64
|
+
// Skip test files
|
|
65
|
+
if (/\.(test|spec)\.(ts|js|tsx|jsx)$/i.test(filePath) ||
|
|
66
|
+
/__(tests?|mocks?)__/i.test(filePath)) {
|
|
67
|
+
return violations;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Sensitive data patterns
|
|
71
|
+
const sensitivePatterns = [
|
|
72
|
+
'password', 'passwd', 'pwd',
|
|
73
|
+
'secret', 'apikey', 'api_key', 'api-key',
|
|
74
|
+
'private_key', 'privatekey', 'private-key',
|
|
75
|
+
'credit_card', 'creditcard', 'credit-card',
|
|
76
|
+
'card_number', 'cardnumber', 'cvv', 'cvc',
|
|
77
|
+
'ssn', 'social_security', 'socialSecurity',
|
|
78
|
+
'bank_account', 'bankaccount', 'bank-account',
|
|
79
|
+
'encryption_key', 'encryptionkey',
|
|
80
|
+
'auth_token', 'authtoken', // different from session token
|
|
81
|
+
'refresh_token', 'refreshtoken',
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Allowed patterns (session tokens are acceptable)
|
|
85
|
+
const allowedPatterns = [
|
|
86
|
+
'session_token', 'sessiontoken', 'session-token',
|
|
87
|
+
'access_token', 'accesstoken', 'access-token',
|
|
88
|
+
'jwt', 'bearer',
|
|
89
|
+
'theme', 'language', 'locale', 'preference',
|
|
90
|
+
'cart', 'wishlist'
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
// Storage methods to check
|
|
94
|
+
const storagePatterns = [
|
|
95
|
+
{
|
|
96
|
+
regex: /localStorage\.setItem\s*\(\s*['"`]([^'"`]+)['"`]\s*,/g,
|
|
97
|
+
storage: 'localStorage'
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
regex: /sessionStorage\.setItem\s*\(\s*['"`]([^'"`]+)['"`]\s*,/g,
|
|
101
|
+
storage: 'sessionStorage'
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
regex: /localStorage\s*\[\s*['"`]([^'"`]+)['"`]\s*\]\s*=/g,
|
|
105
|
+
storage: 'localStorage'
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
regex: /sessionStorage\s*\[\s*['"`]([^'"`]+)['"`]\s*\]\s*=/g,
|
|
109
|
+
storage: 'sessionStorage'
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
regex: /indexedDB\.open\s*\(\s*['"`]([^'"`]+)['"`]/g,
|
|
113
|
+
storage: 'IndexedDB'
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
regex: /\.put\s*\(\s*\{[^}]*(?:password|secret|apiKey|creditCard)[^}]*\}/gi,
|
|
117
|
+
storage: 'IndexedDB',
|
|
118
|
+
directMatch: true
|
|
119
|
+
}
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
lines.forEach((line, lineIndex) => {
|
|
123
|
+
// Skip comments
|
|
124
|
+
if (/^\s*(\/\/|\/\*|\*)/.test(line)) return;
|
|
125
|
+
|
|
126
|
+
for (const pattern of storagePatterns) {
|
|
127
|
+
let match;
|
|
128
|
+
pattern.regex.lastIndex = 0;
|
|
129
|
+
|
|
130
|
+
while ((match = pattern.regex.exec(line)) !== null) {
|
|
131
|
+
const key = match[1] ? match[1].toLowerCase() : '';
|
|
132
|
+
|
|
133
|
+
// Check if key matches sensitive patterns
|
|
134
|
+
let isSensitive = false;
|
|
135
|
+
let matchedPattern = '';
|
|
136
|
+
|
|
137
|
+
if (pattern.directMatch) {
|
|
138
|
+
isSensitive = true;
|
|
139
|
+
matchedPattern = 'sensitive data object';
|
|
140
|
+
} else {
|
|
141
|
+
for (const sensitive of sensitivePatterns) {
|
|
142
|
+
if (key.includes(sensitive.toLowerCase())) {
|
|
143
|
+
// Check if it's actually allowed
|
|
144
|
+
const isAllowed = allowedPatterns.some(allowed =>
|
|
145
|
+
key.includes(allowed.toLowerCase())
|
|
146
|
+
);
|
|
147
|
+
if (!isAllowed) {
|
|
148
|
+
isSensitive = true;
|
|
149
|
+
matchedPattern = sensitive;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (isSensitive) {
|
|
157
|
+
violations.push({
|
|
158
|
+
ruleId: 'S018',
|
|
159
|
+
file: filePath,
|
|
160
|
+
line: lineIndex + 1,
|
|
161
|
+
column: match.index + 1,
|
|
162
|
+
severity: 'error',
|
|
163
|
+
message: `Sensitive data "${matchedPattern}" stored in ${pattern.storage} - use server-side session instead`,
|
|
164
|
+
suggestion: 'Store sensitive data on the server, not in browser storage. Use HttpOnly cookies for session management.'
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Check for storing entire objects that might contain sensitive data
|
|
171
|
+
if (/localStorage\.setItem.*JSON\.stringify.*(?:user|credentials|auth)/i.test(line) ||
|
|
172
|
+
/sessionStorage\.setItem.*JSON\.stringify.*(?:user|credentials|auth)/i.test(line)) {
|
|
173
|
+
violations.push({
|
|
174
|
+
ruleId: 'S018',
|
|
175
|
+
file: filePath,
|
|
176
|
+
line: lineIndex + 1,
|
|
177
|
+
column: 1,
|
|
178
|
+
severity: 'warning',
|
|
179
|
+
message: 'Storing user/auth object in browser storage may expose sensitive data',
|
|
180
|
+
suggestion: 'Ensure the object does not contain passwords, tokens, or PII before storing'
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
return violations;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
supportsLanguage(language) {
|
|
189
|
+
return ['typescript', 'javascript', 'ts', 'js'].includes(language.toLowerCase());
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = TypeScriptS018Analyzer;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S021 Dart Analyzer - Referrer Policy
|
|
3
|
+
*
|
|
4
|
+
* This is a JS wrapper that delegates to DartAnalyzer binary.
|
|
5
|
+
* Actual implementation: dart_analyzer/lib/rules/security/S021_referrer_policy.dart
|
|
6
|
+
*
|
|
7
|
+
* Rule: Set Referrer-Policy to prevent sensitive data leakage
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
class DartS021Analyzer {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.ruleId = 'S021';
|
|
13
|
+
this.language = 'dart';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
getMetadata() {
|
|
17
|
+
return {
|
|
18
|
+
ruleId: 'S021',
|
|
19
|
+
name: 'Referrer Policy',
|
|
20
|
+
language: 'dart',
|
|
21
|
+
delegateTo: 'dart_analyzer',
|
|
22
|
+
description: 'Set Referrer-Policy to prevent sensitive data leakage'
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getConfig() {
|
|
27
|
+
return {
|
|
28
|
+
checkHeaders: true,
|
|
29
|
+
checkExternalLinks: true,
|
|
30
|
+
severity: 'warning'
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async analyze(files, language, options) {
|
|
35
|
+
// Delegated to DartAnalyzer binary via heuristic-engine.js
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
supportsLanguage(language) {
|
|
40
|
+
return language === 'dart';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = DartS021Analyzer;
|