hackmyagent 0.16.0 → 0.16.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/dist/.integrity-manifest.json +1 -1
- package/dist/arp/intelligence/nanomind-l1.d.ts +30 -0
- package/dist/arp/intelligence/nanomind-l1.d.ts.map +1 -1
- package/dist/arp/intelligence/nanomind-l1.js +115 -0
- package/dist/arp/intelligence/nanomind-l1.js.map +1 -1
- package/dist/cli.js +220 -19
- package/dist/cli.js.map +1 -1
- package/dist/hardening/scanner.d.ts.map +1 -1
- package/dist/hardening/scanner.js +148 -7
- package/dist/hardening/scanner.js.map +1 -1
- package/dist/hardening/taxonomy.d.ts +2 -0
- package/dist/hardening/taxonomy.d.ts.map +1 -1
- package/dist/hardening/taxonomy.js +5 -0
- package/dist/hardening/taxonomy.js.map +1 -1
- package/dist/nanomind-core/analyzers/credential-analyzer.js +12 -3
- package/dist/nanomind-core/analyzers/credential-analyzer.js.map +1 -1
- package/dist/nanomind-core/analyzers/stego-analyzer.d.ts +30 -0
- package/dist/nanomind-core/analyzers/stego-analyzer.d.ts.map +1 -0
- package/dist/nanomind-core/analyzers/stego-analyzer.js +533 -0
- package/dist/nanomind-core/analyzers/stego-analyzer.js.map +1 -0
- package/dist/nanomind-core/daemon-lifecycle.d.ts +28 -0
- package/dist/nanomind-core/daemon-lifecycle.d.ts.map +1 -0
- package/dist/nanomind-core/daemon-lifecycle.js +142 -0
- package/dist/nanomind-core/daemon-lifecycle.js.map +1 -0
- package/dist/nanomind-core/inference/tme-classifier.d.ts +3 -2
- package/dist/nanomind-core/inference/tme-classifier.d.ts.map +1 -1
- package/dist/nanomind-core/inference/tme-classifier.js +26 -16
- package/dist/nanomind-core/inference/tme-classifier.js.map +1 -1
- package/dist/nanomind-core/orchestrate.d.ts.map +1 -1
- package/dist/nanomind-core/orchestrate.js +11 -1
- package/dist/nanomind-core/orchestrate.js.map +1 -1
- package/dist/nanomind-core/scanner-bridge.d.ts.map +1 -1
- package/dist/nanomind-core/scanner-bridge.js +6 -0
- package/dist/nanomind-core/scanner-bridge.js.map +1 -1
- package/dist/plugins/credvault.d.ts.map +1 -1
- package/dist/plugins/credvault.js +25 -0
- package/dist/plugins/credvault.js.map +1 -1
- package/dist/semantic/nanomind-enhancer.d.ts.map +1 -1
- package/dist/semantic/nanomind-enhancer.js +206 -0
- package/dist/semantic/nanomind-enhancer.js.map +1 -1
- package/dist/telemetry/nanomind-feedback.d.ts +43 -0
- package/dist/telemetry/nanomind-feedback.d.ts.map +1 -0
- package/dist/telemetry/nanomind-feedback.js +104 -0
- package/dist/telemetry/nanomind-feedback.js.map +1 -0
- package/dist/telemetry/nanomind-telemetry.d.ts +48 -0
- package/dist/telemetry/nanomind-telemetry.d.ts.map +1 -0
- package/dist/telemetry/nanomind-telemetry.js +123 -0
- package/dist/telemetry/nanomind-telemetry.js.map +1 -0
- 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,EAAE,eAAe,EAAY,WAAW,EAAE,MAAM,kBAAkB,CAAC;AA4G3F,0CAA0C;AAC1C,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;AAEtD,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;;;;;OAKG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB,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":"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"}
|
|
@@ -172,7 +172,9 @@ const SKILL_CREDENTIAL_ACCESS_PATTERNS = [
|
|
|
172
172
|
/wallet.*\.json/gi,
|
|
173
173
|
/seed.*phrase/gi,
|
|
174
174
|
/private.*key/gi,
|
|
175
|
-
|
|
175
|
+
// Match .env as a standalone file reference, not as part of process.env or documentation
|
|
176
|
+
// like ".env.example in sync" or "set in .env.local"
|
|
177
|
+
/(?:^|[\s"'`(])\.env(?:\.local|\.production|\.development)?(?:[\s"'`)]|$)/gi,
|
|
176
178
|
/credentials\.json/gi,
|
|
177
179
|
];
|
|
178
180
|
const SKILL_EXFILTRATION_PATTERNS = [
|
|
@@ -246,6 +248,101 @@ function shellEscape(s) {
|
|
|
246
248
|
// Wrap in single quotes and escape embedded single quotes: ' -> '\''
|
|
247
249
|
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
248
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Check if a variation selector at position i in rawBuffer is a legitimate
|
|
253
|
+
* emoji presentation selector (U+FE0F following an emoji base character).
|
|
254
|
+
*
|
|
255
|
+
* Emoji base characters that commonly precede FE0F:
|
|
256
|
+
* - Keycap digits/symbols: 0-9, #, * (encoded as single ASCII bytes)
|
|
257
|
+
* - BMP symbols: U+2600-27BF range (encoded as 3-byte UTF-8: E2 XX XX or E2 XX XX)
|
|
258
|
+
* - SMP emoji: U+1F300-1FAFF (encoded as 4-byte UTF-8: F0 9F XX XX)
|
|
259
|
+
*/
|
|
260
|
+
function isEmojiVariationSelector(buf, vsStart) {
|
|
261
|
+
// Walk backward to find the preceding character
|
|
262
|
+
// The variation selector is at vsStart (3 bytes: EF B8 8F)
|
|
263
|
+
// We need to check what character precedes it
|
|
264
|
+
if (vsStart === 0)
|
|
265
|
+
return false;
|
|
266
|
+
// Check for 4-byte SMP emoji before (F0 9F XX XX) — most common case
|
|
267
|
+
if (vsStart >= 4) {
|
|
268
|
+
const b0 = buf[vsStart - 4];
|
|
269
|
+
const b1 = buf[vsStart - 3];
|
|
270
|
+
if (b0 === 0xF0 && b1 === 0x9F)
|
|
271
|
+
return true; // U+1F000-1FFFF (emoji range)
|
|
272
|
+
}
|
|
273
|
+
// Check for 3-byte BMP symbol before (E2 XX XX) — symbols like warning, gear, etc.
|
|
274
|
+
if (vsStart >= 3) {
|
|
275
|
+
const b0 = buf[vsStart - 3];
|
|
276
|
+
const b1 = buf[vsStart - 2];
|
|
277
|
+
if (b0 === 0xE2) {
|
|
278
|
+
// U+2600-27BF: Misc Symbols, Dingbats (E2 98 80 through E2 9E BF)
|
|
279
|
+
if (b1 >= 0x98 && b1 <= 0x9E)
|
|
280
|
+
return true;
|
|
281
|
+
// U+2300-23FF: Misc Technical (E2 8C 80 through E2 8F BF) — includes hourglass, etc.
|
|
282
|
+
if (b1 >= 0x8C && b1 <= 0x8F)
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
// U+2700-27BF also encoded as E2 9C XX - E2 9E XX
|
|
286
|
+
if (b0 === 0xE2 && b1 >= 0x9C && b1 <= 0x9E)
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
// Check for 1-byte ASCII keycap base: #, *, 0-9
|
|
290
|
+
if (vsStart >= 1) {
|
|
291
|
+
const prev = buf[vsStart - 1];
|
|
292
|
+
if (prev === 0x23 || prev === 0x2A)
|
|
293
|
+
return true; // # or *
|
|
294
|
+
if (prev >= 0x30 && prev <= 0x39)
|
|
295
|
+
return true; // 0-9
|
|
296
|
+
}
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Check if a Cyrillic character at position ci in chars[] is in a Cyrillic
|
|
301
|
+
* text context (legitimate i18n) rather than mixed into a Latin word (attack).
|
|
302
|
+
*
|
|
303
|
+
* Looks at a window of nearby characters. If the neighborhood contains
|
|
304
|
+
* mostly Cyrillic or other non-Latin chars, it's i18n. If surrounded by
|
|
305
|
+
* Latin chars, it's a homoglyph attack.
|
|
306
|
+
*/
|
|
307
|
+
function isCyrillicInCyrillicContext(chars, ci) {
|
|
308
|
+
// Look at a window of 10 chars in each direction
|
|
309
|
+
const windowSize = 10;
|
|
310
|
+
const start = Math.max(0, ci - windowSize);
|
|
311
|
+
const end = Math.min(chars.length, ci + windowSize + 1);
|
|
312
|
+
let latinCount = 0;
|
|
313
|
+
let cyrillicCount = 0;
|
|
314
|
+
for (let j = start; j < end; j++) {
|
|
315
|
+
if (j === ci)
|
|
316
|
+
continue;
|
|
317
|
+
const cp = chars[j].codePointAt(0);
|
|
318
|
+
// Latin letter
|
|
319
|
+
if ((cp >= 0x41 && cp <= 0x5A) || (cp >= 0x61 && cp <= 0x7A)) {
|
|
320
|
+
latinCount++;
|
|
321
|
+
}
|
|
322
|
+
// Any Cyrillic (U+0400-052F)
|
|
323
|
+
if (cp >= 0x0400 && cp <= 0x052F) {
|
|
324
|
+
cyrillicCount++;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
// If there are at least 3 other Cyrillic chars nearby, this is i18n text
|
|
328
|
+
// (translations, i18n badges, etc. always have multiple Cyrillic chars together)
|
|
329
|
+
if (cyrillicCount >= 3)
|
|
330
|
+
return true;
|
|
331
|
+
// If the immediate neighbors are both Latin, this is a homoglyph attack
|
|
332
|
+
const prevLatin = ci > 0 && (() => {
|
|
333
|
+
const cp = chars[ci - 1].codePointAt(0);
|
|
334
|
+
return (cp >= 0x41 && cp <= 0x5A) || (cp >= 0x61 && cp <= 0x7A);
|
|
335
|
+
})();
|
|
336
|
+
const nextLatin = ci < chars.length - 1 && (() => {
|
|
337
|
+
const cp = chars[ci + 1].codePointAt(0);
|
|
338
|
+
return (cp >= 0x41 && cp <= 0x5A) || (cp >= 0x61 && cp <= 0x7A);
|
|
339
|
+
})();
|
|
340
|
+
if (prevLatin && nextLatin)
|
|
341
|
+
return false; // Sandwiched in Latin = attack
|
|
342
|
+
// Ambiguous case: not enough context. If there are ANY other Cyrillic
|
|
343
|
+
// chars nearby, give benefit of the doubt (i18n).
|
|
344
|
+
return cyrillicCount > 0;
|
|
345
|
+
}
|
|
249
346
|
class HardeningScanner {
|
|
250
347
|
constructor() {
|
|
251
348
|
this.cliName = 'hackmyagent';
|
|
@@ -4330,17 +4427,35 @@ dist/
|
|
|
4330
4427
|
await fs.writeFile(skillFile, content);
|
|
4331
4428
|
}
|
|
4332
4429
|
// SKILL-005: Credential File Access
|
|
4430
|
+
// Only flag as CRITICAL inside frontmatter (capabilities section).
|
|
4431
|
+
// Body text often describes credential handling in documentation,
|
|
4432
|
+
// which is informational, not an actual access pattern.
|
|
4433
|
+
let inSkill005Frontmatter = false;
|
|
4434
|
+
let skill005FrontmatterDelimiters = 0;
|
|
4333
4435
|
for (let i = 0; i < lines.length; i++) {
|
|
4436
|
+
const trimmed = lines[i].trim();
|
|
4437
|
+
if (trimmed === '---') {
|
|
4438
|
+
skill005FrontmatterDelimiters++;
|
|
4439
|
+
inSkill005Frontmatter = skill005FrontmatterDelimiters === 1;
|
|
4440
|
+
if (skill005FrontmatterDelimiters >= 2)
|
|
4441
|
+
inSkill005Frontmatter = false;
|
|
4442
|
+
continue;
|
|
4443
|
+
}
|
|
4334
4444
|
const line = lines[i];
|
|
4335
4445
|
for (const pattern of SKILL_CREDENTIAL_ACCESS_PATTERNS) {
|
|
4336
4446
|
pattern.lastIndex = 0;
|
|
4337
4447
|
if (pattern.test(line)) {
|
|
4448
|
+
// Frontmatter = actual capability declaration (CRITICAL)
|
|
4449
|
+
// Body = still suspicious but lower severity (MEDIUM)
|
|
4450
|
+
const severity = inSkill005Frontmatter ? 'critical' : 'medium';
|
|
4338
4451
|
findings.push({
|
|
4339
4452
|
checkId: 'SKILL-005',
|
|
4340
4453
|
name: 'Credential File Access',
|
|
4341
|
-
description:
|
|
4454
|
+
description: inSkill005Frontmatter
|
|
4455
|
+
? 'Skill declares access to credential or sensitive configuration files'
|
|
4456
|
+
: 'Skill body mentions credential file patterns',
|
|
4342
4457
|
category: 'skill',
|
|
4343
|
-
severity
|
|
4458
|
+
severity,
|
|
4344
4459
|
passed: false,
|
|
4345
4460
|
message: `Credential file access pattern detected: "${line.trim().substring(0, 80)}..."`,
|
|
4346
4461
|
file: relativePath,
|
|
@@ -6872,13 +6987,19 @@ dist/
|
|
|
6872
6987
|
currentLine++;
|
|
6873
6988
|
continue;
|
|
6874
6989
|
}
|
|
6875
|
-
// Variation selectors: EF B8 80-8F
|
|
6990
|
+
// Variation selectors: EF B8 80-8F (U+FE00-FE0F)
|
|
6991
|
+
// Skip U+FE0F (EF B8 8F) when preceded by an emoji base character,
|
|
6992
|
+
// as it's the standard emoji presentation selector (not steganography).
|
|
6876
6993
|
if (rawBuffer[i] === 0xEF &&
|
|
6877
6994
|
i + 2 < rawBuffer.length &&
|
|
6878
6995
|
rawBuffer[i + 1] === 0xB8 &&
|
|
6879
6996
|
rawBuffer[i + 2] >= 0x80 &&
|
|
6880
6997
|
rawBuffer[i + 2] <= 0x8F) {
|
|
6881
|
-
if (
|
|
6998
|
+
// Check if this is an emoji presentation selector (FE0F after emoji base)
|
|
6999
|
+
if (rawBuffer[i + 2] === 0x8F && isEmojiVariationSelector(rawBuffer, i)) {
|
|
7000
|
+
// Legitimate emoji — skip
|
|
7001
|
+
}
|
|
7002
|
+
else if (!hasVariationSelectors) {
|
|
6882
7003
|
hasVariationSelectors = true;
|
|
6883
7004
|
variationSelectorLine = currentLine;
|
|
6884
7005
|
}
|
|
@@ -7129,17 +7250,37 @@ dist/
|
|
|
7129
7250
|
let homoglyphChar = '';
|
|
7130
7251
|
const contentForHomoglyph = content || rawBuffer.toString('utf-8');
|
|
7131
7252
|
const homoglyphLines = contentForHomoglyph.split('\n');
|
|
7253
|
+
// Track markdown code fences: homoglyphs inside ```...``` blocks in .md files
|
|
7254
|
+
// are documentation examples, not executable code — skip them.
|
|
7255
|
+
const isMarkdown = relativePath.endsWith('.md') || relativePath.endsWith('.txt');
|
|
7256
|
+
let inCodeFence = false;
|
|
7132
7257
|
for (let lineIdx = 0; lineIdx < homoglyphLines.length; lineIdx++) {
|
|
7133
7258
|
const line = homoglyphLines[lineIdx];
|
|
7134
7259
|
if (line.length > MAX_LINE_LENGTH)
|
|
7135
7260
|
continue;
|
|
7261
|
+
// Track code fence boundaries in markdown files
|
|
7262
|
+
if (isMarkdown && line.trimStart().startsWith('```')) {
|
|
7263
|
+
inCodeFence = !inCodeFence;
|
|
7264
|
+
continue;
|
|
7265
|
+
}
|
|
7266
|
+
// Skip lines inside markdown code fences (documentation examples)
|
|
7267
|
+
if (isMarkdown && inCodeFence)
|
|
7268
|
+
continue;
|
|
7136
7269
|
// Skip comment lines
|
|
7137
7270
|
const trimmed = line.trimStart();
|
|
7138
7271
|
if (trimmed.startsWith('//') || trimmed.startsWith('#') || trimmed.startsWith('*'))
|
|
7139
7272
|
continue;
|
|
7140
|
-
|
|
7141
|
-
|
|
7273
|
+
const chars = [...line];
|
|
7274
|
+
for (let ci = 0; ci < chars.length; ci++) {
|
|
7275
|
+
const cp = chars[ci].codePointAt(0);
|
|
7142
7276
|
if (homoglyphCodepoints.has(cp)) {
|
|
7277
|
+
// Check if this Cyrillic char is in a Cyrillic text block (i18n)
|
|
7278
|
+
// vs mixed into a Latin word (homoglyph attack).
|
|
7279
|
+
// Look at neighboring characters: if surrounded by other Cyrillic
|
|
7280
|
+
// or non-Latin chars, it's legitimate i18n text.
|
|
7281
|
+
if (isCyrillicInCyrillicContext(chars, ci)) {
|
|
7282
|
+
continue; // Legitimate i18n — skip
|
|
7283
|
+
}
|
|
7143
7284
|
homoglyphFound = true;
|
|
7144
7285
|
homoglyphLine = lineIdx + 1;
|
|
7145
7286
|
homoglyphChar = `U+${cp.toString(16).toUpperCase().padStart(4, '0')}`;
|