hackmyagent 0.11.2 → 0.11.4
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/dist/cli.js +51 -61
- package/dist/cli.js.map +1 -1
- package/dist/hardening/scanner.d.ts.map +1 -1
- package/dist/hardening/scanner.js +107 -112
- package/dist/hardening/scanner.js.map +1 -1
- package/dist/hardening/taxonomy.d.ts.map +1 -1
- package/dist/hardening/taxonomy.js +67 -66
- package/dist/hardening/taxonomy.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/telemetry/contribute.d.ts +5 -0
- package/dist/telemetry/contribute.d.ts.map +1 -1
- package/dist/telemetry/contribute.js +34 -0
- package/dist/telemetry/contribute.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/hardening/scanner.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,UAAU,EAA0C,MAAM,kBAAkB,CAAC;AAkF3F,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,oDAAoD;IACpD,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;
|
|
1
|
+
{"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/hardening/scanner.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,OAAO,KAAK,EAAE,UAAU,EAA0C,MAAM,kBAAkB,CAAC;AAkF3F,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,oDAAoD;IACpD,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAoID,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,OAAO,CAAiB;IAEhC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAiBlC;IAEF;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAMvB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;YAmSvC,cAAc;IAwE5B;;OAEG;YACW,iBAAiB;IA+F/B;;OAEG;IACH,OAAO,CAAC,gBAAgB;YAeV,uBAAuB;YAmGvB,aAAa;YAgDb,cAAc;YA+Fd,oBAAoB;YAwDpB,gBAAgB;YA0IhB,oBAAoB;YAgFpB,gBAAgB;YA2IhB,mBAAmB;YA4EnB,iBAAiB;YAyCjB,iBAAiB;YA+DjB,wBAAwB;YA0FxB,wBAAwB;YAmExB,wBAAwB;YAqHxB,oBAAoB;YA+GpB,uBAAuB;YAwIvB,iBAAiB;YA8GjB,oBAAoB;YAsHpB,mBAAmB;YAiGnB,gBAAgB;YAmIhB,oBAAoB;YAoIpB,gBAAgB;YAyHhB,qBAAqB;YA+GrB,eAAe;IAiI7B;;OAEG;YACW,mBAAmB;IA8GjC;;OAEG;YACW,oBAAoB;IAiKlC;;OAEG;YACW,iBAAiB;IA4I/B;;OAEG;YACW,oBAAoB;IAwIlC;;OAEG;YACW,eAAe;IAqJ7B;;OAEG;YACW,eAAe;IAuI7B;;OAEG;YACW,eAAe;IAyG7B;;OAEG;YACW,mBAAmB;IAmHjC,OAAO,CAAC,cAAc;IAsBtB;;OAEG;YACW,YAAY;IAkD1B;;OAEG;IACG,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA6DhD;;;OAGG;YACW,cAAc;IAgD5B;;OAEG;YACW,mBAAmB;IAycjC;;;OAGG;YACW,kBAAkB;IAgDhC;;OAEG;YACW,sBAAsB;IA2LpC;;OAEG;YACW,sBAAsB;IA+BpC;;OAEG;YACW,oBAAoB;IAqVlC;;OAEG;IACH,OAAO,CAAC,mBAAmB;IA4B3B;;OAEG;YACW,iBAAiB;IA8D/B;;OAEG;YACW,mBAAmB;IA6VjC;;OAEG;YACW,wBAAwB;IA4OtC;;OAEG;YACW,gBAAgB;IA6J9B;;;OAGG;YACW,eAAe;IAoD7B;;;OAGG;YACW,aAAa;IAwC3B;;;OAGG;YACW,oBAAoB;IA+JlC;;;OAGG;YACW,iBAAiB;IA6H/B;;;OAGG;YACW,kBAAkB;IA+EhC;;;OAGG;YACW,aAAa;IAuF3B;;OAEG;YACW,gBAAgB;IA+D9B;;;;OAIG;YACW,yBAAyB;CAoWxC"}
|
|
@@ -218,6 +218,11 @@ const SEVERITY_WEIGHTS = {
|
|
|
218
218
|
};
|
|
219
219
|
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB max file size to prevent memory exhaustion
|
|
220
220
|
const MAX_LINE_LENGTH = 10000; // 10KB max line length for regex safety
|
|
221
|
+
/** Shell-escape a string for safe interpolation into advisory fix commands. */
|
|
222
|
+
function shellEscape(s) {
|
|
223
|
+
// Wrap in single quotes and escape embedded single quotes: ' -> '\''
|
|
224
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
225
|
+
}
|
|
221
226
|
class HardeningScanner {
|
|
222
227
|
constructor() {
|
|
223
228
|
this.cliName = 'hackmyagent';
|
|
@@ -3756,7 +3761,7 @@ dist/
|
|
|
3756
3761
|
await fs.access(backupBaseDir);
|
|
3757
3762
|
}
|
|
3758
3763
|
catch {
|
|
3759
|
-
throw new Error('No backup found. Run hackmyagent
|
|
3764
|
+
throw new Error('No backup found. Run hackmyagent secure --fix <dir> first to create a backup.');
|
|
3760
3765
|
}
|
|
3761
3766
|
// Find the most recent backup
|
|
3762
3767
|
const backups = await fs.readdir(backupBaseDir);
|
|
@@ -3765,7 +3770,7 @@ dist/
|
|
|
3765
3770
|
.sort()
|
|
3766
3771
|
.reverse();
|
|
3767
3772
|
if (sortedBackups.length === 0) {
|
|
3768
|
-
throw new Error('No backup found. Run hackmyagent
|
|
3773
|
+
throw new Error('No backup found. Run hackmyagent secure --fix <dir> first to create a backup.');
|
|
3769
3774
|
}
|
|
3770
3775
|
const latestBackup = sortedBackups[0];
|
|
3771
3776
|
const backupDir = path.join(backupBaseDir, latestBackup);
|
|
@@ -4883,8 +4888,8 @@ dist/
|
|
|
4883
4888
|
if (!this.isPathWithinDirectory(fullPath, targetDir)) {
|
|
4884
4889
|
continue;
|
|
4885
4890
|
}
|
|
4886
|
-
// Skip node_modules
|
|
4887
|
-
if (entryName === 'node_modules' || entryName === '.git') {
|
|
4891
|
+
// Skip node_modules, .git, and backup directories
|
|
4892
|
+
if (entryName === 'node_modules' || entryName === '.git' || entryName === '.hackmyagent-backup') {
|
|
4888
4893
|
continue;
|
|
4889
4894
|
}
|
|
4890
4895
|
let stat;
|
|
@@ -6224,11 +6229,8 @@ dist/
|
|
|
6224
6229
|
*/
|
|
6225
6230
|
async checkUnicodeSteganography(targetDir, _autoFix) {
|
|
6226
6231
|
const findings = [];
|
|
6227
|
-
// Scan
|
|
6228
|
-
const stegoExtensions = [
|
|
6229
|
-
'.js', '.ts', '.mjs', '.cjs', '.tsx', '.jsx',
|
|
6230
|
-
'.py', '.md', '.txt', '.yaml', '.yml', '.json', '.toml',
|
|
6231
|
-
];
|
|
6232
|
+
// Scan expanded file types beyond JS/TS (configs, docs, and Python are attack surfaces too)
|
|
6233
|
+
const stegoExtensions = ['.ts', '.js', '.mjs', '.cjs', '.tsx', '.jsx', '.py', '.md', '.txt', '.yaml', '.yml', '.json', '.toml'];
|
|
6232
6234
|
const sourceFiles = await this.walkDirectory(targetDir, stegoExtensions);
|
|
6233
6235
|
for (const filePath of sourceFiles) {
|
|
6234
6236
|
const relativePath = path.relative(targetDir, filePath);
|
|
@@ -6244,16 +6246,19 @@ dist/
|
|
|
6244
6246
|
continue;
|
|
6245
6247
|
// UNICODE-STEGO-001: Invisible Codepoint Detection
|
|
6246
6248
|
// Scan for:
|
|
6247
|
-
// -
|
|
6248
|
-
// -
|
|
6249
|
-
// -
|
|
6250
|
-
// -
|
|
6249
|
+
// - Variation selectors U+FE00-FE0F (UTF-8: EF B8 80-8F)
|
|
6250
|
+
// - Tag characters U+E0100-E01EF (UTF-8: F3 A0 84 80 - F3 A0 87 AF)
|
|
6251
|
+
// - Zero-width chars: U+200B (E2 80 8B), U+200C (E2 80 8C), U+200D (E2 80 8D)
|
|
6252
|
+
// - Mid-file BOM: U+FEFF (EF BB BF) -- skip offset 0
|
|
6253
|
+
// - Bidi overrides: U+202A-202E (E2 80 AA-AE), U+2066-2069 (E2 81 A6-A9)
|
|
6251
6254
|
let hasVariationSelectors = false;
|
|
6252
6255
|
let variationSelectorLine = 1;
|
|
6253
6256
|
let hasTagCharsIn001 = false;
|
|
6254
6257
|
let tagCharLine001 = 1;
|
|
6255
6258
|
let hasZeroWidth = false;
|
|
6256
6259
|
let zeroWidthLine = 1;
|
|
6260
|
+
let hasMidFileBom = false;
|
|
6261
|
+
let midFileBomLine = 1;
|
|
6257
6262
|
let hasBidiOverride = false;
|
|
6258
6263
|
let bidiOverrideLine = 1;
|
|
6259
6264
|
let currentLine = 1;
|
|
@@ -6262,7 +6267,29 @@ dist/
|
|
|
6262
6267
|
currentLine++;
|
|
6263
6268
|
continue;
|
|
6264
6269
|
}
|
|
6265
|
-
//
|
|
6270
|
+
// Variation selectors: EF B8 80-8F
|
|
6271
|
+
if (rawBuffer[i] === 0xEF &&
|
|
6272
|
+
i + 2 < rawBuffer.length &&
|
|
6273
|
+
rawBuffer[i + 1] === 0xB8 &&
|
|
6274
|
+
rawBuffer[i + 2] >= 0x80 &&
|
|
6275
|
+
rawBuffer[i + 2] <= 0x8F) {
|
|
6276
|
+
if (!hasVariationSelectors) {
|
|
6277
|
+
hasVariationSelectors = true;
|
|
6278
|
+
variationSelectorLine = currentLine;
|
|
6279
|
+
}
|
|
6280
|
+
}
|
|
6281
|
+
// Tag characters in U+E0100-E01EF: F3 A0 84 80 through F3 A0 87 AF
|
|
6282
|
+
if (rawBuffer[i] === 0xF3 &&
|
|
6283
|
+
i + 2 < rawBuffer.length &&
|
|
6284
|
+
rawBuffer[i + 1] === 0xA0 &&
|
|
6285
|
+
rawBuffer[i + 2] >= 0x84 &&
|
|
6286
|
+
rawBuffer[i + 2] <= 0x87) {
|
|
6287
|
+
if (!hasTagCharsIn001) {
|
|
6288
|
+
hasTagCharsIn001 = true;
|
|
6289
|
+
tagCharLine001 = currentLine;
|
|
6290
|
+
}
|
|
6291
|
+
}
|
|
6292
|
+
// Zero-width chars: U+200B/200C/200D = E2 80 8B/8C/8D
|
|
6266
6293
|
if (rawBuffer[i] === 0xE2 &&
|
|
6267
6294
|
i + 2 < rawBuffer.length &&
|
|
6268
6295
|
rawBuffer[i + 1] === 0x80 &&
|
|
@@ -6273,18 +6300,18 @@ dist/
|
|
|
6273
6300
|
zeroWidthLine = currentLine;
|
|
6274
6301
|
}
|
|
6275
6302
|
}
|
|
6276
|
-
//
|
|
6277
|
-
if (
|
|
6303
|
+
// Mid-file BOM: U+FEFF = EF BB BF (skip if at offset 0)
|
|
6304
|
+
if (i > 0 &&
|
|
6305
|
+
rawBuffer[i] === 0xEF &&
|
|
6278
6306
|
i + 2 < rawBuffer.length &&
|
|
6279
6307
|
rawBuffer[i + 1] === 0xBB &&
|
|
6280
|
-
rawBuffer[i + 2] === 0xBF
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
zeroWidthLine = currentLine;
|
|
6308
|
+
rawBuffer[i + 2] === 0xBF) {
|
|
6309
|
+
if (!hasMidFileBom) {
|
|
6310
|
+
hasMidFileBom = true;
|
|
6311
|
+
midFileBomLine = currentLine;
|
|
6285
6312
|
}
|
|
6286
6313
|
}
|
|
6287
|
-
//
|
|
6314
|
+
// Bidi overrides: U+202A-202E = E2 80 AA-AE
|
|
6288
6315
|
if (rawBuffer[i] === 0xE2 &&
|
|
6289
6316
|
i + 2 < rawBuffer.length &&
|
|
6290
6317
|
rawBuffer[i + 1] === 0x80 &&
|
|
@@ -6295,7 +6322,7 @@ dist/
|
|
|
6295
6322
|
bidiOverrideLine = currentLine;
|
|
6296
6323
|
}
|
|
6297
6324
|
}
|
|
6298
|
-
//
|
|
6325
|
+
// Bidi isolates: U+2066-2069 = E2 81 A6-A9
|
|
6299
6326
|
if (rawBuffer[i] === 0xE2 &&
|
|
6300
6327
|
i + 2 < rawBuffer.length &&
|
|
6301
6328
|
rawBuffer[i + 1] === 0x81 &&
|
|
@@ -6306,62 +6333,42 @@ dist/
|
|
|
6306
6333
|
bidiOverrideLine = currentLine;
|
|
6307
6334
|
}
|
|
6308
6335
|
}
|
|
6309
|
-
// Variation selectors: EF B8 80-8F
|
|
6310
|
-
if (rawBuffer[i] === 0xEF &&
|
|
6311
|
-
i + 2 < rawBuffer.length &&
|
|
6312
|
-
rawBuffer[i + 1] === 0xB8 &&
|
|
6313
|
-
rawBuffer[i + 2] >= 0x80 &&
|
|
6314
|
-
rawBuffer[i + 2] <= 0x8F) {
|
|
6315
|
-
if (!hasVariationSelectors) {
|
|
6316
|
-
hasVariationSelectors = true;
|
|
6317
|
-
variationSelectorLine = currentLine;
|
|
6318
|
-
}
|
|
6319
|
-
}
|
|
6320
|
-
// Tag characters in U+E0100-E01EF: F3 A0 84 80 through F3 A0 87 AF
|
|
6321
|
-
if (rawBuffer[i] === 0xF3 &&
|
|
6322
|
-
i + 2 < rawBuffer.length &&
|
|
6323
|
-
rawBuffer[i + 1] === 0xA0 &&
|
|
6324
|
-
rawBuffer[i + 2] >= 0x84 &&
|
|
6325
|
-
rawBuffer[i + 2] <= 0x87) {
|
|
6326
|
-
if (!hasTagCharsIn001) {
|
|
6327
|
-
hasTagCharsIn001 = true;
|
|
6328
|
-
tagCharLine001 = currentLine;
|
|
6329
|
-
}
|
|
6330
|
-
}
|
|
6331
6336
|
}
|
|
6332
|
-
|
|
6337
|
+
// Bidi and variation/tag chars are critical; zero-width-only is high
|
|
6338
|
+
const hasCriticalInvisible = hasVariationSelectors || hasTagCharsIn001 || hasBidiOverride;
|
|
6339
|
+
const hasAnyInvisible = hasCriticalInvisible || hasZeroWidth || hasMidFileBom;
|
|
6340
|
+
if (hasAnyInvisible) {
|
|
6333
6341
|
const detectedTypes = [];
|
|
6334
|
-
if (hasZeroWidth)
|
|
6335
|
-
detectedTypes.push('zero-width characters (U+200B-U+200D, U+FEFF)');
|
|
6336
|
-
if (hasBidiOverride)
|
|
6337
|
-
detectedTypes.push('bidirectional overrides (U+202A-U+202E, U+2066-U+2069)');
|
|
6338
6342
|
if (hasVariationSelectors)
|
|
6339
6343
|
detectedTypes.push('variation selectors (U+FE00-FE0F)');
|
|
6340
6344
|
if (hasTagCharsIn001)
|
|
6341
6345
|
detectedTypes.push('tag characters (U+E0100-E01EF)');
|
|
6342
|
-
const lineNumbers = [];
|
|
6343
6346
|
if (hasZeroWidth)
|
|
6344
|
-
|
|
6347
|
+
detectedTypes.push('zero-width characters (U+200B-200D)');
|
|
6348
|
+
if (hasMidFileBom)
|
|
6349
|
+
detectedTypes.push('mid-file BOM (U+FEFF)');
|
|
6345
6350
|
if (hasBidiOverride)
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6351
|
+
detectedTypes.push('bidi overrides (U+202A-202E, U+2066-2069)');
|
|
6352
|
+
// Determine first line hit for reporting
|
|
6353
|
+
const firstLine = Math.min(...[
|
|
6354
|
+
hasVariationSelectors ? variationSelectorLine : Infinity,
|
|
6355
|
+
hasTagCharsIn001 ? tagCharLine001 : Infinity,
|
|
6356
|
+
hasZeroWidth ? zeroWidthLine : Infinity,
|
|
6357
|
+
hasMidFileBom ? midFileBomLine : Infinity,
|
|
6358
|
+
hasBidiOverride ? bidiOverrideLine : Infinity,
|
|
6359
|
+
]);
|
|
6353
6360
|
findings.push({
|
|
6354
6361
|
checkId: 'UNICODE-STEGO-001',
|
|
6355
6362
|
name: 'Invisible Unicode Codepoints Detected',
|
|
6356
|
-
description: 'Source file contains invisible Unicode codepoints that can hide malicious payloads (
|
|
6357
|
-
category: '
|
|
6358
|
-
severity,
|
|
6363
|
+
description: 'Source file contains invisible Unicode codepoints that can hide malicious payloads (GlassWorm attack vector)',
|
|
6364
|
+
category: 'unicode-stego',
|
|
6365
|
+
severity: hasCriticalInvisible ? 'critical' : 'high',
|
|
6359
6366
|
passed: false,
|
|
6360
6367
|
message: `Found ${detectedTypes.join(' and ')} in ${relativePath}`,
|
|
6361
6368
|
file: relativePath,
|
|
6362
|
-
line:
|
|
6369
|
+
line: firstLine,
|
|
6363
6370
|
fixable: false,
|
|
6364
|
-
fix: 'Inspect the file with a hex editor (e.g., xxd) to identify and remove invisible Unicode codepoints. Run: xxd ' + relativePath + ' | grep -iE "e280
|
|
6371
|
+
fix: 'Inspect the file with a hex editor (e.g., xxd) to identify and remove invisible Unicode codepoints. Run: xxd ' + shellEscape(relativePath) + ' | grep -iE "e280[8-9a-e]|efbb|efb8|f3a0"',
|
|
6365
6372
|
});
|
|
6366
6373
|
}
|
|
6367
6374
|
// UNICODE-STEGO-002: GlassWorm Decoder Pattern
|
|
@@ -6390,7 +6397,7 @@ dist/
|
|
|
6390
6397
|
checkId: 'UNICODE-STEGO-002',
|
|
6391
6398
|
name: 'GlassWorm Decoder Pattern Detected',
|
|
6392
6399
|
description: 'Source file contains .codePointAt() usage combined with Unicode variation selector or tag character hex literals - this is the decoder half of a GlassWorm attack',
|
|
6393
|
-
category: '
|
|
6400
|
+
category: 'unicode-stego',
|
|
6394
6401
|
severity: 'critical',
|
|
6395
6402
|
passed: false,
|
|
6396
6403
|
message: `Found GlassWorm decoder pattern (.codePointAt + hex range literals) in ${relativePath}`,
|
|
@@ -6444,14 +6451,14 @@ dist/
|
|
|
6444
6451
|
checkId: 'UNICODE-STEGO-003',
|
|
6445
6452
|
name: 'Eval on String with Hidden Payload',
|
|
6446
6453
|
description: 'eval() or Function() is called with a string that has very few visible characters but a large byte footprint - indicates invisible Unicode payload',
|
|
6447
|
-
category: '
|
|
6454
|
+
category: 'unicode-stego',
|
|
6448
6455
|
severity: 'critical',
|
|
6449
6456
|
passed: false,
|
|
6450
6457
|
message: `Found eval/Function with ${visibleChars} visible chars but ${byteLength} bytes in ${relativePath}`,
|
|
6451
6458
|
file: relativePath,
|
|
6452
6459
|
line: evalLine,
|
|
6453
6460
|
fixable: false,
|
|
6454
|
-
fix: 'Remove the eval/Function call and inspect the string argument with a hex editor. The string likely contains invisible Unicode characters encoding a malicious payload. Run: node -e "const fs=require(\'fs\'); const s=fs.readFileSync(
|
|
6461
|
+
fix: 'Remove the eval/Function call and inspect the string argument with a hex editor. The string likely contains invisible Unicode characters encoding a malicious payload. Run: node -e "const fs=require(\'fs\'); const s=fs.readFileSync(' + JSON.stringify(relativePath) + ',\'utf8\'); console.log([...s].filter(c=>c.codePointAt(0)>0x200).map(c=>c.codePointAt(0).toString(16)))"',
|
|
6455
6462
|
});
|
|
6456
6463
|
break; // One finding per file
|
|
6457
6464
|
}
|
|
@@ -6486,78 +6493,66 @@ dist/
|
|
|
6486
6493
|
checkId: 'UNICODE-STEGO-004',
|
|
6487
6494
|
name: 'Unicode Tag Character Block Detected',
|
|
6488
6495
|
description: 'Source file contains characters from the Unicode Tag block (U+E0000-U+E01EF) which have no visible rendering and can be used to hide data',
|
|
6489
|
-
category: '
|
|
6496
|
+
category: 'unicode-stego',
|
|
6490
6497
|
severity: 'high',
|
|
6491
6498
|
passed: false,
|
|
6492
6499
|
message: `Found Unicode tag block characters in ${relativePath}`,
|
|
6493
6500
|
file: relativePath,
|
|
6494
6501
|
line: tagBlockLine,
|
|
6495
6502
|
fixable: false,
|
|
6496
|
-
fix: 'Inspect the file with a hex editor to identify tag block characters (byte sequence starting with F3 A0). These characters are invisible and have no legitimate use in source code. Run: xxd ' + relativePath + ' | grep "f3a0"',
|
|
6503
|
+
fix: 'Inspect the file with a hex editor to identify tag block characters (byte sequence starting with F3 A0). These characters are invisible and have no legitimate use in source code. Run: xxd ' + shellEscape(relativePath) + ' | grep "f3a0"',
|
|
6497
6504
|
});
|
|
6498
6505
|
}
|
|
6499
6506
|
}
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
const
|
|
6516
|
-
let
|
|
6517
|
-
|
|
6518
|
-
content = await fs.readFile(filePath, 'utf-8');
|
|
6519
|
-
}
|
|
6520
|
-
catch {
|
|
6521
|
-
continue;
|
|
6522
|
-
}
|
|
6523
|
-
if (content.length > MAX_FILE_SIZE)
|
|
6524
|
-
continue;
|
|
6525
|
-
const lines = content.split('\n');
|
|
6526
|
-
const foundHomoglyphs = [];
|
|
6527
|
-
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
6528
|
-
const line = lines[lineIdx];
|
|
6507
|
+
// UNICODE-STEGO-005: Homoglyph Confusable Detection
|
|
6508
|
+
// Detect Cyrillic/Greek characters that look identical to Latin but have different codepoints.
|
|
6509
|
+
// These can be used to bypass code review and hide malicious identifiers.
|
|
6510
|
+
const homoglyphCodepoints = new Set([
|
|
6511
|
+
// Cyrillic uppercase that look like Latin: A, B, C, E, H, K, M, O, P, T, X
|
|
6512
|
+
0x0410, 0x0412, 0x0421, 0x0415, 0x041D, 0x041A, 0x041C, 0x041E, 0x0420, 0x0422, 0x0425,
|
|
6513
|
+
// Cyrillic lowercase that look like Latin: a, e, o, p, c, x
|
|
6514
|
+
0x0430, 0x0435, 0x043E, 0x0440, 0x0441, 0x0445,
|
|
6515
|
+
// Fullwidth Latin (U+FF21-FF3A, U+FF41-FF5A) -- spot-check common ones
|
|
6516
|
+
0xFF21, 0xFF22, 0xFF41, 0xFF42,
|
|
6517
|
+
]);
|
|
6518
|
+
let homoglyphFound = false;
|
|
6519
|
+
let homoglyphLine = 1;
|
|
6520
|
+
let homoglyphChar = '';
|
|
6521
|
+
const contentForHomoglyph = content || rawBuffer.toString('utf-8');
|
|
6522
|
+
const homoglyphLines = contentForHomoglyph.split('\n');
|
|
6523
|
+
for (let lineIdx = 0; lineIdx < homoglyphLines.length; lineIdx++) {
|
|
6524
|
+
const line = homoglyphLines[lineIdx];
|
|
6529
6525
|
if (line.length > MAX_LINE_LENGTH)
|
|
6530
6526
|
continue;
|
|
6527
|
+
// Skip comment lines
|
|
6531
6528
|
const trimmed = line.trimStart();
|
|
6532
6529
|
if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*'))
|
|
6533
6530
|
continue;
|
|
6534
6531
|
for (const ch of line) {
|
|
6535
6532
|
const cp = ch.codePointAt(0);
|
|
6536
|
-
if (
|
|
6537
|
-
|
|
6533
|
+
if (homoglyphCodepoints.has(cp)) {
|
|
6534
|
+
homoglyphFound = true;
|
|
6535
|
+
homoglyphLine = lineIdx + 1;
|
|
6536
|
+
homoglyphChar = `U+${cp.toString(16).toUpperCase().padStart(4, '0')}`;
|
|
6538
6537
|
break;
|
|
6539
6538
|
}
|
|
6540
6539
|
}
|
|
6541
|
-
if (
|
|
6540
|
+
if (homoglyphFound)
|
|
6542
6541
|
break;
|
|
6543
6542
|
}
|
|
6544
|
-
if (
|
|
6545
|
-
const examples = foundHomoglyphs
|
|
6546
|
-
.slice(0, 3)
|
|
6547
|
-
.map((h) => `U+${h.cp.toString(16).toUpperCase().padStart(4, '0')} (looks like '${h.lookalike}') at line ${h.line}`)
|
|
6548
|
-
.join(', ');
|
|
6543
|
+
if (homoglyphFound) {
|
|
6549
6544
|
findings.push({
|
|
6550
6545
|
checkId: 'UNICODE-STEGO-005',
|
|
6551
|
-
name: 'Homoglyph
|
|
6552
|
-
description: 'Source file contains non-Latin
|
|
6553
|
-
category: '
|
|
6546
|
+
name: 'Homoglyph Confusable Characters Detected',
|
|
6547
|
+
description: 'Source file contains characters from non-Latin scripts (Cyrillic, Greek, Fullwidth) that visually resemble Latin letters. These can be used to create identifiers that look identical in code review but behave differently at runtime.',
|
|
6548
|
+
category: 'unicode-stego',
|
|
6554
6549
|
severity: 'high',
|
|
6555
6550
|
passed: false,
|
|
6556
|
-
message: `Found ${
|
|
6551
|
+
message: `Found homoglyph confusable character (${homoglyphChar}) in ${relativePath} at line ${homoglyphLine}`,
|
|
6557
6552
|
file: relativePath,
|
|
6558
|
-
line:
|
|
6553
|
+
line: homoglyphLine,
|
|
6559
6554
|
fixable: false,
|
|
6560
|
-
fix: '
|
|
6555
|
+
fix: 'Inspect the file for characters that look like Latin letters but are actually Cyrillic, Greek, or Fullwidth. Replace them with their ASCII equivalents. Run: node -e "const fs=require(\'fs\'); [...fs.readFileSync(' + JSON.stringify(relativePath) + ',\'utf8\')].forEach((c,i)=>{const cp=c.codePointAt(0); if(cp>0x7F && cp<0xFFFF) console.log(i, cp.toString(16), c)})"',
|
|
6561
6556
|
});
|
|
6562
6557
|
}
|
|
6563
6558
|
}
|