hackmyagent 0.11.0 → 0.11.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.
@@ -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;CA4NxC"}
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"}
@@ -3909,7 +3909,7 @@ dist/
3909
3909
  fixable: true,
3910
3910
  fixed: skill001Fixed,
3911
3911
  fixMessage: skill001Fixed ? 'Added SHA-256 signature block to skill file' : undefined,
3912
- fix: 'Sign the skill using: openclaw sign skill.md --key ~/.openclaw/signing-key.pem',
3912
+ fix: 'Run `hackmyagent fix-all --with-aim` to automatically sign all skill files with a cryptographic identity',
3913
3913
  });
3914
3914
  // SKILL-002: Remote Fetch Pattern
3915
3915
  for (let i = 0; i < lines.length; i++) {
@@ -4391,7 +4391,7 @@ dist/
4391
4391
  : 'Heartbeat lacks hash pinning - content integrity cannot be verified',
4392
4392
  file: relativePath,
4393
4393
  fixable: false,
4394
- fix: 'Add pinned_hash: sha256:<hash> to verify heartbeat content integrity',
4394
+ fix: 'Run `hackmyagent fix-all --with-aim` to automatically pin and sign heartbeat files',
4395
4395
  });
4396
4396
  // HEARTBEAT-003: Unsigned Heartbeat
4397
4397
  const hasSignature = content.includes('opena2a_signature:') ||
@@ -4409,7 +4409,7 @@ dist/
4409
4409
  : 'Heartbeat is unsigned - cannot verify authenticity or integrity',
4410
4410
  file: relativePath,
4411
4411
  fixable: false,
4412
- fix: 'Sign the heartbeat using: openclaw sign heartbeat.md --key ~/.openclaw/signing-key.pem',
4412
+ fix: 'Run `hackmyagent fix-all --with-aim` to automatically sign all heartbeat files with a cryptographic identity',
4413
4413
  });
4414
4414
  // HEARTBEAT-004: Dangerous Capabilities
4415
4415
  for (let i = 0; i < lines.length; i++) {
@@ -6018,7 +6018,7 @@ dist/
6018
6018
  message: `${idFile} declares identity without cryptographic key binding`,
6019
6019
  fixable: false,
6020
6020
  file: idFile,
6021
- fix: 'Bind agent identity to a cryptographic key pair. Add publicKey or keyId field to the agent card.',
6021
+ fix: 'Run `hackmyagent fix-all --with-aim` to bind identity to an Ed25519 key pair automatically',
6022
6022
  });
6023
6023
  }
6024
6024
  }
@@ -6059,7 +6059,7 @@ dist/
6059
6059
  message: 'Agent project has no identity declaration file (agent-card.json, agent.json, aim.json)',
6060
6060
  fixable: false,
6061
6061
  file: 'package.json',
6062
- fix: 'Create an agent-card.json with agentId, name, publicKey, and capabilities fields.',
6062
+ fix: 'Run `hackmyagent fix-all --with-aim` to create a cryptographic identity with Ed25519 key pair, audit logging, and trust scoring',
6063
6063
  });
6064
6064
  }
6065
6065
  }
@@ -6097,7 +6097,7 @@ dist/
6097
6097
  message: `${dnaFile} has no signature or content hash`,
6098
6098
  fixable: false,
6099
6099
  file: dnaFile,
6100
- fix: 'Sign the behavioral profile: add a contentHash (SHA-256) or signature field verified at startup.',
6100
+ fix: 'Run `hackmyagent fix-all --with-aim` to automatically sign behavioral profiles with a cryptographic identity',
6101
6101
  });
6102
6102
  }
6103
6103
  // DNA-003: No behavioral drift detection
@@ -6224,7 +6224,12 @@ dist/
6224
6224
  */
6225
6225
  async checkUnicodeSteganography(targetDir, _autoFix) {
6226
6226
  const findings = [];
6227
- const sourceFiles = await this.findSourceFiles(targetDir, targetDir);
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
+ const sourceFiles = await this.walkDirectory(targetDir, stegoExtensions);
6228
6233
  for (const filePath of sourceFiles) {
6229
6234
  const relativePath = path.relative(targetDir, filePath);
6230
6235
  let rawBuffer;
@@ -6238,18 +6243,69 @@ dist/
6238
6243
  if (rawBuffer.length > MAX_FILE_SIZE)
6239
6244
  continue;
6240
6245
  // UNICODE-STEGO-001: Invisible Codepoint Detection
6241
- // Scan for variation selectors U+FE00-FE0F (UTF-8: EF B8 80-8F)
6242
- // and tag characters U+E0100-E01EF (UTF-8: F3 A0 84 80 - F3 A0 87 AF)
6246
+ // 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
6243
6251
  let hasVariationSelectors = false;
6244
6252
  let variationSelectorLine = 1;
6245
6253
  let hasTagCharsIn001 = false;
6246
6254
  let tagCharLine001 = 1;
6255
+ let hasZeroWidth = false;
6256
+ let zeroWidthLine = 1;
6257
+ let hasBidiOverride = false;
6258
+ let bidiOverrideLine = 1;
6247
6259
  let currentLine = 1;
6248
6260
  for (let i = 0; i < rawBuffer.length; i++) {
6249
6261
  if (rawBuffer[i] === 0x0A) {
6250
6262
  currentLine++;
6251
6263
  continue;
6252
6264
  }
6265
+ // Zero-width characters: E2 80 8B (ZWSP), E2 80 8C (ZWNJ), E2 80 8D (ZWJ)
6266
+ if (rawBuffer[i] === 0xE2 &&
6267
+ i + 2 < rawBuffer.length &&
6268
+ rawBuffer[i + 1] === 0x80 &&
6269
+ rawBuffer[i + 2] >= 0x8B &&
6270
+ rawBuffer[i + 2] <= 0x8D) {
6271
+ if (!hasZeroWidth) {
6272
+ hasZeroWidth = true;
6273
+ zeroWidthLine = currentLine;
6274
+ }
6275
+ }
6276
+ // BOM / Zero-width no-break space U+FEFF: EF BB BF (only suspicious mid-file)
6277
+ if (rawBuffer[i] === 0xEF &&
6278
+ i + 2 < rawBuffer.length &&
6279
+ rawBuffer[i + 1] === 0xBB &&
6280
+ rawBuffer[i + 2] === 0xBF &&
6281
+ i > 0) {
6282
+ if (!hasZeroWidth) {
6283
+ hasZeroWidth = true;
6284
+ zeroWidthLine = currentLine;
6285
+ }
6286
+ }
6287
+ // Bidirectional override chars U+202A-U+202E: E2 80 AA-AE
6288
+ if (rawBuffer[i] === 0xE2 &&
6289
+ i + 2 < rawBuffer.length &&
6290
+ rawBuffer[i + 1] === 0x80 &&
6291
+ rawBuffer[i + 2] >= 0xAA &&
6292
+ rawBuffer[i + 2] <= 0xAE) {
6293
+ if (!hasBidiOverride) {
6294
+ hasBidiOverride = true;
6295
+ bidiOverrideLine = currentLine;
6296
+ }
6297
+ }
6298
+ // Bidirectional isolate chars U+2066-U+2069: E2 81 A6-A9
6299
+ if (rawBuffer[i] === 0xE2 &&
6300
+ i + 2 < rawBuffer.length &&
6301
+ rawBuffer[i + 1] === 0x81 &&
6302
+ rawBuffer[i + 2] >= 0xA6 &&
6303
+ rawBuffer[i + 2] <= 0xA9) {
6304
+ if (!hasBidiOverride) {
6305
+ hasBidiOverride = true;
6306
+ bidiOverrideLine = currentLine;
6307
+ }
6308
+ }
6253
6309
  // Variation selectors: EF B8 80-8F
6254
6310
  if (rawBuffer[i] === 0xEF &&
6255
6311
  i + 2 < rawBuffer.length &&
@@ -6273,24 +6329,39 @@ dist/
6273
6329
  }
6274
6330
  }
6275
6331
  }
6276
- if (hasVariationSelectors || hasTagCharsIn001) {
6332
+ if (hasZeroWidth || hasVariationSelectors || hasTagCharsIn001 || hasBidiOverride) {
6277
6333
  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)');
6278
6338
  if (hasVariationSelectors)
6279
6339
  detectedTypes.push('variation selectors (U+FE00-FE0F)');
6280
6340
  if (hasTagCharsIn001)
6281
6341
  detectedTypes.push('tag characters (U+E0100-E01EF)');
6342
+ const lineNumbers = [];
6343
+ if (hasZeroWidth)
6344
+ lineNumbers.push(zeroWidthLine);
6345
+ 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';
6282
6353
  findings.push({
6283
6354
  checkId: 'UNICODE-STEGO-001',
6284
6355
  name: 'Invisible Unicode Codepoints Detected',
6285
- description: 'Source file contains invisible Unicode codepoints that can hide malicious payloads (GlassWorm attack vector)',
6286
- category: 'unicode-stego',
6287
- severity: 'critical',
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,
6288
6359
  passed: false,
6289
6360
  message: `Found ${detectedTypes.join(' and ')} in ${relativePath}`,
6290
6361
  file: relativePath,
6291
- line: hasVariationSelectors ? variationSelectorLine : tagCharLine001,
6362
+ line: earliestLine,
6292
6363
  fixable: false,
6293
- fix: 'Inspect the file with a hex editor (e.g., xxd) to identify and remove invisible Unicode codepoints. Run: xxd ' + relativePath + ' | grep -E "fe0[0-9a-f]|f3a0"',
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"',
6294
6365
  });
6295
6366
  }
6296
6367
  // UNICODE-STEGO-002: GlassWorm Decoder Pattern
@@ -6319,7 +6390,7 @@ dist/
6319
6390
  checkId: 'UNICODE-STEGO-002',
6320
6391
  name: 'GlassWorm Decoder Pattern Detected',
6321
6392
  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',
6322
- category: 'unicode-stego',
6393
+ category: 'supply-chain',
6323
6394
  severity: 'critical',
6324
6395
  passed: false,
6325
6396
  message: `Found GlassWorm decoder pattern (.codePointAt + hex range literals) in ${relativePath}`,
@@ -6373,7 +6444,7 @@ dist/
6373
6444
  checkId: 'UNICODE-STEGO-003',
6374
6445
  name: 'Eval on String with Hidden Payload',
6375
6446
  description: 'eval() or Function() is called with a string that has very few visible characters but a large byte footprint - indicates invisible Unicode payload',
6376
- category: 'unicode-stego',
6447
+ category: 'supply-chain',
6377
6448
  severity: 'critical',
6378
6449
  passed: false,
6379
6450
  message: `Found eval/Function with ${visibleChars} visible chars but ${byteLength} bytes in ${relativePath}`,
@@ -6415,7 +6486,7 @@ dist/
6415
6486
  checkId: 'UNICODE-STEGO-004',
6416
6487
  name: 'Unicode Tag Character Block Detected',
6417
6488
  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',
6418
- category: 'unicode-stego',
6489
+ category: 'supply-chain',
6419
6490
  severity: 'high',
6420
6491
  passed: false,
6421
6492
  message: `Found Unicode tag block characters in ${relativePath}`,
@@ -6427,6 +6498,69 @@ dist/
6427
6498
  }
6428
6499
  }
6429
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];
6529
+ if (line.length > MAX_LINE_LENGTH)
6530
+ continue;
6531
+ const trimmed = line.trimStart();
6532
+ if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*'))
6533
+ continue;
6534
+ for (const ch of line) {
6535
+ const cp = ch.codePointAt(0);
6536
+ if (homoglyphCodes.has(cp)) {
6537
+ foundHomoglyphs.push({ line: lineIdx + 1, lookalike: HOMOGLYPHS[cp], cp });
6538
+ break;
6539
+ }
6540
+ }
6541
+ if (foundHomoglyphs.length >= 5)
6542
+ break;
6543
+ }
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(', ');
6549
+ findings.push({
6550
+ 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',
6554
+ severity: 'high',
6555
+ passed: false,
6556
+ message: `Found ${foundHomoglyphs.length} homoglyph substitution(s) in ${relativePath}: ${examples}`,
6557
+ file: relativePath,
6558
+ line: foundHomoglyphs[0].line,
6559
+ 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))})"',
6561
+ });
6562
+ }
6563
+ }
6430
6564
  return findings;
6431
6565
  }
6432
6566
  }