hackmyagent 0.11.3 → 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 +29 -2
- package/dist/cli.js.map +1 -1
- package/dist/hardening/scanner.d.ts.map +1 -1
- package/dist/hardening/scanner.js +142 -13
- package/dist/hardening/scanner.js.map +1 -1
- package/dist/hardening/taxonomy.d.ts.map +1 -1
- package/dist/hardening/taxonomy.js +67 -65
- 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,7 +6229,9 @@ dist/
|
|
|
6224
6229
|
*/
|
|
6225
6230
|
async checkUnicodeSteganography(targetDir, _autoFix) {
|
|
6226
6231
|
const findings = [];
|
|
6227
|
-
|
|
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'];
|
|
6234
|
+
const sourceFiles = await this.walkDirectory(targetDir, stegoExtensions);
|
|
6228
6235
|
for (const filePath of sourceFiles) {
|
|
6229
6236
|
const relativePath = path.relative(targetDir, filePath);
|
|
6230
6237
|
let rawBuffer;
|
|
@@ -6238,12 +6245,22 @@ dist/
|
|
|
6238
6245
|
if (rawBuffer.length > MAX_FILE_SIZE)
|
|
6239
6246
|
continue;
|
|
6240
6247
|
// UNICODE-STEGO-001: Invisible Codepoint Detection
|
|
6241
|
-
// Scan for
|
|
6242
|
-
//
|
|
6248
|
+
// Scan for:
|
|
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)
|
|
6243
6254
|
let hasVariationSelectors = false;
|
|
6244
6255
|
let variationSelectorLine = 1;
|
|
6245
6256
|
let hasTagCharsIn001 = false;
|
|
6246
6257
|
let tagCharLine001 = 1;
|
|
6258
|
+
let hasZeroWidth = false;
|
|
6259
|
+
let zeroWidthLine = 1;
|
|
6260
|
+
let hasMidFileBom = false;
|
|
6261
|
+
let midFileBomLine = 1;
|
|
6262
|
+
let hasBidiOverride = false;
|
|
6263
|
+
let bidiOverrideLine = 1;
|
|
6247
6264
|
let currentLine = 1;
|
|
6248
6265
|
for (let i = 0; i < rawBuffer.length; i++) {
|
|
6249
6266
|
if (rawBuffer[i] === 0x0A) {
|
|
@@ -6272,25 +6289,86 @@ dist/
|
|
|
6272
6289
|
tagCharLine001 = currentLine;
|
|
6273
6290
|
}
|
|
6274
6291
|
}
|
|
6292
|
+
// Zero-width chars: U+200B/200C/200D = E2 80 8B/8C/8D
|
|
6293
|
+
if (rawBuffer[i] === 0xE2 &&
|
|
6294
|
+
i + 2 < rawBuffer.length &&
|
|
6295
|
+
rawBuffer[i + 1] === 0x80 &&
|
|
6296
|
+
rawBuffer[i + 2] >= 0x8B &&
|
|
6297
|
+
rawBuffer[i + 2] <= 0x8D) {
|
|
6298
|
+
if (!hasZeroWidth) {
|
|
6299
|
+
hasZeroWidth = true;
|
|
6300
|
+
zeroWidthLine = currentLine;
|
|
6301
|
+
}
|
|
6302
|
+
}
|
|
6303
|
+
// Mid-file BOM: U+FEFF = EF BB BF (skip if at offset 0)
|
|
6304
|
+
if (i > 0 &&
|
|
6305
|
+
rawBuffer[i] === 0xEF &&
|
|
6306
|
+
i + 2 < rawBuffer.length &&
|
|
6307
|
+
rawBuffer[i + 1] === 0xBB &&
|
|
6308
|
+
rawBuffer[i + 2] === 0xBF) {
|
|
6309
|
+
if (!hasMidFileBom) {
|
|
6310
|
+
hasMidFileBom = true;
|
|
6311
|
+
midFileBomLine = currentLine;
|
|
6312
|
+
}
|
|
6313
|
+
}
|
|
6314
|
+
// Bidi overrides: U+202A-202E = E2 80 AA-AE
|
|
6315
|
+
if (rawBuffer[i] === 0xE2 &&
|
|
6316
|
+
i + 2 < rawBuffer.length &&
|
|
6317
|
+
rawBuffer[i + 1] === 0x80 &&
|
|
6318
|
+
rawBuffer[i + 2] >= 0xAA &&
|
|
6319
|
+
rawBuffer[i + 2] <= 0xAE) {
|
|
6320
|
+
if (!hasBidiOverride) {
|
|
6321
|
+
hasBidiOverride = true;
|
|
6322
|
+
bidiOverrideLine = currentLine;
|
|
6323
|
+
}
|
|
6324
|
+
}
|
|
6325
|
+
// Bidi isolates: U+2066-2069 = E2 81 A6-A9
|
|
6326
|
+
if (rawBuffer[i] === 0xE2 &&
|
|
6327
|
+
i + 2 < rawBuffer.length &&
|
|
6328
|
+
rawBuffer[i + 1] === 0x81 &&
|
|
6329
|
+
rawBuffer[i + 2] >= 0xA6 &&
|
|
6330
|
+
rawBuffer[i + 2] <= 0xA9) {
|
|
6331
|
+
if (!hasBidiOverride) {
|
|
6332
|
+
hasBidiOverride = true;
|
|
6333
|
+
bidiOverrideLine = currentLine;
|
|
6334
|
+
}
|
|
6335
|
+
}
|
|
6275
6336
|
}
|
|
6276
|
-
|
|
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) {
|
|
6277
6341
|
const detectedTypes = [];
|
|
6278
6342
|
if (hasVariationSelectors)
|
|
6279
6343
|
detectedTypes.push('variation selectors (U+FE00-FE0F)');
|
|
6280
6344
|
if (hasTagCharsIn001)
|
|
6281
6345
|
detectedTypes.push('tag characters (U+E0100-E01EF)');
|
|
6346
|
+
if (hasZeroWidth)
|
|
6347
|
+
detectedTypes.push('zero-width characters (U+200B-200D)');
|
|
6348
|
+
if (hasMidFileBom)
|
|
6349
|
+
detectedTypes.push('mid-file BOM (U+FEFF)');
|
|
6350
|
+
if (hasBidiOverride)
|
|
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
|
+
]);
|
|
6282
6360
|
findings.push({
|
|
6283
6361
|
checkId: 'UNICODE-STEGO-001',
|
|
6284
6362
|
name: 'Invisible Unicode Codepoints Detected',
|
|
6285
6363
|
description: 'Source file contains invisible Unicode codepoints that can hide malicious payloads (GlassWorm attack vector)',
|
|
6286
6364
|
category: 'unicode-stego',
|
|
6287
|
-
severity: 'critical',
|
|
6365
|
+
severity: hasCriticalInvisible ? 'critical' : 'high',
|
|
6288
6366
|
passed: false,
|
|
6289
6367
|
message: `Found ${detectedTypes.join(' and ')} in ${relativePath}`,
|
|
6290
6368
|
file: relativePath,
|
|
6291
|
-
line:
|
|
6369
|
+
line: firstLine,
|
|
6292
6370
|
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 -
|
|
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"',
|
|
6294
6372
|
});
|
|
6295
6373
|
}
|
|
6296
6374
|
// UNICODE-STEGO-002: GlassWorm Decoder Pattern
|
|
@@ -6380,7 +6458,7 @@ dist/
|
|
|
6380
6458
|
file: relativePath,
|
|
6381
6459
|
line: evalLine,
|
|
6382
6460
|
fixable: false,
|
|
6383
|
-
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)))"',
|
|
6384
6462
|
});
|
|
6385
6463
|
break; // One finding per file
|
|
6386
6464
|
}
|
|
@@ -6422,10 +6500,61 @@ dist/
|
|
|
6422
6500
|
file: relativePath,
|
|
6423
6501
|
line: tagBlockLine,
|
|
6424
6502
|
fixable: false,
|
|
6425
|
-
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"',
|
|
6426
6504
|
});
|
|
6427
6505
|
}
|
|
6428
6506
|
}
|
|
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];
|
|
6525
|
+
if (line.length > MAX_LINE_LENGTH)
|
|
6526
|
+
continue;
|
|
6527
|
+
// Skip comment lines
|
|
6528
|
+
const trimmed = line.trimStart();
|
|
6529
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*'))
|
|
6530
|
+
continue;
|
|
6531
|
+
for (const ch of line) {
|
|
6532
|
+
const cp = ch.codePointAt(0);
|
|
6533
|
+
if (homoglyphCodepoints.has(cp)) {
|
|
6534
|
+
homoglyphFound = true;
|
|
6535
|
+
homoglyphLine = lineIdx + 1;
|
|
6536
|
+
homoglyphChar = `U+${cp.toString(16).toUpperCase().padStart(4, '0')}`;
|
|
6537
|
+
break;
|
|
6538
|
+
}
|
|
6539
|
+
}
|
|
6540
|
+
if (homoglyphFound)
|
|
6541
|
+
break;
|
|
6542
|
+
}
|
|
6543
|
+
if (homoglyphFound) {
|
|
6544
|
+
findings.push({
|
|
6545
|
+
checkId: 'UNICODE-STEGO-005',
|
|
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',
|
|
6549
|
+
severity: 'high',
|
|
6550
|
+
passed: false,
|
|
6551
|
+
message: `Found homoglyph confusable character (${homoglyphChar}) in ${relativePath} at line ${homoglyphLine}`,
|
|
6552
|
+
file: relativePath,
|
|
6553
|
+
line: homoglyphLine,
|
|
6554
|
+
fixable: false,
|
|
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)})"',
|
|
6556
|
+
});
|
|
6557
|
+
}
|
|
6429
6558
|
}
|
|
6430
6559
|
return findings;
|
|
6431
6560
|
}
|