i18ntk 1.10.1 → 2.0.2

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 (110) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +141 -1185
  3. package/main/i18ntk-analyze.js +149 -133
  4. package/main/i18ntk-backup-class.js +420 -0
  5. package/main/i18ntk-backup.js +4 -4
  6. package/main/i18ntk-complete.js +90 -65
  7. package/main/i18ntk-doctor.js +123 -103
  8. package/main/i18ntk-fixer.js +61 -725
  9. package/main/i18ntk-go.js +14 -15
  10. package/main/i18ntk-init.js +76 -25
  11. package/main/i18ntk-java.js +27 -32
  12. package/main/i18ntk-js.js +70 -68
  13. package/main/i18ntk-manage.js +128 -29
  14. package/main/i18ntk-php.js +75 -75
  15. package/main/i18ntk-py.js +55 -56
  16. package/main/i18ntk-scanner.js +59 -57
  17. package/main/i18ntk-setup.js +10 -396
  18. package/main/i18ntk-sizing.js +46 -40
  19. package/main/i18ntk-summary.js +21 -18
  20. package/main/i18ntk-ui.js +11 -10
  21. package/main/i18ntk-usage.js +55 -19
  22. package/main/i18ntk-validate.js +13 -13
  23. package/main/manage/commands/AnalyzeCommand.js +1124 -0
  24. package/main/manage/commands/BackupCommand.js +62 -0
  25. package/main/manage/commands/CommandRouter.js +295 -0
  26. package/main/manage/commands/CompleteCommand.js +61 -0
  27. package/main/manage/commands/DoctorCommand.js +60 -0
  28. package/main/manage/commands/FixerCommand.js +624 -0
  29. package/main/manage/commands/InitCommand.js +62 -0
  30. package/main/manage/commands/ScannerCommand.js +654 -0
  31. package/main/manage/commands/SizingCommand.js +60 -0
  32. package/main/manage/commands/SummaryCommand.js +61 -0
  33. package/main/manage/commands/UsageCommand.js +60 -0
  34. package/main/manage/commands/ValidateCommand.js +978 -0
  35. package/main/manage/index-fixed.js +1447 -0
  36. package/main/manage/index.js +1462 -0
  37. package/main/manage/managers/DebugMenu.js +140 -0
  38. package/main/manage/managers/InteractiveMenu.js +177 -0
  39. package/main/manage/managers/LanguageMenu.js +62 -0
  40. package/main/manage/managers/SettingsMenu.js +53 -0
  41. package/main/manage/services/AuthenticationService.js +263 -0
  42. package/main/manage/services/ConfigurationService-fixed.js +449 -0
  43. package/main/manage/services/ConfigurationService.js +449 -0
  44. package/main/manage/services/FileManagementService.js +368 -0
  45. package/main/manage/services/FrameworkDetectionService.js +458 -0
  46. package/main/manage/services/InitService.js +1051 -0
  47. package/main/manage/services/SetupService.js +462 -0
  48. package/main/manage/services/SummaryService.js +450 -0
  49. package/main/manage/services/UsageService.js +1502 -0
  50. package/package.json +32 -30
  51. package/runtime/enhanced.d.ts +221 -221
  52. package/runtime/index.d.ts +29 -29
  53. package/runtime/index.full.d.ts +331 -331
  54. package/runtime/index.js +7 -6
  55. package/scripts/build-lite.js +17 -17
  56. package/scripts/deprecate-versions.js +23 -6
  57. package/scripts/export-translations.js +5 -5
  58. package/scripts/fix-all-i18n.js +3 -3
  59. package/scripts/fix-and-purify-i18n.js +3 -2
  60. package/scripts/fix-locale-control-chars.js +110 -0
  61. package/scripts/lint-locales.js +80 -0
  62. package/scripts/locale-optimizer.js +8 -8
  63. package/scripts/prepublish.js +21 -21
  64. package/scripts/security-check.js +13 -5
  65. package/scripts/sync-translations.js +4 -4
  66. package/scripts/sync-ui-locales.js +9 -8
  67. package/scripts/validate-all-translations.js +8 -7
  68. package/scripts/verify-deprecations.js +23 -15
  69. package/scripts/verify-translations.js +6 -5
  70. package/settings/i18ntk-config.json +282 -282
  71. package/settings/language-config.json +5 -5
  72. package/settings/settings-cli.js +9 -9
  73. package/settings/settings-manager.js +23 -20
  74. package/ui-locales/de.json +2417 -2348
  75. package/ui-locales/en.json +2415 -2352
  76. package/ui-locales/es.json +2425 -2353
  77. package/ui-locales/fr.json +2418 -2348
  78. package/ui-locales/ja.json +2463 -2361
  79. package/ui-locales/ru.json +2463 -2359
  80. package/ui-locales/zh.json +2418 -2351
  81. package/utils/admin-auth.js +2 -2
  82. package/utils/admin-cli.js +297 -297
  83. package/utils/admin-pin.js +9 -9
  84. package/utils/cli-helper.js +9 -9
  85. package/utils/config-helper.js +152 -103
  86. package/utils/config-manager.js +204 -164
  87. package/utils/config.js +5 -4
  88. package/utils/env-manager.js +256 -0
  89. package/utils/framework-detector.js +27 -24
  90. package/utils/i18n-helper.js +85 -41
  91. package/utils/init-helper.js +152 -94
  92. package/utils/json-output.js +98 -98
  93. package/utils/logger.js +6 -2
  94. package/utils/mini-commander.js +179 -0
  95. package/utils/missing-key-validator.js +5 -5
  96. package/utils/plugin-loader.js +29 -11
  97. package/utils/prompt.js +14 -44
  98. package/utils/safe-json.js +40 -0
  99. package/utils/secure-errors.js +3 -3
  100. package/utils/security-check-improved.js +390 -0
  101. package/utils/security-config.js +5 -5
  102. package/utils/security-fixed.js +607 -0
  103. package/utils/security.js +462 -248
  104. package/utils/setup-enforcer.js +136 -44
  105. package/utils/setup-validator.js +33 -32
  106. package/utils/terminal-icons.js +1 -1
  107. package/utils/ultra-performance-optimizer.js +11 -9
  108. package/utils/watch-locales.js +2 -1
  109. package/utils/prompt-fixed.js +0 -55
  110. package/utils/security-check.js +0 -450
@@ -775,14 +775,14 @@ class MissingKeyValidator {
775
775
  }
776
776
 
777
777
  async validateLanguageKeys(language) {
778
- const localePath = path.join(this.projectRoot, 'ui-locales', `${language}.json`);
778
+ const localePath = path.join(this.projectRoot, 'resources', 'i18n', 'ui-locales', `${language}.json`);
779
779
 
780
- if (!fs.existsSync(localePath)) {
780
+ if (!SecurityUtils.safeExistsSync(localePath)) {
781
781
  return this.requiredKeys; // All keys missing if file doesn't exist
782
782
  }
783
783
 
784
784
  try {
785
- const localeData = JSON.parse(fs.readFileSync(localePath, 'utf8'));
785
+ const localeData = JSON.parse(SecurityUtils.safeReadFileSync(localePath, 'utf8'));
786
786
  const existingKeys = Object.keys(localeData);
787
787
 
788
788
  return this.requiredKeys.filter(key => !existingKeys.includes(key));
@@ -807,7 +807,7 @@ class MissingKeyValidator {
807
807
  };
808
808
 
809
809
  const reportPath = path.join(__dirname, 'missing-keys-report.json');
810
- fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
810
+ SecurityUtils.safeWriteFileSync(reportPath, JSON.stringify(report, null, 2));
811
811
 
812
812
  console.log('\n📊 Missing Keys Report:');
813
813
  console.log(`Total Languages: ${report.summary.totalLanguages}`);
@@ -837,7 +837,7 @@ class MissingKeyValidator {
837
837
  });
838
838
 
839
839
  const templatePath = path.join(__dirname, 'translation-template.json');
840
- fs.writeFileSync(templatePath, JSON.stringify(template, null, 2));
840
+ SecurityUtils.safeWriteFileSync(templatePath, JSON.stringify(template, null, 2));
841
841
 
842
842
  console.log(`Translation template saved to: ${templatePath}`);
843
843
  return template;
@@ -1,13 +1,31 @@
1
- const path = require('path');
2
-
3
- function loadOptionalModule(name, cwd = process.cwd()) {
4
- try {
5
- const resolved = require.resolve(name, { paths: [cwd] });
6
- return require(resolved);
7
- } catch {
8
- return null;
9
- }
10
- }
1
+ function loadOptionalModule(name) {
2
+ // Security hardening: allow only known built-in optional plugins.
3
+ // This avoids dynamic runtime require of arbitrary package names.
4
+ const builtinPlugins = {
5
+ regex: () => require('./extractors/regex'),
6
+ 'i18ntk-extractor-regex': () => require('./extractors/regex')
7
+ };
8
+
9
+ const direct = builtinPlugins[name];
10
+ if (direct) {
11
+ try {
12
+ return direct();
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ // Sanitize the module name to prevent path traversal
19
+ const sanitizedName = name.replace(/[^a-zA-Z0-9@/_-]/g, '');
20
+ if (sanitizedName !== name) {
21
+ // If the name was changed, it was invalid
22
+ return null;
23
+ }
24
+
25
+ // Unknown optional modules are disabled by default.
26
+ // Register plugins programmatically through PluginLoader.registerPlugin instead.
27
+ return null;
28
+ }
11
29
  class PluginLoader {
12
30
  constructor() {
13
31
  this.plugins = {};
@@ -28,4 +46,4 @@ class PluginLoader {
28
46
  }
29
47
 
30
48
  module.exports = PluginLoader;
31
- module.exports.loadOptionalModule = loadOptionalModule;
49
+ module.exports.loadOptionalModule = loadOptionalModule;
package/utils/prompt.js CHANGED
@@ -1,9 +1,6 @@
1
- const readline = require('node:readline');
2
- const { stdin: input, stdout: output } = require('node:process');
1
+ const readline = require('readline');
2
+ const { logger } = require('./logger');
3
3
 
4
- /**
5
- * Simple prompt utility for CLI interactions
6
- */
7
4
  class Prompt {
8
5
  constructor() {
9
6
  this.rl = readline.createInterface({
@@ -26,60 +23,33 @@ class Prompt {
26
23
 
27
24
  async confirm(questionText, defaultValue = false) {
28
25
  const answer = await this.question(`${questionText} (${defaultValue ? 'Y/n' : 'y/N'}) `);
29
- const answer = await this.question(`${message} (${defaultValue ? 'Y/n' : 'y/N'}): `);
30
- return answer ? answer.toLowerCase().startsWith('y') : defaultValue;
31
- }
32
-
33
- async select(message, choices, defaultIndex = 0) {
34
- logger.log(`\n${message}:`);
35
- async confirm(question, defaultValue = false) {
36
- const answer = await this.question(`${question} (${defaultValue ? 'Y/n' : 'y/N'}) `);
37
26
  if (answer === '') return defaultValue;
38
27
  return /^y|yes$/i.test(answer);
39
28
  }
40
29
 
41
- /**
42
- * Present a list of choices and let the user select one
43
- * @param {string} question - The question to ask
44
- * @param {Array<string>} choices - Array of choices
45
- * @returns {Promise<string>} The selected choice
46
- */
47
- async list(question, choices) {
48
- if (!choices || !choices.length) {
49
- throw new Error('No choices provided');
50
- }
51
-
52
- console.log(question);
30
+ async select(questionText, choices, defaultIndex = 0) {
31
+ logger.log(`\n${questionText}:`);
53
32
  choices.forEach((choice, index) => {
54
- console.log(` ${index + 1}. ${choice}`);
33
+ logger.log(` ${index + 1}. ${choice}`);
55
34
  });
56
35
 
57
36
  while (true) {
58
- const answer = await this.question(`Select an option (1-${choices.length}): `);
59
- const index = parseInt(answer.trim(), 10) - 1;
60
-
61
- if (index >= 0 && index < choices.length) {
62
- return choices[index];
37
+ const answer = await this.question(`\nSelect an option (1-${choices.length}): `);
38
+ const selected = parseInt(answer, 10) - 1;
39
+ if (!isNaN(selected) && selected >= 0 && selected < choices.length) {
40
+ return selected;
63
41
  }
64
-
65
- console.log('Invalid selection. Please try again.');
42
+ logger.log('Invalid selection. Please try again.');
66
43
  }
67
44
  }
68
45
 
69
- /**
70
- * Close the readline interface
71
- */
72
- close() {
73
- this.rl.close();
46
+ async input(questionText, defaultValue = '') {
47
+ const answer = await this.question(`${questionText}${defaultValue ? ` [${defaultValue}]` : ''}: `);
48
+ return answer || defaultValue;
74
49
  }
75
50
  }
76
51
 
77
- // Create a singleton instance
78
52
  const prompt = new Prompt();
79
-
80
- // Handle process termination
81
- process.on('exit', () => {
82
- prompt.close();
83
- });
53
+ process.on('exit', () => prompt.close());
84
54
 
85
55
  module.exports = prompt;
@@ -0,0 +1,40 @@
1
+ // utils/safe-json.js
2
+ const { readFile } = require('fs/promises');
3
+
4
+ function stripBOM(s) {
5
+ if (typeof s === 'string' && s.charCodeAt(0) === 0xFEFF) return s.slice(1);
6
+ return s;
7
+ }
8
+
9
+ /**
10
+ * Safe JSON load with guardrails.
11
+ * - Max file size (default 1MB)
12
+ * - BOM stripping
13
+ * - Single, typed error (no loops)
14
+ */
15
+ async function readJsonSafe(filePath, { maxBytes = 1_000_000 } = {}) {
16
+ const buf = await readFile(filePath);
17
+ if (buf.length === 0) {
18
+ const err = new Error('Empty JSON file');
19
+ err.code = 'EJSONEMPTY';
20
+ err.path = filePath;
21
+ throw err;
22
+ }
23
+ if (buf.length > maxBytes) {
24
+ const err = new Error(`JSON too large (${buf.length} bytes)`);
25
+ err.code = 'EJSONTOOBIG';
26
+ err.path = filePath;
27
+ throw err;
28
+ }
29
+ try {
30
+ return JSON.parse(stripBOM(buf.toString('utf8')));
31
+ } catch (e) {
32
+ const err = new Error(`Invalid JSON`);
33
+ err.code = 'EJSONPARSE';
34
+ err.path = filePath;
35
+ err.cause = e;
36
+ throw err;
37
+ }
38
+ }
39
+
40
+ module.exports = { readJsonSafe };
@@ -76,7 +76,7 @@ function secureErrorHandler(options = {}) {
76
76
  const config = { ...defaults, ...options };
77
77
 
78
78
  // Ensure log directory exists
79
- if (config.logFilePath && !fs.existsSync(path.dirname(config.logFilePath))) {
79
+ if (config.logFilePath && !SecurityUtils.safeExistsSync(path.dirname(config.logFilePath))) {
80
80
  try {
81
81
  fs.mkdirSync(path.dirname(config.logFilePath), { recursive: true });
82
82
  } catch (e) {
@@ -126,12 +126,12 @@ function secureErrorHandler(options = {}) {
126
126
 
127
127
  // Log to console
128
128
  if (typeof config.logFunction === 'function') {
129
- config.logFunction(JSON.stringify(logEntry, null, 2));
129
+ SecurityUtils.safeWriteFileSync(config.logFilePath, JSON.stringify(logEntry, null, 2));
130
130
  }
131
131
 
132
132
  // Log to file if configured
133
133
  if (config.logFilePath) {
134
- fs.appendFileSync(
134
+ SecurityUtils.safeWriteFileSync(
135
135
  config.logFilePath,
136
136
  JSON.stringify(logEntry) + '\n',
137
137
  'utf8'
@@ -0,0 +1,390 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * i18ntk Security Check Utility - IMPROVED VERSION
5
+ * Performs comprehensive security validation before build/publish
6
+ * Enhanced to intelligently distinguish between safe and dangerous requires
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const crypto = require('crypto');
12
+
13
+ class SecurityChecker {
14
+ constructor() {
15
+ this.issues = [];
16
+ this.warnings = [];
17
+ this.projectRoot = path.resolve(__dirname, '..');
18
+ }
19
+
20
+ log(message, type = 'info') {
21
+ const timestamp = new Date().toISOString();
22
+ const colors = {
23
+ error: '\x1b[31m',
24
+ warning: '\x1b[33m',
25
+ success: '\x1b[32m',
26
+ info: '\x1b[36m',
27
+ reset: '\x1b[0m'
28
+ };
29
+ console.log(`${colors[type]}[${timestamp}] ${message}${colors.reset}`);
30
+ }
31
+
32
+ addIssue(message, file = null, line = null) {
33
+ this.issues.push({ message, file, line, type: 'error' });
34
+ }
35
+
36
+ addWarning(message, file = null, line = null) {
37
+ this.warnings.push({ message, file, line, type: 'warning' });
38
+ }
39
+
40
+ async checkFileExists(filePath) {
41
+ try {
42
+ await fs.promises.access(filePath);
43
+ return true;
44
+ } catch {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ async readFile(filePath) {
50
+ try {
51
+ return await fs.promises.readFile(filePath, 'utf8');
52
+ } catch (error) {
53
+ this.addIssue(`Cannot read file: ${filePath}`, filePath);
54
+ return null;
55
+ }
56
+ }
57
+
58
+ async checkPackageJson() {
59
+ this.log('Checking package.json security...');
60
+
61
+ const packageJsonPath = path.join(this.projectRoot, 'package.json');
62
+ const content = await this.readFile(packageJsonPath);
63
+
64
+ if (!content) return;
65
+
66
+ try {
67
+ const pkg = JSON.parse(content);
68
+
69
+ // Check for dangerous scripts
70
+ const dangerousScripts = ['preinstall', 'postinstall', 'preuninstall', 'postuninstall'];
71
+ const scripts = pkg.scripts || {};
72
+
73
+ for (const script of dangerousScripts) {
74
+ if (scripts[script]) {
75
+ this.addWarning(`Potentially dangerous script found: ${script}`, packageJsonPath);
76
+ }
77
+ }
78
+
79
+ // Check dependencies for known vulnerabilities (basic check)
80
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
81
+ for (const [dep, version] of Object.entries(allDeps || {})) {
82
+ if (version.includes('*') || version.includes('latest')) {
83
+ this.addWarning(`Unpinned dependency version: ${dep}@${version}`, packageJsonPath);
84
+ }
85
+ }
86
+
87
+ // Verify security scripts exist
88
+ const requiredScripts = ['security:check', 'security:test', 'security:audit'];
89
+ for (const script of requiredScripts) {
90
+ if (!scripts[script]) {
91
+ this.addIssue(`Missing required security script: ${script}`, packageJsonPath);
92
+ }
93
+ }
94
+
95
+ } catch (error) {
96
+ this.addIssue(`Invalid JSON in package.json: ${error.message}`, packageJsonPath);
97
+ }
98
+ }
99
+
100
+ async checkSecurityUtils() {
101
+ this.log('Checking SecurityUtils implementation...');
102
+
103
+ const securityUtilsPath = path.join(this.projectRoot, 'utils/security.js');
104
+ if (!(await this.checkFileExists(securityUtilsPath))) {
105
+ this.addIssue('SecurityUtils file not found', securityUtilsPath);
106
+ return;
107
+ }
108
+
109
+ const content = await this.readFile(securityUtilsPath);
110
+ if (!content) return;
111
+
112
+ // Check for required security methods
113
+ const requiredMethods = [
114
+ 'safeReadFileSync',
115
+ 'safeExistsSync',
116
+ 'safeWriteFileSync',
117
+ 'validatePath',
118
+ 'sanitizeInput',
119
+ 'safeParseJSON'
120
+ ];
121
+
122
+ for (const method of requiredMethods) {
123
+ if (!content.includes(method)) {
124
+ this.addIssue(`Missing security method: ${method}`, securityUtilsPath);
125
+ }
126
+ }
127
+
128
+ // Check for dangerous patterns (excluding the overly broad require pattern)
129
+ const dangerousPatterns = [
130
+ /fs\.readFileSync\s*\(/g,
131
+ /fs\.writeFileSync\s*\(/g,
132
+ /fs\.existsSync\s*\(/g,
133
+ /eval\s*\(/g,
134
+ /Function\s*\(/g
135
+ ];
136
+
137
+ for (const pattern of dangerousPatterns) {
138
+ const matches = content.match(pattern);
139
+ if (matches) {
140
+ this.addWarning(`Potentially unsafe pattern found: ${pattern}`, securityUtilsPath);
141
+ }
142
+ }
143
+ }
144
+
145
+ async checkSourceFiles() {
146
+ this.log('Checking source files for security issues...');
147
+
148
+ const sourceDirs = ['main', 'utils', 'scripts', 'settings'];
149
+ const excludeFiles = ['security.js', 'security-fixed.js', 'security-check.js', 'security-check-improved.js'];
150
+
151
+ for (const dir of sourceDirs) {
152
+ const dirPath = path.join(this.projectRoot, dir);
153
+ if (!(await this.checkFileExists(dirPath))) continue;
154
+
155
+ try {
156
+ const files = await fs.promises.readdir(dirPath);
157
+ for (const file of files) {
158
+ if (!file.endsWith('.js') || excludeFiles.includes(file)) continue;
159
+
160
+ const filePath = path.join(dirPath, file);
161
+ const content = await this.readFile(filePath);
162
+ if (!content) continue;
163
+
164
+ await this.analyzeFileSecurity(filePath, content);
165
+ }
166
+ } catch (error) {
167
+ this.addIssue(`Cannot read directory: ${dirPath}`, dirPath);
168
+ }
169
+ }
170
+ }
171
+
172
+ async analyzeFileSecurity(filePath, content) {
173
+ const lines = content.split('\n');
174
+
175
+ lines.forEach((line, index) => {
176
+ // Check for direct fs operations
177
+ if (line.includes('fs.readFileSync(') && !line.includes('SecurityUtils')) {
178
+ this.addIssue('Direct fs.readFileSync usage (use SecurityUtils.safeReadFileSync)', filePath, index + 1);
179
+ }
180
+ if (line.includes('fs.writeFileSync(') && !line.includes('SecurityUtils')) {
181
+ this.addIssue('Direct fs.writeFileSync usage (use SecurityUtils.safeWriteFileSync)', filePath, index + 1);
182
+ }
183
+ if (line.includes('fs.existsSync(') && !line.includes('SecurityUtils')) {
184
+ this.addIssue('Direct fs.existsSync usage (use SecurityUtils.safeExistsSync)', filePath, index + 1);
185
+ }
186
+
187
+ // Check for dangerous patterns
188
+ if (line.includes('eval(') || line.includes('Function(')) {
189
+ this.addIssue('Dangerous code execution pattern detected', filePath, index + 1);
190
+ }
191
+
192
+ // Check for unsafe require patterns - be more intelligent
193
+ if (line.includes('require(')) {
194
+ this.analyzeRequireStatement(line, filePath, index + 1);
195
+ }
196
+ });
197
+ }
198
+
199
+ analyzeRequireStatement(line, filePath, lineNumber) {
200
+ // Extract the require path
201
+ const requireMatch = line.match(/require\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/);
202
+ if (!requireMatch) return;
203
+
204
+ const requirePath = requireMatch[1];
205
+
206
+ // Skip safe built-in modules
207
+ // Note: child_process is intentionally excluded to keep runtime zero-shell
208
+ const safeBuiltins = ['fs', 'path', 'crypto', 'os', 'util', 'events', 'stream', 'buffer', 'http', 'https', 'url', 'querystring'];
209
+ if (safeBuiltins.includes(requirePath)) {
210
+ return; // Safe built-in module
211
+ }
212
+
213
+ // Skip safe relative requires within project structure
214
+ if (requirePath.startsWith('../') || requirePath.startsWith('./')) {
215
+ // Check if it's going too far up (more than 2 levels)
216
+ const upLevels = (requirePath.match(/\.\.\//g) || []).length;
217
+ if (upLevels > 2) {
218
+ this.addWarning('Deep relative require (more than 2 levels up)', filePath, lineNumber);
219
+ }
220
+ // Otherwise, relative requires within project are generally safe
221
+ return;
222
+ }
223
+
224
+ // Check for dynamic requires (variables)
225
+ if (requirePath.includes('${') || requirePath.includes('+') || requirePath.includes('variable')) {
226
+ this.addIssue('Dynamic require statement detected (potential security risk)', filePath, lineNumber);
227
+ return;
228
+ }
229
+
230
+ // Check for absolute paths outside node_modules
231
+ if (requirePath.startsWith('/') && !requirePath.includes('node_modules')) {
232
+ this.addWarning('Absolute path require outside node_modules', filePath, lineNumber);
233
+ return;
234
+ }
235
+
236
+ // Check for suspicious patterns
237
+ const suspiciousPatterns = [
238
+ /\.\./, // path traversal
239
+ /^~/, // home directory shorthand
240
+ /\$(HOME|USER)\b/, // shell env expansions
241
+ /^[a-z][a-z0-9+.-]*:/i // URL/protocol-like require targets
242
+ ];
243
+ for (const pattern of suspiciousPatterns) {
244
+ if (pattern.test(requirePath)) {
245
+ this.addIssue(`Suspicious require path pattern: ${pattern}`, filePath, lineNumber);
246
+ return;
247
+ }
248
+ }
249
+
250
+ // If we get here, it's likely a safe npm package require
251
+ // No action needed for legitimate package requires
252
+ }
253
+
254
+ async checkFilePermissions() {
255
+ this.log('Checking file permissions...');
256
+
257
+ const criticalFiles = [
258
+ 'utils/security.js',
259
+ 'tests/security.test.js',
260
+ 'package.json'
261
+ ];
262
+
263
+ for (const file of criticalFiles) {
264
+ const filePath = path.join(this.projectRoot, file);
265
+ if (!(await this.checkFileExists(filePath))) {
266
+ this.addIssue(`Critical file not found: ${file}`, filePath);
267
+ continue;
268
+ }
269
+
270
+ try {
271
+ const stats = await fs.promises.stat(filePath);
272
+ const permissions = (stats.mode & parseInt('777', 8)).toString(8);
273
+
274
+ // Check if file is writable by group or others
275
+ if (permissions[1] !== '0' || permissions[2] !== '0') {
276
+ this.addWarning(`File has overly permissive permissions: ${file} (${permissions})`, filePath);
277
+ }
278
+ } catch (error) {
279
+ this.addIssue(`Cannot check permissions for: ${file}`, filePath);
280
+ }
281
+ }
282
+ }
283
+
284
+ async checkDependencies() {
285
+ this.log('Checking for dependency vulnerabilities...');
286
+
287
+ const packageJsonPath = path.join(this.projectRoot, 'package.json');
288
+ const content = await this.readFile(packageJsonPath);
289
+
290
+ if (!content) return;
291
+
292
+ try {
293
+ const pkg = JSON.parse(content);
294
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
295
+
296
+ // Check for zero dependencies claim
297
+ if (Object.keys(allDeps || {}).length > 0) {
298
+ this.addWarning('Package claims zero dependencies but has dependencies in package.json');
299
+ }
300
+
301
+ // Check for suspicious dependency names
302
+ const suspiciousDeps = ['malicious', 'hack', 'exploit', 'trojan'];
303
+ for (const dep of Object.keys(allDeps || {})) {
304
+ for (const suspicious of suspiciousDeps) {
305
+ if (dep.toLowerCase().includes(suspicious)) {
306
+ this.addIssue(`Suspicious dependency name: ${dep}`);
307
+ }
308
+ }
309
+ }
310
+ } catch (error) {
311
+ this.addIssue(`Cannot parse package.json: ${error.message}`, packageJsonPath);
312
+ }
313
+ }
314
+
315
+ async run() {
316
+ this.log('Starting i18ntk Security Check (IMPROVED VERSION)...', 'info');
317
+
318
+ try {
319
+ await this.checkPackageJson();
320
+ await this.checkSecurityUtils();
321
+ await this.checkSourceFiles();
322
+ await this.checkFilePermissions();
323
+ await this.checkDependencies();
324
+
325
+ // Generate report
326
+ this.generateReport();
327
+
328
+ // Final status with detailed counts
329
+ const totalIssues = this.issues.length + this.warnings.length;
330
+ if (this.issues.length > 0) {
331
+ this.log(`Security check FAILED: ${this.issues.length} critical issues, ${this.warnings.length} warnings found`, 'error');
332
+ this.log(`Total: ${totalIssues} issues detected`, 'error');
333
+ // Ensure output is flushed before exit
334
+ await new Promise(resolve => setImmediate(resolve));
335
+ process.exit(1);
336
+ } else if (this.warnings.length > 0) {
337
+ this.log('Security check PASSED: No critical issues found', 'success');
338
+ this.log(`${this.warnings.length} warnings found (non-blocking)`, 'warning');
339
+ this.log(`Total: ${totalIssues} issues detected`, 'warning');
340
+ } else {
341
+ this.log('Security check PASSED: No issues found', 'success');
342
+ }
343
+ } catch (error) {
344
+ this.log(`Security check failed with error: ${error.message}`, 'error');
345
+ console.error('Stack trace:', error.stack);
346
+ process.exit(1);
347
+ }
348
+ }
349
+
350
+ generateReport() {
351
+ if (this.issues.length === 0 && this.warnings.length === 0) {
352
+ return;
353
+ }
354
+
355
+ console.log('\n=== SECURITY CHECK REPORT (IMPROVED) ===\n');
356
+
357
+ if (this.issues.length > 0) {
358
+ console.log('🔴 CRITICAL ISSUES:');
359
+ this.issues.forEach(issue => {
360
+ console.log(` • ${issue.message}`);
361
+ if (issue.file) {
362
+ console.log(` File: ${issue.file}${issue.line ? `:${issue.line}` : ''}`);
363
+ }
364
+ });
365
+ console.log('');
366
+ }
367
+
368
+ if (this.warnings.length > 0) {
369
+ console.log('🟡 WARNINGS:');
370
+ this.warnings.forEach(warning => {
371
+ console.log(` • ${warning.message}`);
372
+ if (warning.file) {
373
+ console.log(` File: ${warning.file}${warning.line ? `:${warning.line}` : ''}`);
374
+ }
375
+ });
376
+ console.log('');
377
+ }
378
+ }
379
+ }
380
+
381
+ // Run security check if called directly
382
+ if (require.main === module) {
383
+ const checker = new SecurityChecker();
384
+ checker.run().catch(error => {
385
+ console.error('Security check failed:', error);
386
+ process.exit(1);
387
+ });
388
+ }
389
+
390
+ module.exports = SecurityChecker;
@@ -124,7 +124,7 @@ class SecurityConfig {
124
124
 
125
125
  // Ensure config directory exists
126
126
  const configDir = path.dirname(this.configPath);
127
- if (!fs.existsSync(configDir)) {
127
+ if (!SecurityUtils.safeExistsSync(configDir)) {
128
128
  fs.mkdirSync(configDir, { recursive: true });
129
129
  }
130
130
 
@@ -138,7 +138,7 @@ class SecurityConfig {
138
138
  }
139
139
  };
140
140
 
141
- fs.writeFileSync(this.configPath, JSON.stringify(safeConfig, null, 2));
141
+ SecurityUtils.safeWriteFileSync(this.configPath, JSON.stringify(safeConfig, null, 2));
142
142
 
143
143
  return {
144
144
  configPath: this.configPath,
@@ -150,12 +150,12 @@ class SecurityConfig {
150
150
  * Load and validate existing configuration
151
151
  */
152
152
  loadSecurityConfig() {
153
- if (!fs.existsSync(this.configPath)) {
153
+ if (!SecurityUtils.safeExistsSync(this.configPath)) {
154
154
  return this.createSecureConfig();
155
155
  }
156
156
 
157
157
  try {
158
- const config = JSON.parse(fs.readFileSync(this.configPath, 'utf8'));
158
+ const config = JSON.parse(SecurityUtils.safeReadFileSync(this.configPath, path.dirname(this.configPath), 'utf8'));
159
159
  const validation = this.validateSecurityConfig(config);
160
160
 
161
161
  return {
@@ -178,7 +178,7 @@ class SecurityConfig {
178
178
  const timestamp = new Date().toISOString();
179
179
 
180
180
  // Create backup of old config
181
- if (fs.existsSync(this.configPath)) {
181
+ if (SecurityUtils.safeExistsSync(this.configPath)) {
182
182
  fs.copyFileSync(this.configPath, `${this.configPath}.backup.${timestamp}`);
183
183
  }
184
184