@goplus/agentguard 1.1.27 → 1.1.28-beta.1

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.
Files changed (80) hide show
  1. package/dist/action/detectors/exec.d.ts.map +1 -1
  2. package/dist/action/detectors/exec.js +22 -4
  3. package/dist/action/detectors/exec.js.map +1 -1
  4. package/dist/action/detectors/network.d.ts.map +1 -1
  5. package/dist/action/detectors/network.js +50 -6
  6. package/dist/action/detectors/network.js.map +1 -1
  7. package/dist/action/index.d.ts +4 -0
  8. package/dist/action/index.d.ts.map +1 -1
  9. package/dist/action/index.js +44 -5
  10. package/dist/action/index.js.map +1 -1
  11. package/dist/adapters/claude-code.d.ts.map +1 -1
  12. package/dist/adapters/claude-code.js +7 -2
  13. package/dist/adapters/claude-code.js.map +1 -1
  14. package/dist/adapters/common.d.ts.map +1 -1
  15. package/dist/adapters/common.js +1 -0
  16. package/dist/adapters/common.js.map +1 -1
  17. package/dist/adapters/hermes.d.ts.map +1 -1
  18. package/dist/adapters/hermes.js +12 -2
  19. package/dist/adapters/hermes.js.map +1 -1
  20. package/dist/adapters/openclaw-plugin.d.ts.map +1 -1
  21. package/dist/adapters/openclaw-plugin.js +26 -3
  22. package/dist/adapters/openclaw-plugin.js.map +1 -1
  23. package/dist/adapters/openclaw.d.ts.map +1 -1
  24. package/dist/adapters/openclaw.js +6 -0
  25. package/dist/adapters/openclaw.js.map +1 -1
  26. package/dist/cli.js +41 -12
  27. package/dist/cli.js.map +1 -1
  28. package/dist/installers.js +28 -20
  29. package/dist/installers.js.map +1 -1
  30. package/dist/mcp-server.js +2 -2
  31. package/dist/mcp-server.js.map +1 -1
  32. package/dist/runtime/approvals.d.ts.map +1 -1
  33. package/dist/runtime/approvals.js +7 -1
  34. package/dist/runtime/approvals.js.map +1 -1
  35. package/dist/runtime/evaluator.d.ts +1 -0
  36. package/dist/runtime/evaluator.d.ts.map +1 -1
  37. package/dist/runtime/evaluator.js +511 -9
  38. package/dist/runtime/evaluator.js.map +1 -1
  39. package/dist/runtime/policy.d.ts.map +1 -1
  40. package/dist/runtime/policy.js +1 -0
  41. package/dist/runtime/policy.js.map +1 -1
  42. package/dist/runtime/protect.d.ts +1 -0
  43. package/dist/runtime/protect.d.ts.map +1 -1
  44. package/dist/runtime/protect.js +52 -8
  45. package/dist/runtime/protect.js.map +1 -1
  46. package/dist/runtime/types.d.ts +3 -1
  47. package/dist/runtime/types.d.ts.map +1 -1
  48. package/dist/tests/action.test.js +69 -8
  49. package/dist/tests/action.test.js.map +1 -1
  50. package/dist/tests/adapter.test.js +21 -7
  51. package/dist/tests/adapter.test.js.map +1 -1
  52. package/dist/tests/cli-checkup.test.js +1 -1
  53. package/dist/tests/cli-checkup.test.js.map +1 -1
  54. package/dist/tests/cli-connect.test.js +77 -3
  55. package/dist/tests/cli-connect.test.js.map +1 -1
  56. package/dist/tests/cli-init.test.js +12 -2
  57. package/dist/tests/cli-init.test.js.map +1 -1
  58. package/dist/tests/cli-policy.test.js +34 -4
  59. package/dist/tests/cli-policy.test.js.map +1 -1
  60. package/dist/tests/cli-subscribe.test.js +80 -3
  61. package/dist/tests/cli-subscribe.test.js.map +1 -1
  62. package/dist/tests/installer.test.js +35 -0
  63. package/dist/tests/installer.test.js.map +1 -1
  64. package/dist/tests/integration.test.js +22 -0
  65. package/dist/tests/integration.test.js.map +1 -1
  66. package/dist/tests/runtime-cloud.test.js +368 -0
  67. package/dist/tests/runtime-cloud.test.js.map +1 -1
  68. package/dist/tests/smoke.test.js +9 -4
  69. package/dist/tests/smoke.test.js.map +1 -1
  70. package/dist/types/action.d.ts +9 -3
  71. package/dist/types/action.d.ts.map +1 -1
  72. package/docs/SECURITY-POLICY.md +1 -1
  73. package/docs/claude-code.md +2 -1
  74. package/docs/hermes.md +6 -2
  75. package/package.json +1 -1
  76. package/skills/agentguard/README.md +3 -2
  77. package/skills/agentguard/SKILL.md +2 -1
  78. package/skills/agentguard/hermes-hooks.yaml +4 -1
  79. package/skills/agentguard/scripts/action-cli.js +6 -0
  80. package/skills/agentguard/scripts/hermes-hook.js +13 -1
@@ -1,10 +1,41 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.__resetNetworkBehaviorForTests = __resetNetworkBehaviorForTests;
3
4
  exports.evaluateLocalAction = evaluateLocalAction;
4
5
  const index_js_1 = require("../action/index.js");
6
+ const node_crypto_1 = require("node:crypto");
7
+ const node_fs_1 = require("node:fs");
5
8
  const node_os_1 = require("node:os");
9
+ const node_path_1 = require("node:path");
6
10
  const skill_js_1 = require("../types/skill.js");
11
+ const patterns_js_1 = require("../utils/patterns.js");
12
+ const config_js_1 = require("../config.js");
7
13
  const redaction_js_1 = require("./redaction.js");
14
+ const ONE_MINUTE_MS = 60_000;
15
+ const TEN_MINUTES_MS = 10 * ONE_MINUTE_MS;
16
+ const HIGH_FREQUENCY_THRESHOLD = 100;
17
+ const ODD_HOUR_FREQUENCY_THRESHOLD = 20;
18
+ const TOKEN_DOMAIN_THRESHOLD = 10;
19
+ const REPLAY_THRESHOLD = 5;
20
+ const DOS_STATUS_THRESHOLD = 5;
21
+ const SINGLE_RESPONSE_BYTES_THRESHOLD = 10 * 1024 * 1024;
22
+ const WINDOW_RESPONSE_BYTES_THRESHOLD = 100 * 1024 * 1024;
23
+ const MAX_PERSISTED_BEHAVIOR_EVENTS = 1_000;
24
+ const networkBehaviorEvents = [];
25
+ let networkBehaviorStateLoaded = false;
26
+ function __resetNetworkBehaviorForTests() {
27
+ networkBehaviorEvents.length = 0;
28
+ networkBehaviorStateLoaded = true;
29
+ const statePath = process.env.AGENTGUARD_BEHAVIOR_STATE_PATH;
30
+ if (statePath) {
31
+ try {
32
+ (0, node_fs_1.rmSync)(statePath, { force: true });
33
+ }
34
+ catch {
35
+ // Test cleanup only.
36
+ }
37
+ }
38
+ }
8
39
  function reason(code, severity, title, description, evidence) {
9
40
  return {
10
41
  code,
@@ -64,11 +95,26 @@ function customPolicyReasons(policy, action) {
64
95
  }
65
96
  }
66
97
  for (const domain of policy.network.blockedDomains) {
67
- if (domain && lower.includes(domain.toLowerCase())) {
98
+ if (domain && matchesNetworkReference(input, domain)) {
68
99
  reasons.push(reason('CUSTOM_BLOCKED_DOMAIN', 'high', 'Custom blocked domain', 'The action references a domain blocked by runtime policy.', domain));
69
100
  }
70
101
  }
71
102
  }
103
+ if (action.actionType === 'network' || action.actionType === 'browser') {
104
+ for (const domain of policy.network.blockedDomains) {
105
+ if (matchesNetworkTarget(input, domain)) {
106
+ reasons.push(reason('CUSTOM_BLOCKED_DOMAIN', 'high', 'Custom blocked domain', 'The action references a domain blocked by runtime policy.', domain));
107
+ }
108
+ }
109
+ if (policy.network.defaultOutbound !== 'allow') {
110
+ reasons.push(reason('NETWORK_OUTBOUND', 'medium', 'Network outbound policy', 'The action makes an outbound network request covered by runtime policy.', input));
111
+ }
112
+ }
113
+ const networkTargets = networkTargetsFromAction(action);
114
+ if (networkTargets.length > 0) {
115
+ reasons.push(...networkBehaviorReasons(action, networkTargets));
116
+ reasons.push(...networkResponseReasons(action, networkTargets[0]));
117
+ }
72
118
  if (action.actionType === 'file_read' || action.actionType === 'file_write') {
73
119
  for (const pathPattern of policy.protectedPaths) {
74
120
  if (matchesPath(input, pathPattern)) {
@@ -129,11 +175,71 @@ function mapRuntimeAction(action) {
129
175
  if (action.actionType === 'file_write') {
130
176
  return { type: 'write_file', data: { path: action.input } };
131
177
  }
178
+ if (action.actionType === 'web_search') {
179
+ return { type: 'web_search', data: { query: action.input } };
180
+ }
132
181
  if (action.actionType === 'network' || action.actionType === 'browser') {
133
- return { type: 'network_request', data: { method: 'GET', url: action.input } };
182
+ return {
183
+ type: 'network_request',
184
+ data: {
185
+ method: methodFromMetadata(action.metadata?.method),
186
+ url: action.input,
187
+ body_preview: stringFromMetadata(action.metadata?.bodyPreview),
188
+ },
189
+ };
134
190
  }
135
191
  return null;
136
192
  }
193
+ function methodFromMetadata(value) {
194
+ const method = typeof value === 'string' ? value.toUpperCase() : '';
195
+ if (method === 'GET' ||
196
+ method === 'HEAD' ||
197
+ method === 'OPTIONS' ||
198
+ method === 'POST' ||
199
+ method === 'PUT' ||
200
+ method === 'DELETE' ||
201
+ method === 'PATCH') {
202
+ return method;
203
+ }
204
+ return 'GET';
205
+ }
206
+ function stringFromMetadata(value) {
207
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
208
+ }
209
+ function firstStringFromMetadata(...values) {
210
+ for (const value of values) {
211
+ if (typeof value === 'string' && value.length > 0)
212
+ return value;
213
+ }
214
+ return undefined;
215
+ }
216
+ function numberFromMetadata(...values) {
217
+ for (const value of values) {
218
+ if (typeof value === 'number' && Number.isFinite(value))
219
+ return value;
220
+ if (typeof value === 'string' && value.trim()) {
221
+ const parsed = Number(value);
222
+ if (Number.isFinite(parsed))
223
+ return parsed;
224
+ }
225
+ }
226
+ return undefined;
227
+ }
228
+ function timeFromMetadata(value) {
229
+ if (typeof value === 'number' && Number.isFinite(value))
230
+ return value;
231
+ if (typeof value === 'string') {
232
+ const parsed = Date.parse(value);
233
+ if (Number.isFinite(parsed))
234
+ return parsed;
235
+ }
236
+ return undefined;
237
+ }
238
+ function recordFromMetadata(value) {
239
+ if (!value || typeof value !== 'object' || Array.isArray(value))
240
+ return undefined;
241
+ return value;
242
+ }
137
243
  function normalizeOssReason(tag, evidence, action) {
138
244
  const evidenceText = evidence?.match || evidence?.description || action.input;
139
245
  if (tag === 'DANGEROUS_COMMAND') {
@@ -153,33 +259,193 @@ function normalizeOssReason(tag, evidence, action) {
153
259
  }
154
260
  return reason(tag, 'medium', tag.replace(/_/g, ' ').toLowerCase(), 'The local OSS runtime detected a risky action.', evidenceText);
155
261
  }
262
+ const BEHAVIOR_ANOMALY_CODES = new Set([
263
+ 'NETWORK_RATE_LIMIT',
264
+ 'NETWORK_TOKEN_DOMAIN_SWEEP',
265
+ 'NETWORK_ODD_HOUR_ACTIVITY',
266
+ 'NETWORK_REPLAY',
267
+ 'NETWORK_LARGE_RESPONSE',
268
+ 'NETWORK_DOS_RESPONSE',
269
+ ]);
270
+ const RESPONSE_ANOMALY_CODES = new Set([
271
+ 'RESPONSE_XSS_ECHO',
272
+ 'RESPONSE_ERROR_DISCLOSURE',
273
+ 'RESPONSE_MALICIOUS_SCRIPT',
274
+ 'RESPONSE_PATH_TRAVERSAL',
275
+ 'RESPONSE_CONTENT_TYPE_MISMATCH',
276
+ 'RESPONSE_CREDENTIAL_ECHO',
277
+ ]);
278
+ function networkTargetsFromAction(action) {
279
+ if (action.actionType === 'network' || action.actionType === 'browser') {
280
+ const target = parseNetworkTarget(action.input);
281
+ return target ? [target] : [];
282
+ }
283
+ if (action.actionType !== 'shell')
284
+ return [];
285
+ const targets = new Map();
286
+ for (const reference of extractNetworkReferences(action.input)) {
287
+ const target = parseNetworkTarget(reference);
288
+ if (target)
289
+ targets.set(`${target.hostname}${target.pathname}`, target);
290
+ }
291
+ return [...targets.values()];
292
+ }
293
+ function networkBehaviorReasons(action, targets) {
294
+ const timestamp = timeFromMetadata(action.metadata?.timestamp) ?? Date.now();
295
+ loadNetworkBehaviorState(timestamp);
296
+ pruneNetworkBehaviorEvents(timestamp);
297
+ const method = methodFromMetadata(action.metadata?.method);
298
+ const headers = recordFromMetadata(action.metadata?.headers) ?? recordFromMetadata(action.metadata?.requestHeaders);
299
+ const bodyPreview = stringFromMetadata(action.metadata?.bodyPreview);
300
+ const responseBytes = numberFromMetadata(action.metadata?.responseBodyBytes, action.metadata?.responseBytes, action.metadata?.contentLength);
301
+ const responseStatus = numberFromMetadata(action.metadata?.responseStatusCode, action.metadata?.statusCode, action.metadata?.status);
302
+ const tokenHashes = extractCredentialValues(action.input, headers, bodyPreview).map(hashValue);
303
+ const fingerprint = requestFingerprint(action.input, targets[0], method, headers, bodyPreview);
304
+ for (const target of targets) {
305
+ networkBehaviorEvents.push({
306
+ timestamp,
307
+ sessionId: action.sessionId,
308
+ hostname: target.hostname,
309
+ method,
310
+ fingerprint,
311
+ tokenHashes,
312
+ responseBytes,
313
+ responseStatus,
314
+ });
315
+ }
316
+ const reasons = [];
317
+ const sessionRecent = networkBehaviorEvents.filter((event) => event.sessionId === action.sessionId &&
318
+ timestamp - event.timestamp <= ONE_MINUTE_MS);
319
+ if (sessionRecent.length > HIGH_FREQUENCY_THRESHOLD) {
320
+ reasons.push(reason('NETWORK_RATE_LIMIT', 'high', 'High-frequency network activity', 'The agent made more than 100 outbound requests in one minute.', `${sessionRecent.length} requests in 60s`));
321
+ }
322
+ if (isOddHour(timestamp) && sessionRecent.length > ODD_HOUR_FREQUENCY_THRESHOLD) {
323
+ reasons.push(reason('NETWORK_ODD_HOUR_ACTIVITY', 'high', 'Odd-hour network burst', 'The agent made a high-frequency network burst during the 02:00-06:00 local risk window.', `${sessionRecent.length} requests in 60s`));
324
+ }
325
+ if (fingerprint) {
326
+ const repeats = sessionRecent.filter((event) => event.fingerprint === fingerprint).length;
327
+ if (repeats >= REPLAY_THRESHOLD) {
328
+ reasons.push(reason('NETWORK_REPLAY', 'high', 'Repeated identical request', 'The same request body and headers appeared repeatedly in a short window.', `${repeats} repeats in 60s`));
329
+ }
330
+ }
331
+ for (const tokenHash of tokenHashes) {
332
+ const domains = new Set(networkBehaviorEvents
333
+ .filter((event) => event.sessionId === action.sessionId &&
334
+ timestamp - event.timestamp <= TEN_MINUTES_MS &&
335
+ event.tokenHashes.includes(tokenHash))
336
+ .map((event) => event.hostname));
337
+ if (domains.size > TOKEN_DOMAIN_THRESHOLD) {
338
+ reasons.push(reason('NETWORK_TOKEN_DOMAIN_SWEEP', 'high', 'Credential used across many domains', 'The same credential-like value was used against more than 10 distinct domains.', `${domains.size} domains in 10m`));
339
+ break;
340
+ }
341
+ }
342
+ const responseBytesTotal = sessionRecent.reduce((total, event) => total + (event.responseBytes ?? 0), 0);
343
+ if ((responseBytes !== undefined && responseBytes > SINGLE_RESPONSE_BYTES_THRESHOLD) ||
344
+ responseBytesTotal > WINDOW_RESPONSE_BYTES_THRESHOLD) {
345
+ reasons.push(reason('NETWORK_LARGE_RESPONSE', 'high', 'Large network response volume', 'The network response volume crossed the runtime anomaly threshold.', responseBytes !== undefined ? `${responseBytes} bytes` : `${responseBytesTotal} bytes in 60s`));
346
+ }
347
+ if (responseStatus === 429 || responseStatus === 503) {
348
+ const statusRepeats = sessionRecent.filter((event) => event.responseStatus === responseStatus).length;
349
+ if (statusRepeats >= DOS_STATUS_THRESHOLD) {
350
+ reasons.push(reason('NETWORK_DOS_RESPONSE', 'high', 'Repeated throttling or service unavailable responses', 'The agent received repeated 429/503 responses in a short window.', `${statusRepeats} responses with status ${responseStatus} in 60s`));
351
+ }
352
+ }
353
+ saveNetworkBehaviorState();
354
+ return dedupeReasons(reasons);
355
+ }
356
+ function networkResponseReasons(action, target) {
357
+ const responseBody = firstStringFromMetadata(action.metadata?.responseBodyPreview, action.metadata?.responsePreview, action.metadata?.responseBody);
358
+ const responseHeaders = recordFromMetadata(action.metadata?.responseHeaders);
359
+ const contentType = (firstStringFromMetadata(action.metadata?.responseContentType, contentTypeFromHeaders(responseHeaders)) ?? '').toLowerCase();
360
+ if (!responseBody)
361
+ return [];
362
+ const reasons = [];
363
+ if (hasSuspiciousXssResponse(responseBody)) {
364
+ reasons.push(reason('RESPONSE_XSS_ECHO', 'high', 'Executable markup in response', 'The network response contains script-like markup or JavaScript URL content.', target.hostname));
365
+ }
366
+ if (/sql syntax|mysql_fetch|postgresql|ora-\d+|traceback|stack trace|exception|at .+:\d+:\d+/i.test(responseBody)) {
367
+ reasons.push(reason('RESPONSE_ERROR_DISCLOSURE', 'high', 'Server error disclosure in response', 'The network response contains SQL, command, or stack-trace style error output.', target.hostname));
368
+ }
369
+ if (/eval\s*\(\s*(?:atob|unescape)|fromcharcode|document\.write\s*\(/i.test(responseBody)) {
370
+ reasons.push(reason('RESPONSE_MALICIOUS_SCRIPT', 'critical', 'Obfuscated script in response', 'The network response contains script patterns commonly used for payload staging.', target.hostname));
371
+ }
372
+ if (/root:.*:0:0:|\/etc\/passwd|c:\\windows\\|windows\\system32/i.test(responseBody)) {
373
+ reasons.push(reason('RESPONSE_PATH_TRAVERSAL', 'critical', 'Path traversal content in response', 'The network response contains filesystem markers associated with traversal or local file disclosure.', target.hostname));
374
+ }
375
+ if (contentType && isBinaryOrMediaContentType(contentType) && looksLikeHtmlOrScript(responseBody)) {
376
+ reasons.push(reason('RESPONSE_CONTENT_TYPE_MISMATCH', 'critical', 'Response content type mismatch', 'The response body looks executable or HTML-like while the Content-Type claims binary/media content.', target.hostname));
377
+ }
378
+ const requestSecrets = extractCredentialValues(action.input, recordFromMetadata(action.metadata?.headers) ?? recordFromMetadata(action.metadata?.requestHeaders), stringFromMetadata(action.metadata?.bodyPreview));
379
+ if (requestSecrets.some((secret) => secret.length >= 8 && responseBody.includes(secret))) {
380
+ reasons.push(reason('RESPONSE_CREDENTIAL_ECHO', 'critical', 'Credential echoed in response', 'The response appears to echo a credential-like value from the request.', target.hostname));
381
+ }
382
+ return dedupeReasons(reasons);
383
+ }
156
384
  function decisionFor(policy, reasons, riskLevel, ossDecision) {
385
+ const policyDecisions = [];
157
386
  for (const item of reasons) {
158
- const decision = policyDecisionFor(item.code, policy);
387
+ const decision = policyDecisionFor(item, policy);
159
388
  if (decision)
160
- return decision;
389
+ policyDecisions.push(decision);
161
390
  }
162
- if (ossDecision === 'deny')
163
- return riskLevel === 'critical' ? 'block' : 'require_approval';
164
- if (ossDecision === 'confirm')
165
- return 'require_approval';
391
+ const strongestPolicyDecision = strongestDecision(policyDecisions);
392
+ const scannerDecision = scannerPolicyDecision(ossDecision, riskLevel);
393
+ if (strongestPolicyDecision) {
394
+ if (scannerDecision &&
395
+ DECISION_ORDER[strongestPolicyDecision] <= DECISION_ORDER.warn &&
396
+ DECISION_ORDER[scannerDecision] > DECISION_ORDER[strongestPolicyDecision]) {
397
+ return scannerDecision;
398
+ }
399
+ return strongestPolicyDecision;
400
+ }
401
+ if (scannerDecision)
402
+ return scannerDecision;
166
403
  if (reasons.length > 0)
167
404
  return 'warn';
168
405
  return 'allow';
169
406
  }
170
- function policyDecisionFor(code, policy) {
407
+ function policyDecisionFor(reasonItem, policy) {
408
+ const code = reasonItem.code;
171
409
  if (code === 'CUSTOM_BLOCKED_COMMAND' || code === 'DESTRUCTIVE_COMMAND')
172
410
  return policy.decisions.destructiveCommand;
173
411
  if (code === 'REMOTE_CODE_EXECUTION')
174
412
  return policy.decisions.remoteCodeExecution;
175
413
  if (code === 'CUSTOM_BLOCKED_DOMAIN' || code === 'DATA_EXFILTRATION')
176
414
  return policy.decisions.dataExfiltration;
415
+ if (code === 'NETWORK_OUTBOUND')
416
+ return policy.network.defaultOutbound;
417
+ if (BEHAVIOR_ANOMALY_CODES.has(code))
418
+ return policy.network.behaviorAnomaly ?? 'require_approval';
419
+ if (RESPONSE_ANOMALY_CODES.has(code)) {
420
+ return policy.network.responseAnomaly ?? (reasonItem.severity === 'critical' ? 'block' : 'require_approval');
421
+ }
177
422
  if (code === 'SECRET_ACCESS')
178
423
  return policy.decisions.secretAccess;
179
424
  if (code === 'DEPLOYMENT_ACTION')
180
425
  return policy.decisions.deployAction;
181
426
  return null;
182
427
  }
428
+ const DECISION_ORDER = {
429
+ allow: 0,
430
+ warn: 1,
431
+ require_approval: 2,
432
+ block: 3,
433
+ };
434
+ function strongestDecision(decisions) {
435
+ let strongest = null;
436
+ for (const decision of decisions) {
437
+ if (!strongest || DECISION_ORDER[decision] > DECISION_ORDER[strongest])
438
+ strongest = decision;
439
+ }
440
+ return strongest;
441
+ }
442
+ function scannerPolicyDecision(ossDecision, riskLevel) {
443
+ if (ossDecision === 'deny')
444
+ return riskLevel === 'critical' ? 'block' : 'require_approval';
445
+ if (ossDecision === 'confirm')
446
+ return 'require_approval';
447
+ return null;
448
+ }
183
449
  function riskScoreFor(reasons, ossRiskLevel) {
184
450
  if (reasons.some((item) => item.severity === 'critical') || ossRiskLevel === 'critical')
185
451
  return 95;
@@ -233,6 +499,242 @@ function matchesAllowedCommand(input, pattern) {
233
499
  return normalizedInput === normalizedPattern ||
234
500
  (!inputHasControl && normalizedInput.startsWith(`${normalizedPattern} `));
235
501
  }
502
+ function matchesNetworkTarget(input, pattern) {
503
+ const target = parseNetworkTarget(input);
504
+ const matcher = parseNetworkPattern(pattern);
505
+ if (!target || !matcher)
506
+ return false;
507
+ if (!(0, patterns_js_1.domainMatchesPattern)(target.hostname, matcher.hostname))
508
+ return false;
509
+ if (!matcher.pathname)
510
+ return true;
511
+ return target.pathname === matcher.pathname ||
512
+ target.pathname.startsWith(`${matcher.pathname.replace(/\/+$/, '')}/`);
513
+ }
514
+ function matchesNetworkReference(input, pattern) {
515
+ if (matchesNetworkTarget(input, pattern))
516
+ return true;
517
+ return extractNetworkReferences(input).some((reference) => matchesNetworkTarget(reference, pattern));
518
+ }
519
+ function parseNetworkTarget(value) {
520
+ const trimmed = trimNetworkToken(value);
521
+ if (!trimmed)
522
+ return null;
523
+ const urlLike = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
524
+ ? trimmed
525
+ : `https://${trimmed}`;
526
+ try {
527
+ const parsed = new URL(urlLike);
528
+ return {
529
+ hostname: parsed.hostname.toLowerCase(),
530
+ pathname: normalizeNetworkPath(parsed.pathname),
531
+ };
532
+ }
533
+ catch {
534
+ return null;
535
+ }
536
+ }
537
+ function parseNetworkPattern(value) {
538
+ const trimmed = trimNetworkToken(value).toLowerCase();
539
+ if (!trimmed)
540
+ return null;
541
+ const urlLike = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
542
+ ? trimmed
543
+ : `https://${trimmed}`;
544
+ try {
545
+ const parsed = new URL(urlLike);
546
+ return {
547
+ hostname: parsed.hostname.toLowerCase(),
548
+ pathname: parsed.pathname && parsed.pathname !== '/' ? normalizeNetworkPath(parsed.pathname) : null,
549
+ };
550
+ }
551
+ catch {
552
+ return null;
553
+ }
554
+ }
555
+ function extractNetworkReferences(input) {
556
+ const references = new Set();
557
+ for (const match of input.matchAll(/https?:\/\/[^\s'"<>`]+/gi)) {
558
+ references.add(trimNetworkToken(match[0]));
559
+ }
560
+ for (const match of input.matchAll(/\b[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?\.[a-z]{2,}(?:\/[^\s'"<>`]*)?/gi)) {
561
+ references.add(trimNetworkToken(match[0]));
562
+ }
563
+ return [...references].filter(Boolean);
564
+ }
565
+ function trimNetworkToken(value) {
566
+ return value.trim().replace(/[),.;\]]+$/g, '');
567
+ }
568
+ function normalizeNetworkPath(pathname) {
569
+ if (!pathname || pathname === '/')
570
+ return '/';
571
+ return pathname.replace(/\/+$/g, '') || '/';
572
+ }
573
+ function behaviorStatePath() {
574
+ return process.env.AGENTGUARD_BEHAVIOR_STATE_PATH ||
575
+ (0, node_path_1.join)((0, config_js_1.getAgentGuardPaths)().home, 'network-behavior.json');
576
+ }
577
+ function loadNetworkBehaviorState(now) {
578
+ if (networkBehaviorStateLoaded)
579
+ return;
580
+ networkBehaviorStateLoaded = true;
581
+ const statePath = behaviorStatePath();
582
+ try {
583
+ if (!(0, node_fs_1.existsSync)(statePath))
584
+ return;
585
+ const parsed = JSON.parse((0, node_fs_1.readFileSync)(statePath, 'utf8'));
586
+ if (!Array.isArray(parsed))
587
+ return;
588
+ const events = [];
589
+ for (const item of parsed) {
590
+ const event = parseNetworkBehaviorEvent(item);
591
+ if (event && now - event.timestamp <= TEN_MINUTES_MS)
592
+ events.push(event);
593
+ }
594
+ networkBehaviorEvents.push(...events);
595
+ }
596
+ catch {
597
+ networkBehaviorEvents.length = 0;
598
+ }
599
+ }
600
+ function saveNetworkBehaviorState() {
601
+ const statePath = behaviorStatePath();
602
+ try {
603
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(statePath), { recursive: true, mode: 0o700 });
604
+ const events = networkBehaviorEvents.slice(-MAX_PERSISTED_BEHAVIOR_EVENTS);
605
+ (0, node_fs_1.writeFileSync)(statePath, `${JSON.stringify(events)}\n`, { mode: 0o600 });
606
+ (0, node_fs_1.chmodSync)(statePath, 0o600);
607
+ }
608
+ catch {
609
+ // Behavior state is best-effort; runtime protection still works without it.
610
+ }
611
+ }
612
+ function parseNetworkBehaviorEvent(value) {
613
+ if (!value || typeof value !== 'object' || Array.isArray(value))
614
+ return null;
615
+ const record = value;
616
+ if (typeof record.timestamp !== 'number' ||
617
+ typeof record.sessionId !== 'string' ||
618
+ typeof record.hostname !== 'string' ||
619
+ typeof record.method !== 'string') {
620
+ return null;
621
+ }
622
+ return {
623
+ timestamp: record.timestamp,
624
+ sessionId: record.sessionId,
625
+ hostname: record.hostname,
626
+ method: record.method,
627
+ fingerprint: typeof record.fingerprint === 'string' ? record.fingerprint : undefined,
628
+ tokenHashes: Array.isArray(record.tokenHashes)
629
+ ? record.tokenHashes.filter((item) => typeof item === 'string')
630
+ : [],
631
+ responseBytes: typeof record.responseBytes === 'number' ? record.responseBytes : undefined,
632
+ responseStatus: typeof record.responseStatus === 'number' ? record.responseStatus : undefined,
633
+ };
634
+ }
635
+ function pruneNetworkBehaviorEvents(now) {
636
+ const cutoff = now - TEN_MINUTES_MS;
637
+ while (networkBehaviorEvents.length > 0 && networkBehaviorEvents[0].timestamp < cutoff) {
638
+ networkBehaviorEvents.shift();
639
+ }
640
+ }
641
+ function isOddHour(timestamp) {
642
+ const hour = new Date(timestamp).getHours();
643
+ return hour >= 2 && hour < 6;
644
+ }
645
+ function requestFingerprint(input, target, method, headers, bodyPreview) {
646
+ if (!headers && !bodyPreview)
647
+ return undefined;
648
+ return hashValue(JSON.stringify({
649
+ method,
650
+ host: target.hostname,
651
+ path: target.pathname,
652
+ input,
653
+ headers: stableRecordString(headers),
654
+ bodyPreview,
655
+ }));
656
+ }
657
+ function extractCredentialValues(input, headers, bodyPreview) {
658
+ const values = new Set();
659
+ collectUrlCredentialValues(input, values);
660
+ if (headers) {
661
+ for (const [key, value] of Object.entries(headers)) {
662
+ if (!/(authorization|api[-_]?key|access[-_]?token|token|secret)/i.test(key))
663
+ continue;
664
+ const text = Array.isArray(value) ? value.join(',') : String(value ?? '');
665
+ for (const item of text.matchAll(/[A-Za-z0-9._~+/=-]{8,}/g)) {
666
+ values.add(item[0]);
667
+ }
668
+ }
669
+ }
670
+ if (bodyPreview) {
671
+ for (const match of bodyPreview.matchAll(/(?:api[-_]?key|access[-_]?token|token|authorization|secret)["':=\s]+([A-Za-z0-9._~+/=-]{8,})/gi)) {
672
+ values.add(match[1]);
673
+ }
674
+ }
675
+ return [...values];
676
+ }
677
+ function collectUrlCredentialValues(input, values) {
678
+ for (const reference of extractNetworkReferences(input).length > 0 ? extractNetworkReferences(input) : [input]) {
679
+ const token = trimNetworkToken(reference);
680
+ if (!token)
681
+ continue;
682
+ const urlLike = /^[a-z][a-z0-9+.-]*:\/\//i.test(token) ? token : `https://${token}`;
683
+ try {
684
+ const parsed = new URL(urlLike);
685
+ for (const [key, value] of parsed.searchParams.entries()) {
686
+ if (/(api[-_]?key|access[-_]?token|token|authorization|secret|auth|key)/i.test(key) && value.length >= 8) {
687
+ values.add(value);
688
+ }
689
+ }
690
+ }
691
+ catch {
692
+ // Ignore malformed references.
693
+ }
694
+ }
695
+ }
696
+ function stableRecordString(record) {
697
+ if (!record)
698
+ return undefined;
699
+ return Object.keys(record)
700
+ .sort((a, b) => a.localeCompare(b))
701
+ .map((key) => `${key.toLowerCase()}:${String(record[key])}`)
702
+ .join('\n');
703
+ }
704
+ function hashValue(value) {
705
+ return (0, node_crypto_1.createHash)('sha256').update(value).digest('hex');
706
+ }
707
+ function contentTypeFromHeaders(headers) {
708
+ if (!headers)
709
+ return undefined;
710
+ for (const [key, value] of Object.entries(headers)) {
711
+ if (key.toLowerCase() === 'content-type' && typeof value === 'string')
712
+ return value;
713
+ }
714
+ return undefined;
715
+ }
716
+ function isBinaryOrMediaContentType(contentType) {
717
+ return /^(image|audio|video)\//i.test(contentType) ||
718
+ /application\/(?:octet-stream|pdf|zip|gzip|x-tar)/i.test(contentType);
719
+ }
720
+ function hasSuspiciousXssResponse(body) {
721
+ return /javascript:\s*(?:alert|confirm|prompt|eval|fetch|\w+\()/i.test(body) ||
722
+ /on(?:error|load|click|mouseover|focus)\s*=\s*["']?(?:alert|confirm|prompt|eval|fetch|javascript:)/i.test(body) ||
723
+ /<script\b(?![^>]*\bsrc=)[^>]*>[\s\S]{0,500}?(?:alert|confirm|prompt|document\.cookie|localStorage|eval|fetch\s*\()/i.test(body);
724
+ }
725
+ function looksLikeHtmlOrScript(body) {
726
+ return /^\s*(?:<!doctype\s+html|<html\b|<script\b)/i.test(body) ||
727
+ /javascript:|<script\b/i.test(body);
728
+ }
729
+ function dedupeReasons(items) {
730
+ const seen = new Set();
731
+ return items.filter((item) => {
732
+ if (seen.has(item.code))
733
+ return false;
734
+ seen.add(item.code);
735
+ return true;
736
+ });
737
+ }
236
738
  function normalizeCommand(value) {
237
739
  return value.trim().replace(/\s+/g, ' ').toLowerCase();
238
740
  }