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.
- package/LICENSE +1 -1
- package/README.md +141 -1185
- package/main/i18ntk-analyze.js +149 -133
- package/main/i18ntk-backup-class.js +420 -0
- package/main/i18ntk-backup.js +4 -4
- package/main/i18ntk-complete.js +90 -65
- package/main/i18ntk-doctor.js +123 -103
- package/main/i18ntk-fixer.js +61 -725
- package/main/i18ntk-go.js +14 -15
- package/main/i18ntk-init.js +76 -25
- package/main/i18ntk-java.js +27 -32
- package/main/i18ntk-js.js +70 -68
- package/main/i18ntk-manage.js +128 -29
- package/main/i18ntk-php.js +75 -75
- package/main/i18ntk-py.js +55 -56
- package/main/i18ntk-scanner.js +59 -57
- package/main/i18ntk-setup.js +10 -396
- package/main/i18ntk-sizing.js +46 -40
- package/main/i18ntk-summary.js +21 -18
- package/main/i18ntk-ui.js +11 -10
- package/main/i18ntk-usage.js +55 -19
- package/main/i18ntk-validate.js +13 -13
- package/main/manage/commands/AnalyzeCommand.js +1124 -0
- package/main/manage/commands/BackupCommand.js +62 -0
- package/main/manage/commands/CommandRouter.js +295 -0
- package/main/manage/commands/CompleteCommand.js +61 -0
- package/main/manage/commands/DoctorCommand.js +60 -0
- package/main/manage/commands/FixerCommand.js +624 -0
- package/main/manage/commands/InitCommand.js +62 -0
- package/main/manage/commands/ScannerCommand.js +654 -0
- package/main/manage/commands/SizingCommand.js +60 -0
- package/main/manage/commands/SummaryCommand.js +61 -0
- package/main/manage/commands/UsageCommand.js +60 -0
- package/main/manage/commands/ValidateCommand.js +978 -0
- package/main/manage/index-fixed.js +1447 -0
- package/main/manage/index.js +1462 -0
- package/main/manage/managers/DebugMenu.js +140 -0
- package/main/manage/managers/InteractiveMenu.js +177 -0
- package/main/manage/managers/LanguageMenu.js +62 -0
- package/main/manage/managers/SettingsMenu.js +53 -0
- package/main/manage/services/AuthenticationService.js +263 -0
- package/main/manage/services/ConfigurationService-fixed.js +449 -0
- package/main/manage/services/ConfigurationService.js +449 -0
- package/main/manage/services/FileManagementService.js +368 -0
- package/main/manage/services/FrameworkDetectionService.js +458 -0
- package/main/manage/services/InitService.js +1051 -0
- package/main/manage/services/SetupService.js +462 -0
- package/main/manage/services/SummaryService.js +450 -0
- package/main/manage/services/UsageService.js +1502 -0
- package/package.json +32 -30
- package/runtime/enhanced.d.ts +221 -221
- package/runtime/index.d.ts +29 -29
- package/runtime/index.full.d.ts +331 -331
- package/runtime/index.js +7 -6
- package/scripts/build-lite.js +17 -17
- package/scripts/deprecate-versions.js +23 -6
- package/scripts/export-translations.js +5 -5
- package/scripts/fix-all-i18n.js +3 -3
- package/scripts/fix-and-purify-i18n.js +3 -2
- package/scripts/fix-locale-control-chars.js +110 -0
- package/scripts/lint-locales.js +80 -0
- package/scripts/locale-optimizer.js +8 -8
- package/scripts/prepublish.js +21 -21
- package/scripts/security-check.js +13 -5
- package/scripts/sync-translations.js +4 -4
- package/scripts/sync-ui-locales.js +9 -8
- package/scripts/validate-all-translations.js +8 -7
- package/scripts/verify-deprecations.js +23 -15
- package/scripts/verify-translations.js +6 -5
- package/settings/i18ntk-config.json +282 -282
- package/settings/language-config.json +5 -5
- package/settings/settings-cli.js +9 -9
- package/settings/settings-manager.js +23 -20
- package/ui-locales/de.json +2417 -2348
- package/ui-locales/en.json +2415 -2352
- package/ui-locales/es.json +2425 -2353
- package/ui-locales/fr.json +2418 -2348
- package/ui-locales/ja.json +2463 -2361
- package/ui-locales/ru.json +2463 -2359
- package/ui-locales/zh.json +2418 -2351
- package/utils/admin-auth.js +2 -2
- package/utils/admin-cli.js +297 -297
- package/utils/admin-pin.js +9 -9
- package/utils/cli-helper.js +9 -9
- package/utils/config-helper.js +152 -103
- package/utils/config-manager.js +204 -164
- package/utils/config.js +5 -4
- package/utils/env-manager.js +256 -0
- package/utils/framework-detector.js +27 -24
- package/utils/i18n-helper.js +85 -41
- package/utils/init-helper.js +152 -94
- package/utils/json-output.js +98 -98
- package/utils/logger.js +6 -2
- package/utils/mini-commander.js +179 -0
- package/utils/missing-key-validator.js +5 -5
- package/utils/plugin-loader.js +29 -11
- package/utils/prompt.js +14 -44
- package/utils/safe-json.js +40 -0
- package/utils/secure-errors.js +3 -3
- package/utils/security-check-improved.js +390 -0
- package/utils/security-config.js +5 -5
- package/utils/security-fixed.js +607 -0
- package/utils/security.js +462 -248
- package/utils/setup-enforcer.js +136 -44
- package/utils/setup-validator.js +33 -32
- package/utils/terminal-icons.js +1 -1
- package/utils/ultra-performance-optimizer.js +11 -9
- package/utils/watch-locales.js +2 -1
- package/utils/prompt-fixed.js +0 -55
- 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 (!
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
840
|
+
SecurityUtils.safeWriteFileSync(templatePath, JSON.stringify(template, null, 2));
|
|
841
841
|
|
|
842
842
|
console.log(`Translation template saved to: ${templatePath}`);
|
|
843
843
|
return template;
|
package/utils/plugin-loader.js
CHANGED
|
@@ -1,13 +1,31 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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('
|
|
2
|
-
const {
|
|
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
|
-
|
|
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
|
-
|
|
33
|
+
logger.log(` ${index + 1}. ${choice}`);
|
|
55
34
|
});
|
|
56
35
|
|
|
57
36
|
while (true) {
|
|
58
|
-
const answer = await this.question(
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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 };
|
package/utils/secure-errors.js
CHANGED
|
@@ -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 && !
|
|
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.
|
|
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
|
-
|
|
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;
|
package/utils/security-config.js
CHANGED
|
@@ -124,7 +124,7 @@ class SecurityConfig {
|
|
|
124
124
|
|
|
125
125
|
// Ensure config directory exists
|
|
126
126
|
const configDir = path.dirname(this.configPath);
|
|
127
|
-
if (!
|
|
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
|
-
|
|
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 (!
|
|
153
|
+
if (!SecurityUtils.safeExistsSync(this.configPath)) {
|
|
154
154
|
return this.createSecureConfig();
|
|
155
155
|
}
|
|
156
156
|
|
|
157
157
|
try {
|
|
158
|
-
const config = JSON.parse(
|
|
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 (
|
|
181
|
+
if (SecurityUtils.safeExistsSync(this.configPath)) {
|
|
182
182
|
fs.copyFileSync(this.configPath, `${this.configPath}.backup.${timestamp}`);
|
|
183
183
|
}
|
|
184
184
|
|