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.
@@ -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;AA8HD,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;CAuWxC"}
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 harden --fix <dir> first to create a backup.');
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 harden --fix <dir> first to create a backup.');
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 and .git directories
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 a broad set of file types for unicode stego -- not just JS/TS
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
- // - Zero-width characters: U+200B (ZWSP), U+200C (ZWNJ), U+200D (ZWJ), U+FEFF (BOM mid-file)
6248
- // - Variation selectors U+FE00-FE0F
6249
- // - Tag characters U+E0100-E01EF
6250
- // - Bidirectional override chars U+202A-U+202E and U+2066-U+2069
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
- // Zero-width characters: E2 80 8B (ZWSP), E2 80 8C (ZWNJ), E2 80 8D (ZWJ)
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
- // BOM / Zero-width no-break space U+FEFF: EF BB BF (only suspicious mid-file)
6277
- if (rawBuffer[i] === 0xEF &&
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
- i > 0) {
6282
- if (!hasZeroWidth) {
6283
- hasZeroWidth = true;
6284
- zeroWidthLine = currentLine;
6308
+ rawBuffer[i + 2] === 0xBF) {
6309
+ if (!hasMidFileBom) {
6310
+ hasMidFileBom = true;
6311
+ midFileBomLine = currentLine;
6285
6312
  }
6286
6313
  }
6287
- // Bidirectional override chars U+202A-U+202E: E2 80 AA-AE
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
- // Bidirectional isolate chars U+2066-U+2069: E2 81 A6-A9
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
- if (hasZeroWidth || hasVariationSelectors || hasTagCharsIn001 || hasBidiOverride) {
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
- lineNumbers.push(zeroWidthLine);
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
- lineNumbers.push(bidiOverrideLine);
6347
- if (hasVariationSelectors)
6348
- lineNumbers.push(variationSelectorLine);
6349
- if (hasTagCharsIn001)
6350
- lineNumbers.push(tagCharLine001);
6351
- const earliestLine = Math.min(...lineNumbers);
6352
- const severity = hasBidiOverride || hasVariationSelectors || hasTagCharsIn001 ? 'critical' : 'high';
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 (zero-width characters, bidirectional overrides, variation selectors, or tag characters)',
6357
- category: 'supply-chain',
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: earliestLine,
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 8[bcd]|efbb bf|e280 a[a-e]|e281 a[6-9]|efb8 8|f3a0"',
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: 'supply-chain',
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: 'supply-chain',
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(\'' + relativePath + '\',\'utf8\'); console.log([...s].filter(c=>c.codePointAt(0)>0x200).map(c=>c.codePointAt(0).toString(16)))"',
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: 'supply-chain',
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
- // UNICODE-STEGO-005: Homoglyph Substitution Detection
6502
- // Detect Cyrillic/Greek/fullwidth letters that look identical to Latin letters.
6503
- // Only scan code files where identifiers matter.
6504
- const codeExtensions = new Set(['.js', '.ts', '.mjs', '.cjs', '.tsx', '.jsx', '.py']);
6505
- const codeFiles = sourceFiles.filter((f) => codeExtensions.has(path.extname(f).toLowerCase()));
6506
- const HOMOGLYPHS = {
6507
- 0x0430: 'a', 0x0435: 'e', 0x043E: 'o', 0x0440: 'p',
6508
- 0x0441: 'c', 0x0443: 'y', 0x0445: 'x', 0x0456: 'i',
6509
- 0x0455: 's', 0x04BB: 'h', 0x0501: 'd', 0x051B: 'q',
6510
- 0x03B1: 'a', 0x03BF: 'o', 0x03C1: 'p',
6511
- 0xFF41: 'a', 0xFF45: 'e', 0xFF4F: 'o',
6512
- };
6513
- const homoglyphCodes = new Set(Object.keys(HOMOGLYPHS).map(Number));
6514
- for (const filePath of codeFiles) {
6515
- const relativePath = path.relative(targetDir, filePath);
6516
- let content;
6517
- try {
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 (homoglyphCodes.has(cp)) {
6537
- foundHomoglyphs.push({ line: lineIdx + 1, lookalike: HOMOGLYPHS[cp], cp });
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 (foundHomoglyphs.length >= 5)
6540
+ if (homoglyphFound)
6542
6541
  break;
6543
6542
  }
6544
- if (foundHomoglyphs.length > 0) {
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 Character Substitution Detected',
6552
- description: 'Source file contains non-Latin characters (Cyrillic, Greek, or fullwidth) that visually mimic Latin letters. This can disguise malicious identifiers or imports.',
6553
- category: 'supply-chain',
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 ${foundHomoglyphs.length} homoglyph substitution(s) in ${relativePath}: ${examples}`,
6551
+ message: `Found homoglyph confusable character (${homoglyphChar}) in ${relativePath} at line ${homoglyphLine}`,
6557
6552
  file: relativePath,
6558
- line: foundHomoglyphs[0].line,
6553
+ line: homoglyphLine,
6559
6554
  fixable: false,
6560
- fix: 'Replace non-Latin lookalike characters with their ASCII equivalents. Run: node -e "const s=require(\'fs\').readFileSync(\'' + relativePath + '\',\'utf8\');[...s].forEach((c,i)=>{const cp=c.codePointAt(0);if(cp>0x7F&&cp<0xFFFF)console.log(\'offset\',i,\'U+\'+cp.toString(16),JSON.stringify(c))})"',
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
  }