@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.
- package/dist/action/detectors/exec.d.ts.map +1 -1
- package/dist/action/detectors/exec.js +22 -4
- package/dist/action/detectors/exec.js.map +1 -1
- package/dist/action/detectors/network.d.ts.map +1 -1
- package/dist/action/detectors/network.js +50 -6
- package/dist/action/detectors/network.js.map +1 -1
- package/dist/action/index.d.ts +4 -0
- package/dist/action/index.d.ts.map +1 -1
- package/dist/action/index.js +44 -5
- package/dist/action/index.js.map +1 -1
- package/dist/adapters/claude-code.d.ts.map +1 -1
- package/dist/adapters/claude-code.js +7 -2
- package/dist/adapters/claude-code.js.map +1 -1
- package/dist/adapters/common.d.ts.map +1 -1
- package/dist/adapters/common.js +1 -0
- package/dist/adapters/common.js.map +1 -1
- package/dist/adapters/hermes.d.ts.map +1 -1
- package/dist/adapters/hermes.js +12 -2
- package/dist/adapters/hermes.js.map +1 -1
- package/dist/adapters/openclaw-plugin.d.ts.map +1 -1
- package/dist/adapters/openclaw-plugin.js +26 -3
- package/dist/adapters/openclaw-plugin.js.map +1 -1
- package/dist/adapters/openclaw.d.ts.map +1 -1
- package/dist/adapters/openclaw.js +6 -0
- package/dist/adapters/openclaw.js.map +1 -1
- package/dist/cli.js +41 -12
- package/dist/cli.js.map +1 -1
- package/dist/installers.js +28 -20
- package/dist/installers.js.map +1 -1
- package/dist/mcp-server.js +2 -2
- package/dist/mcp-server.js.map +1 -1
- package/dist/runtime/approvals.d.ts.map +1 -1
- package/dist/runtime/approvals.js +7 -1
- package/dist/runtime/approvals.js.map +1 -1
- package/dist/runtime/evaluator.d.ts +1 -0
- package/dist/runtime/evaluator.d.ts.map +1 -1
- package/dist/runtime/evaluator.js +511 -9
- package/dist/runtime/evaluator.js.map +1 -1
- package/dist/runtime/policy.d.ts.map +1 -1
- package/dist/runtime/policy.js +1 -0
- package/dist/runtime/policy.js.map +1 -1
- package/dist/runtime/protect.d.ts +1 -0
- package/dist/runtime/protect.d.ts.map +1 -1
- package/dist/runtime/protect.js +52 -8
- package/dist/runtime/protect.js.map +1 -1
- package/dist/runtime/types.d.ts +3 -1
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/tests/action.test.js +69 -8
- package/dist/tests/action.test.js.map +1 -1
- package/dist/tests/adapter.test.js +21 -7
- package/dist/tests/adapter.test.js.map +1 -1
- package/dist/tests/cli-checkup.test.js +1 -1
- package/dist/tests/cli-checkup.test.js.map +1 -1
- package/dist/tests/cli-connect.test.js +77 -3
- package/dist/tests/cli-connect.test.js.map +1 -1
- package/dist/tests/cli-init.test.js +12 -2
- package/dist/tests/cli-init.test.js.map +1 -1
- package/dist/tests/cli-policy.test.js +34 -4
- package/dist/tests/cli-policy.test.js.map +1 -1
- package/dist/tests/cli-subscribe.test.js +80 -3
- package/dist/tests/cli-subscribe.test.js.map +1 -1
- package/dist/tests/installer.test.js +35 -0
- package/dist/tests/installer.test.js.map +1 -1
- package/dist/tests/integration.test.js +22 -0
- package/dist/tests/integration.test.js.map +1 -1
- package/dist/tests/runtime-cloud.test.js +368 -0
- package/dist/tests/runtime-cloud.test.js.map +1 -1
- package/dist/tests/smoke.test.js +9 -4
- package/dist/tests/smoke.test.js.map +1 -1
- package/dist/types/action.d.ts +9 -3
- package/dist/types/action.d.ts.map +1 -1
- package/docs/SECURITY-POLICY.md +1 -1
- package/docs/claude-code.md +2 -1
- package/docs/hermes.md +6 -2
- package/package.json +1 -1
- package/skills/agentguard/README.md +3 -2
- package/skills/agentguard/SKILL.md +2 -1
- package/skills/agentguard/hermes-hooks.yaml +4 -1
- package/skills/agentguard/scripts/action-cli.js +6 -0
- 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 &&
|
|
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 {
|
|
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
|
|
387
|
+
const decision = policyDecisionFor(item, policy);
|
|
159
388
|
if (decision)
|
|
160
|
-
|
|
389
|
+
policyDecisions.push(decision);
|
|
161
390
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (
|
|
165
|
-
|
|
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(
|
|
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
|
}
|