@gendigital/sage-openclaw 0.4.3 → 0.5.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/README.md +1 -1
- package/dist/index.cjs +2394 -679
- package/dist/index.cjs.map +4 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +24 -8
- package/resources/threats/commands.yaml +51 -1
- package/resources/threats/credentials.yaml +2 -2
- package/resources/threats/files.yaml +14 -22
- package/resources/threats/mac-commands.yaml +355 -0
- package/resources/threats/mac-credentials.yaml +99 -0
- package/resources/threats/mac-files.yaml +124 -0
- package/resources/threats/mac-obfuscation.yaml +92 -0
- package/resources/threats/mac-persistence.yaml +117 -0
- package/resources/threats/mac-supply-chain.yaml +40 -0
- package/resources/threats/obfuscation.yaml +12 -0
- package/resources/threats/persistence.yaml +13 -13
- package/resources/threats/self-defense.yaml +2 -2
- package/resources/threats/win-files.yaml +11 -11
package/dist/index.cjs
CHANGED
|
@@ -3978,10 +3978,10 @@ var require_resolve_block_map = __commonJS({
|
|
|
3978
3978
|
let offset = bm.offset;
|
|
3979
3979
|
let commentEnd = null;
|
|
3980
3980
|
for (const collItem of bm.items) {
|
|
3981
|
-
const { start, key, sep, value } = collItem;
|
|
3981
|
+
const { start, key, sep: sep2, value } = collItem;
|
|
3982
3982
|
const keyProps = resolveProps.resolveProps(start, {
|
|
3983
3983
|
indicator: "explicit-key-ind",
|
|
3984
|
-
next: key ??
|
|
3984
|
+
next: key ?? sep2?.[0],
|
|
3985
3985
|
offset,
|
|
3986
3986
|
onError,
|
|
3987
3987
|
parentIndent: bm.indent,
|
|
@@ -3995,7 +3995,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
3995
3995
|
else if ("indent" in key && key.indent !== bm.indent)
|
|
3996
3996
|
onError(offset, "BAD_INDENT", startColMsg);
|
|
3997
3997
|
}
|
|
3998
|
-
if (!keyProps.anchor && !keyProps.tag && !
|
|
3998
|
+
if (!keyProps.anchor && !keyProps.tag && !sep2) {
|
|
3999
3999
|
commentEnd = keyProps.end;
|
|
4000
4000
|
if (keyProps.comment) {
|
|
4001
4001
|
if (map.comment)
|
|
@@ -4019,7 +4019,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
4019
4019
|
ctx.atKey = false;
|
|
4020
4020
|
if (utilMapIncludes.mapIncludes(ctx, map.items, keyNode))
|
|
4021
4021
|
onError(keyStart, "DUPLICATE_KEY", "Map keys must be unique");
|
|
4022
|
-
const valueProps = resolveProps.resolveProps(
|
|
4022
|
+
const valueProps = resolveProps.resolveProps(sep2 ?? [], {
|
|
4023
4023
|
indicator: "map-value-ind",
|
|
4024
4024
|
next: value,
|
|
4025
4025
|
offset: keyNode.range[2],
|
|
@@ -4035,7 +4035,7 @@ var require_resolve_block_map = __commonJS({
|
|
|
4035
4035
|
if (ctx.options.strict && keyProps.start < valueProps.found.offset - 1024)
|
|
4036
4036
|
onError(keyNode.range, "KEY_OVER_1024_CHARS", "The : indicator must be at most 1024 chars after the start of an implicit block mapping key");
|
|
4037
4037
|
}
|
|
4038
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset,
|
|
4038
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : composeEmptyNode(ctx, offset, sep2, null, valueProps, onError);
|
|
4039
4039
|
if (ctx.schema.compat)
|
|
4040
4040
|
utilFlowIndentCheck.flowIndentCheck(bm.indent, value, onError);
|
|
4041
4041
|
offset = valueNode.range[2];
|
|
@@ -4126,7 +4126,7 @@ var require_resolve_end = __commonJS({
|
|
|
4126
4126
|
let comment = "";
|
|
4127
4127
|
if (end) {
|
|
4128
4128
|
let hasSpace = false;
|
|
4129
|
-
let
|
|
4129
|
+
let sep2 = "";
|
|
4130
4130
|
for (const token of end) {
|
|
4131
4131
|
const { source, type } = token;
|
|
4132
4132
|
switch (type) {
|
|
@@ -4140,13 +4140,13 @@ var require_resolve_end = __commonJS({
|
|
|
4140
4140
|
if (!comment)
|
|
4141
4141
|
comment = cb;
|
|
4142
4142
|
else
|
|
4143
|
-
comment +=
|
|
4144
|
-
|
|
4143
|
+
comment += sep2 + cb;
|
|
4144
|
+
sep2 = "";
|
|
4145
4145
|
break;
|
|
4146
4146
|
}
|
|
4147
4147
|
case "newline":
|
|
4148
4148
|
if (comment)
|
|
4149
|
-
|
|
4149
|
+
sep2 += source;
|
|
4150
4150
|
hasSpace = true;
|
|
4151
4151
|
break;
|
|
4152
4152
|
default:
|
|
@@ -4189,18 +4189,18 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4189
4189
|
let offset = fc.offset + fc.start.source.length;
|
|
4190
4190
|
for (let i = 0; i < fc.items.length; ++i) {
|
|
4191
4191
|
const collItem = fc.items[i];
|
|
4192
|
-
const { start, key, sep, value } = collItem;
|
|
4192
|
+
const { start, key, sep: sep2, value } = collItem;
|
|
4193
4193
|
const props = resolveProps.resolveProps(start, {
|
|
4194
4194
|
flow: fcName,
|
|
4195
4195
|
indicator: "explicit-key-ind",
|
|
4196
|
-
next: key ??
|
|
4196
|
+
next: key ?? sep2?.[0],
|
|
4197
4197
|
offset,
|
|
4198
4198
|
onError,
|
|
4199
4199
|
parentIndent: fc.indent,
|
|
4200
4200
|
startOnNewline: false
|
|
4201
4201
|
});
|
|
4202
4202
|
if (!props.found) {
|
|
4203
|
-
if (!props.anchor && !props.tag && !
|
|
4203
|
+
if (!props.anchor && !props.tag && !sep2 && !value) {
|
|
4204
4204
|
if (i === 0 && props.comma)
|
|
4205
4205
|
onError(props.comma, "UNEXPECTED_TOKEN", `Unexpected , in ${fcName}`);
|
|
4206
4206
|
else if (i < fc.items.length - 1)
|
|
@@ -4254,8 +4254,8 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4254
4254
|
}
|
|
4255
4255
|
}
|
|
4256
4256
|
}
|
|
4257
|
-
if (!isMap && !
|
|
4258
|
-
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end,
|
|
4257
|
+
if (!isMap && !sep2 && !props.found) {
|
|
4258
|
+
const valueNode = value ? composeNode(ctx, value, props, onError) : composeEmptyNode(ctx, props.end, sep2, null, props, onError);
|
|
4259
4259
|
coll.items.push(valueNode);
|
|
4260
4260
|
offset = valueNode.range[2];
|
|
4261
4261
|
if (isBlock(value))
|
|
@@ -4267,7 +4267,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4267
4267
|
if (isBlock(key))
|
|
4268
4268
|
onError(keyNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
4269
4269
|
ctx.atKey = false;
|
|
4270
|
-
const valueProps = resolveProps.resolveProps(
|
|
4270
|
+
const valueProps = resolveProps.resolveProps(sep2 ?? [], {
|
|
4271
4271
|
flow: fcName,
|
|
4272
4272
|
indicator: "map-value-ind",
|
|
4273
4273
|
next: value,
|
|
@@ -4278,8 +4278,8 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4278
4278
|
});
|
|
4279
4279
|
if (valueProps.found) {
|
|
4280
4280
|
if (!isMap && !props.found && ctx.options.strict) {
|
|
4281
|
-
if (
|
|
4282
|
-
for (const st of
|
|
4281
|
+
if (sep2)
|
|
4282
|
+
for (const st of sep2) {
|
|
4283
4283
|
if (st === valueProps.found)
|
|
4284
4284
|
break;
|
|
4285
4285
|
if (st.type === "newline") {
|
|
@@ -4296,7 +4296,7 @@ var require_resolve_flow_collection = __commonJS({
|
|
|
4296
4296
|
else
|
|
4297
4297
|
onError(valueProps.start, "MISSING_CHAR", `Missing , or : between ${fcName} items`);
|
|
4298
4298
|
}
|
|
4299
|
-
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end,
|
|
4299
|
+
const valueNode = value ? composeNode(ctx, value, valueProps, onError) : valueProps.found ? composeEmptyNode(ctx, valueProps.end, sep2, null, valueProps, onError) : null;
|
|
4300
4300
|
if (valueNode) {
|
|
4301
4301
|
if (isBlock(value))
|
|
4302
4302
|
onError(valueNode.range, "BLOCK_IN_FLOW", blockMsg);
|
|
@@ -4476,7 +4476,7 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
4476
4476
|
chompStart = i + 1;
|
|
4477
4477
|
}
|
|
4478
4478
|
let value = "";
|
|
4479
|
-
let
|
|
4479
|
+
let sep2 = "";
|
|
4480
4480
|
let prevMoreIndented = false;
|
|
4481
4481
|
for (let i = 0; i < contentStart; ++i)
|
|
4482
4482
|
value += lines[i][0].slice(trimIndent) + "\n";
|
|
@@ -4493,24 +4493,24 @@ var require_resolve_block_scalar = __commonJS({
|
|
|
4493
4493
|
indent = "";
|
|
4494
4494
|
}
|
|
4495
4495
|
if (type === Scalar.Scalar.BLOCK_LITERAL) {
|
|
4496
|
-
value +=
|
|
4497
|
-
|
|
4496
|
+
value += sep2 + indent.slice(trimIndent) + content;
|
|
4497
|
+
sep2 = "\n";
|
|
4498
4498
|
} else if (indent.length > trimIndent || content[0] === " ") {
|
|
4499
|
-
if (
|
|
4500
|
-
|
|
4501
|
-
else if (!prevMoreIndented &&
|
|
4502
|
-
|
|
4503
|
-
value +=
|
|
4504
|
-
|
|
4499
|
+
if (sep2 === " ")
|
|
4500
|
+
sep2 = "\n";
|
|
4501
|
+
else if (!prevMoreIndented && sep2 === "\n")
|
|
4502
|
+
sep2 = "\n\n";
|
|
4503
|
+
value += sep2 + indent.slice(trimIndent) + content;
|
|
4504
|
+
sep2 = "\n";
|
|
4505
4505
|
prevMoreIndented = true;
|
|
4506
4506
|
} else if (content === "") {
|
|
4507
|
-
if (
|
|
4507
|
+
if (sep2 === "\n")
|
|
4508
4508
|
value += "\n";
|
|
4509
4509
|
else
|
|
4510
|
-
|
|
4510
|
+
sep2 = "\n";
|
|
4511
4511
|
} else {
|
|
4512
|
-
value +=
|
|
4513
|
-
|
|
4512
|
+
value += sep2 + content;
|
|
4513
|
+
sep2 = " ";
|
|
4514
4514
|
prevMoreIndented = false;
|
|
4515
4515
|
}
|
|
4516
4516
|
}
|
|
@@ -4692,25 +4692,25 @@ var require_resolve_flow_scalar = __commonJS({
|
|
|
4692
4692
|
if (!match)
|
|
4693
4693
|
return source;
|
|
4694
4694
|
let res = match[1];
|
|
4695
|
-
let
|
|
4695
|
+
let sep2 = " ";
|
|
4696
4696
|
let pos = first.lastIndex;
|
|
4697
4697
|
line.lastIndex = pos;
|
|
4698
4698
|
while (match = line.exec(source)) {
|
|
4699
4699
|
if (match[1] === "") {
|
|
4700
|
-
if (
|
|
4701
|
-
res +=
|
|
4700
|
+
if (sep2 === "\n")
|
|
4701
|
+
res += sep2;
|
|
4702
4702
|
else
|
|
4703
|
-
|
|
4703
|
+
sep2 = "\n";
|
|
4704
4704
|
} else {
|
|
4705
|
-
res +=
|
|
4706
|
-
|
|
4705
|
+
res += sep2 + match[1];
|
|
4706
|
+
sep2 = " ";
|
|
4707
4707
|
}
|
|
4708
4708
|
pos = line.lastIndex;
|
|
4709
4709
|
}
|
|
4710
4710
|
const last = /[ \t]*(.*)/sy;
|
|
4711
4711
|
last.lastIndex = pos;
|
|
4712
4712
|
match = last.exec(source);
|
|
4713
|
-
return res +
|
|
4713
|
+
return res + sep2 + (match?.[1] ?? "");
|
|
4714
4714
|
}
|
|
4715
4715
|
function doubleQuotedValue(source, onError) {
|
|
4716
4716
|
let res = "";
|
|
@@ -5511,14 +5511,14 @@ var require_cst_stringify = __commonJS({
|
|
|
5511
5511
|
}
|
|
5512
5512
|
}
|
|
5513
5513
|
}
|
|
5514
|
-
function stringifyItem({ start, key, sep, value }) {
|
|
5514
|
+
function stringifyItem({ start, key, sep: sep2, value }) {
|
|
5515
5515
|
let res = "";
|
|
5516
5516
|
for (const st of start)
|
|
5517
5517
|
res += st.source;
|
|
5518
5518
|
if (key)
|
|
5519
5519
|
res += stringifyToken(key);
|
|
5520
|
-
if (
|
|
5521
|
-
for (const st of
|
|
5520
|
+
if (sep2)
|
|
5521
|
+
for (const st of sep2)
|
|
5522
5522
|
res += st.source;
|
|
5523
5523
|
if (value)
|
|
5524
5524
|
res += stringifyToken(value);
|
|
@@ -6667,18 +6667,18 @@ var require_parser = __commonJS({
|
|
|
6667
6667
|
if (this.type === "map-value-ind") {
|
|
6668
6668
|
const prev = getPrevProps(this.peek(2));
|
|
6669
6669
|
const start = getFirstKeyStartProps(prev);
|
|
6670
|
-
let
|
|
6670
|
+
let sep2;
|
|
6671
6671
|
if (scalar.end) {
|
|
6672
|
-
|
|
6673
|
-
|
|
6672
|
+
sep2 = scalar.end;
|
|
6673
|
+
sep2.push(this.sourceToken);
|
|
6674
6674
|
delete scalar.end;
|
|
6675
6675
|
} else
|
|
6676
|
-
|
|
6676
|
+
sep2 = [this.sourceToken];
|
|
6677
6677
|
const map = {
|
|
6678
6678
|
type: "block-map",
|
|
6679
6679
|
offset: scalar.offset,
|
|
6680
6680
|
indent: scalar.indent,
|
|
6681
|
-
items: [{ start, key: scalar, sep }]
|
|
6681
|
+
items: [{ start, key: scalar, sep: sep2 }]
|
|
6682
6682
|
};
|
|
6683
6683
|
this.onKeyLine = true;
|
|
6684
6684
|
this.stack[this.stack.length - 1] = map;
|
|
@@ -6831,15 +6831,15 @@ var require_parser = __commonJS({
|
|
|
6831
6831
|
} else if (isFlowToken(it.key) && !includesToken(it.sep, "newline")) {
|
|
6832
6832
|
const start2 = getFirstKeyStartProps(it.start);
|
|
6833
6833
|
const key = it.key;
|
|
6834
|
-
const
|
|
6835
|
-
|
|
6834
|
+
const sep2 = it.sep;
|
|
6835
|
+
sep2.push(this.sourceToken);
|
|
6836
6836
|
delete it.key;
|
|
6837
6837
|
delete it.sep;
|
|
6838
6838
|
this.stack.push({
|
|
6839
6839
|
type: "block-map",
|
|
6840
6840
|
offset: this.offset,
|
|
6841
6841
|
indent: this.indent,
|
|
6842
|
-
items: [{ start: start2, key, sep }]
|
|
6842
|
+
items: [{ start: start2, key, sep: sep2 }]
|
|
6843
6843
|
});
|
|
6844
6844
|
} else if (start.length > 0) {
|
|
6845
6845
|
it.sep = it.sep.concat(start, this.sourceToken);
|
|
@@ -7033,13 +7033,13 @@ var require_parser = __commonJS({
|
|
|
7033
7033
|
const prev = getPrevProps(parent);
|
|
7034
7034
|
const start = getFirstKeyStartProps(prev);
|
|
7035
7035
|
fixFlowSeqItems(fc);
|
|
7036
|
-
const
|
|
7037
|
-
|
|
7036
|
+
const sep2 = fc.end.splice(1, fc.end.length);
|
|
7037
|
+
sep2.push(this.sourceToken);
|
|
7038
7038
|
const map = {
|
|
7039
7039
|
type: "block-map",
|
|
7040
7040
|
offset: fc.offset,
|
|
7041
7041
|
indent: fc.indent,
|
|
7042
|
-
items: [{ start, key: fc, sep }]
|
|
7042
|
+
items: [{ start, key: fc, sep: sep2 }]
|
|
7043
7043
|
};
|
|
7044
7044
|
this.onKeyLine = true;
|
|
7045
7045
|
this.stack[this.stack.length - 1] = map;
|
|
@@ -7324,11 +7324,6 @@ __export(index_exports, {
|
|
|
7324
7324
|
});
|
|
7325
7325
|
module.exports = __toCommonJS(index_exports);
|
|
7326
7326
|
|
|
7327
|
-
// src/approval-store.ts
|
|
7328
|
-
var import_node_crypto5 = require("node:crypto");
|
|
7329
|
-
var import_node_os5 = require("node:os");
|
|
7330
|
-
var import_node_path9 = require("node:path");
|
|
7331
|
-
|
|
7332
7327
|
// ../core/dist/config.js
|
|
7333
7328
|
var import_node_os = require("node:os");
|
|
7334
7329
|
var import_node_path2 = require("node:path");
|
|
@@ -7348,9 +7343,36 @@ function getFileContentRaw(path) {
|
|
|
7348
7343
|
async function atomicWriteJson(path, data) {
|
|
7349
7344
|
await fsPromises.mkdir((0, import_node_path.dirname)(path), { recursive: true });
|
|
7350
7345
|
const tmp = `${path}.${(0, import_node_crypto.randomBytes)(6).toString("hex")}.tmp`;
|
|
7351
|
-
|
|
7346
|
+
try {
|
|
7347
|
+
await fsPromises.writeFile(tmp, `${JSON.stringify(data, null, 2)}
|
|
7352
7348
|
`, { mode: 384 });
|
|
7353
|
-
|
|
7349
|
+
await fsPromises.rename(tmp, path);
|
|
7350
|
+
} catch (err) {
|
|
7351
|
+
try {
|
|
7352
|
+
await fsPromises.unlink(tmp);
|
|
7353
|
+
} catch {
|
|
7354
|
+
}
|
|
7355
|
+
throw err;
|
|
7356
|
+
}
|
|
7357
|
+
}
|
|
7358
|
+
async function pruneOrphanedTmpFiles(resolvedDir, maxAgeMs = 3e5) {
|
|
7359
|
+
try {
|
|
7360
|
+
const entries = await fsPromises.readdir(resolvedDir);
|
|
7361
|
+
const now = Date.now();
|
|
7362
|
+
for (const entry of entries) {
|
|
7363
|
+
if (!entry.endsWith(".tmp"))
|
|
7364
|
+
continue;
|
|
7365
|
+
try {
|
|
7366
|
+
const fullPath = (0, import_node_path.join)(resolvedDir, entry);
|
|
7367
|
+
const s = await fsPromises.stat(fullPath);
|
|
7368
|
+
if (now - s.mtimeMs > maxAgeMs) {
|
|
7369
|
+
await fsPromises.unlink(fullPath);
|
|
7370
|
+
}
|
|
7371
|
+
} catch {
|
|
7372
|
+
}
|
|
7373
|
+
}
|
|
7374
|
+
} catch {
|
|
7375
|
+
}
|
|
7354
7376
|
}
|
|
7355
7377
|
|
|
7356
7378
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
@@ -11445,7 +11467,9 @@ var AllowlistConfigSchema = external_exports.object({
|
|
|
11445
11467
|
var LoggingConfigSchema = external_exports.object({
|
|
11446
11468
|
enabled: external_exports.boolean().default(true),
|
|
11447
11469
|
log_clean: external_exports.boolean().default(false),
|
|
11448
|
-
path: external_exports.string().default("~/.sage/audit.jsonl")
|
|
11470
|
+
path: external_exports.string().default("~/.sage/audit.jsonl"),
|
|
11471
|
+
max_bytes: external_exports.number().int().min(0).default(5 * 1024 * 1024),
|
|
11472
|
+
max_files: external_exports.number().int().min(0).default(3)
|
|
11449
11473
|
});
|
|
11450
11474
|
var FileCheckConfigSchema = external_exports.object({
|
|
11451
11475
|
endpoint: external_exports.string().optional(),
|
|
@@ -11458,10 +11482,14 @@ var PackageCheckConfigSchema = external_exports.object({
|
|
|
11458
11482
|
// v1: all scoped packages (@scope/pkg) are skipped automatically.
|
|
11459
11483
|
// Future: add private_scopes / public_scopes config for fine-grained control.
|
|
11460
11484
|
});
|
|
11485
|
+
var AmsiCheckConfigSchema = external_exports.object({
|
|
11486
|
+
enabled: external_exports.boolean().default(true)
|
|
11487
|
+
});
|
|
11461
11488
|
var ConfigSchema = external_exports.object({
|
|
11462
11489
|
url_check: UrlCheckConfigSchema.default({}),
|
|
11463
11490
|
file_check: FileCheckConfigSchema.default({}),
|
|
11464
11491
|
package_check: PackageCheckConfigSchema.default({}),
|
|
11492
|
+
amsi_check: AmsiCheckConfigSchema.default({}),
|
|
11465
11493
|
heuristics_enabled: external_exports.boolean().default(true),
|
|
11466
11494
|
cache: CacheConfigSchema.default({}),
|
|
11467
11495
|
allowlist: AllowlistConfigSchema.default({}),
|
|
@@ -11471,37 +11499,112 @@ var ConfigSchema = external_exports.object({
|
|
|
11471
11499
|
});
|
|
11472
11500
|
|
|
11473
11501
|
// ../core/dist/config.js
|
|
11474
|
-
var
|
|
11502
|
+
var SAGE_DIR = "~/.sage";
|
|
11503
|
+
function currentHomeDir() {
|
|
11504
|
+
const envHome = process.env.HOME?.trim();
|
|
11505
|
+
return envHome && envHome.length > 0 ? envHome : (0, import_node_os.homedir)();
|
|
11506
|
+
}
|
|
11507
|
+
function resolvedSageDir() {
|
|
11508
|
+
return resolvePath(SAGE_DIR);
|
|
11509
|
+
}
|
|
11510
|
+
function defaultConfigPath() {
|
|
11511
|
+
return (0, import_node_path2.join)(resolvedSageDir(), "config.json");
|
|
11512
|
+
}
|
|
11513
|
+
function defaultCachePath() {
|
|
11514
|
+
return (0, import_node_path2.join)(resolvedSageDir(), "cache.json");
|
|
11515
|
+
}
|
|
11516
|
+
function defaultAllowlistPath() {
|
|
11517
|
+
return (0, import_node_path2.join)(resolvedSageDir(), "allowlist.json");
|
|
11518
|
+
}
|
|
11519
|
+
function defaultAuditPath() {
|
|
11520
|
+
return (0, import_node_path2.join)(resolvedSageDir(), "audit.jsonl");
|
|
11521
|
+
}
|
|
11475
11522
|
function resolvePath(pathStr) {
|
|
11476
11523
|
if (pathStr.startsWith("~/") || pathStr === "~") {
|
|
11477
|
-
return (0, import_node_path2.join)((
|
|
11524
|
+
return (0, import_node_path2.join)(currentHomeDir(), pathStr.slice(1));
|
|
11478
11525
|
}
|
|
11479
11526
|
return pathStr;
|
|
11480
11527
|
}
|
|
11528
|
+
function isWithinDirectory(baseDir, targetPath) {
|
|
11529
|
+
const rel = (0, import_node_path2.relative)(baseDir, targetPath);
|
|
11530
|
+
if (rel === "")
|
|
11531
|
+
return true;
|
|
11532
|
+
if ((0, import_node_path2.isAbsolute)(rel))
|
|
11533
|
+
return false;
|
|
11534
|
+
return rel !== ".." && !rel.startsWith(`..${import_node_path2.sep}`);
|
|
11535
|
+
}
|
|
11536
|
+
function normalizeStateFilePath(configuredPath, fallbackPath, field, logger) {
|
|
11537
|
+
const sageDir = resolvedSageDir();
|
|
11538
|
+
const trimmed = configuredPath.trim();
|
|
11539
|
+
if (trimmed === "") {
|
|
11540
|
+
logger.warn(`Config ${field}.path is empty; using default`, {
|
|
11541
|
+
configuredPath,
|
|
11542
|
+
defaultPath: fallbackPath
|
|
11543
|
+
});
|
|
11544
|
+
return fallbackPath;
|
|
11545
|
+
}
|
|
11546
|
+
const expanded = resolvePath(trimmed);
|
|
11547
|
+
const resolved = (0, import_node_path2.isAbsolute)(expanded) ? (0, import_node_path2.resolve)(expanded) : (0, import_node_path2.resolve)(sageDir, expanded);
|
|
11548
|
+
if (isWithinDirectory(sageDir, resolved)) {
|
|
11549
|
+
if (resolved === sageDir) {
|
|
11550
|
+
logger.warn(`Config ${field}.path must point to a file; using default`, {
|
|
11551
|
+
configuredPath,
|
|
11552
|
+
defaultPath: fallbackPath
|
|
11553
|
+
});
|
|
11554
|
+
return fallbackPath;
|
|
11555
|
+
}
|
|
11556
|
+
return resolved;
|
|
11557
|
+
}
|
|
11558
|
+
logger.warn(`Config ${field}.path escapes ${sageDir}; using default`, {
|
|
11559
|
+
configuredPath,
|
|
11560
|
+
defaultPath: fallbackPath
|
|
11561
|
+
});
|
|
11562
|
+
return fallbackPath;
|
|
11563
|
+
}
|
|
11564
|
+
function sanitizeConfigPaths(config, logger) {
|
|
11565
|
+
const cachePath = defaultCachePath();
|
|
11566
|
+
const allowlistPath = defaultAllowlistPath();
|
|
11567
|
+
const auditPath = defaultAuditPath();
|
|
11568
|
+
return {
|
|
11569
|
+
...config,
|
|
11570
|
+
cache: {
|
|
11571
|
+
...config.cache,
|
|
11572
|
+
path: normalizeStateFilePath(config.cache.path, cachePath, "cache", logger)
|
|
11573
|
+
},
|
|
11574
|
+
allowlist: {
|
|
11575
|
+
...config.allowlist,
|
|
11576
|
+
path: normalizeStateFilePath(config.allowlist.path, allowlistPath, "allowlist", logger)
|
|
11577
|
+
},
|
|
11578
|
+
logging: {
|
|
11579
|
+
...config.logging,
|
|
11580
|
+
path: normalizeStateFilePath(config.logging.path, auditPath, "logging", logger)
|
|
11581
|
+
}
|
|
11582
|
+
};
|
|
11583
|
+
}
|
|
11481
11584
|
async function loadConfig(configPath, logger = nullLogger) {
|
|
11482
|
-
const path = configPath ??
|
|
11585
|
+
const path = configPath ?? defaultConfigPath();
|
|
11483
11586
|
let raw;
|
|
11484
11587
|
try {
|
|
11485
11588
|
raw = await getFileContent(path);
|
|
11486
11589
|
} catch {
|
|
11487
|
-
return ConfigSchema.parse({});
|
|
11590
|
+
return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
|
|
11488
11591
|
}
|
|
11489
11592
|
let data;
|
|
11490
11593
|
try {
|
|
11491
11594
|
data = JSON.parse(raw);
|
|
11492
11595
|
} catch (e) {
|
|
11493
11596
|
logger.warn(`Failed to parse config from ${path}`, { error: String(e) });
|
|
11494
|
-
return ConfigSchema.parse({});
|
|
11597
|
+
return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
|
|
11495
11598
|
}
|
|
11496
11599
|
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
11497
11600
|
logger.warn(`Config file ${path} does not contain a JSON object`);
|
|
11498
|
-
return ConfigSchema.parse({});
|
|
11601
|
+
return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
|
|
11499
11602
|
}
|
|
11500
11603
|
try {
|
|
11501
|
-
return ConfigSchema.parse(data);
|
|
11604
|
+
return sanitizeConfigPaths(ConfigSchema.parse(data), logger);
|
|
11502
11605
|
} catch (e) {
|
|
11503
11606
|
logger.warn(`Config validation failed, using defaults`, { error: String(e) });
|
|
11504
|
-
return ConfigSchema.parse({});
|
|
11607
|
+
return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
|
|
11505
11608
|
}
|
|
11506
11609
|
}
|
|
11507
11610
|
|
|
@@ -11531,6 +11634,16 @@ function normalizeFilePath(raw) {
|
|
|
11531
11634
|
function emptyAllowlist() {
|
|
11532
11635
|
return { urls: {}, commands: {}, filePaths: {} };
|
|
11533
11636
|
}
|
|
11637
|
+
function addEntry(record, key, reason, originalVerdict) {
|
|
11638
|
+
record[key] = { addedAt: (/* @__PURE__ */ new Date()).toISOString(), reason, originalVerdict };
|
|
11639
|
+
}
|
|
11640
|
+
function removeEntry(record, key) {
|
|
11641
|
+
if (key in record) {
|
|
11642
|
+
delete record[key];
|
|
11643
|
+
return true;
|
|
11644
|
+
}
|
|
11645
|
+
return false;
|
|
11646
|
+
}
|
|
11534
11647
|
function parseEntries(raw) {
|
|
11535
11648
|
const entries = {};
|
|
11536
11649
|
for (const [key, entryData] of Object.entries(raw)) {
|
|
@@ -11583,6 +11696,29 @@ async function loadAllowlist(config, logger = nullLogger) {
|
|
|
11583
11696
|
filePaths
|
|
11584
11697
|
};
|
|
11585
11698
|
}
|
|
11699
|
+
async function saveAllowlist(allowlist, config, logger = nullLogger) {
|
|
11700
|
+
const path = resolvePath(config.path);
|
|
11701
|
+
const serializeEntries = (entries) => Object.fromEntries(Object.entries(entries).map(([key, entry]) => [
|
|
11702
|
+
key,
|
|
11703
|
+
{
|
|
11704
|
+
added_at: entry.addedAt,
|
|
11705
|
+
reason: entry.reason,
|
|
11706
|
+
original_verdict: entry.originalVerdict
|
|
11707
|
+
}
|
|
11708
|
+
]));
|
|
11709
|
+
const data = {
|
|
11710
|
+
urls: serializeEntries(allowlist.urls),
|
|
11711
|
+
commands: serializeEntries(allowlist.commands),
|
|
11712
|
+
file_paths: serializeEntries(allowlist.filePaths)
|
|
11713
|
+
};
|
|
11714
|
+
try {
|
|
11715
|
+
await atomicWriteJson(path, data);
|
|
11716
|
+
return true;
|
|
11717
|
+
} catch (e) {
|
|
11718
|
+
logger.warn(`Failed to save allowlist to ${path}`, { error: String(e) });
|
|
11719
|
+
return false;
|
|
11720
|
+
}
|
|
11721
|
+
}
|
|
11586
11722
|
function isAllowlisted(allowlist, artifacts) {
|
|
11587
11723
|
for (const artifact of artifacts) {
|
|
11588
11724
|
if (artifact.type !== "command") {
|
|
@@ -11606,6 +11742,115 @@ function isAllowlisted(allowlist, artifacts) {
|
|
|
11606
11742
|
}
|
|
11607
11743
|
return false;
|
|
11608
11744
|
}
|
|
11745
|
+
function addUrl(allowlist, url, reason, originalVerdict) {
|
|
11746
|
+
addEntry(allowlist.urls, normalizeUrl(url), reason, originalVerdict);
|
|
11747
|
+
}
|
|
11748
|
+
function addCommand(allowlist, command, reason, originalVerdict) {
|
|
11749
|
+
addEntry(allowlist.commands, hashCommand(command), reason, originalVerdict);
|
|
11750
|
+
}
|
|
11751
|
+
function addFilePath(allowlist, filePath, reason, originalVerdict) {
|
|
11752
|
+
addEntry(allowlist.filePaths, normalizeFilePath(filePath), reason, originalVerdict);
|
|
11753
|
+
}
|
|
11754
|
+
function removeFilePath(allowlist, filePath) {
|
|
11755
|
+
return removeEntry(allowlist.filePaths, normalizeFilePath(filePath));
|
|
11756
|
+
}
|
|
11757
|
+
function removeUrl(allowlist, url) {
|
|
11758
|
+
return removeEntry(allowlist.urls, normalizeUrl(url));
|
|
11759
|
+
}
|
|
11760
|
+
function removeCommand(allowlist, commandHash) {
|
|
11761
|
+
return removeEntry(allowlist.commands, commandHash);
|
|
11762
|
+
}
|
|
11763
|
+
|
|
11764
|
+
// ../core/dist/approval-store.js
|
|
11765
|
+
var import_node_crypto3 = require("node:crypto");
|
|
11766
|
+
var PENDING_STALE_MS = 60 * 60 * 1e3;
|
|
11767
|
+
var APPROVED_TTL_MS = 10 * 60 * 1e3;
|
|
11768
|
+
var ApprovalStore = class _ApprovalStore {
|
|
11769
|
+
pending = /* @__PURE__ */ new Map();
|
|
11770
|
+
approved = /* @__PURE__ */ new Map();
|
|
11771
|
+
static actionId(toolName, params, sessionId) {
|
|
11772
|
+
const payload = JSON.stringify({ toolName, params, sessionId });
|
|
11773
|
+
return (0, import_node_crypto3.createHash)("sha256").update(payload).digest("hex");
|
|
11774
|
+
}
|
|
11775
|
+
static artifactId(type, value) {
|
|
11776
|
+
return `${type}:${value}`;
|
|
11777
|
+
}
|
|
11778
|
+
setPending(actionId, entry) {
|
|
11779
|
+
this.pending.set(actionId, { ...entry, artifacts: [...entry.artifacts] });
|
|
11780
|
+
}
|
|
11781
|
+
deletePending(actionId) {
|
|
11782
|
+
this.pending.delete(actionId);
|
|
11783
|
+
}
|
|
11784
|
+
approve(actionId) {
|
|
11785
|
+
const entry = this.pending.get(actionId);
|
|
11786
|
+
if (!entry)
|
|
11787
|
+
return null;
|
|
11788
|
+
const now = Date.now();
|
|
11789
|
+
this.approved.set(actionId, {
|
|
11790
|
+
artifacts: [...entry.artifacts],
|
|
11791
|
+
approvedAt: now,
|
|
11792
|
+
expiresAt: now + APPROVED_TTL_MS
|
|
11793
|
+
});
|
|
11794
|
+
this.pending.delete(actionId);
|
|
11795
|
+
return entry;
|
|
11796
|
+
}
|
|
11797
|
+
isApproved(actionId) {
|
|
11798
|
+
const entry = this.approved.get(actionId);
|
|
11799
|
+
if (!entry)
|
|
11800
|
+
return false;
|
|
11801
|
+
if (Date.now() >= entry.expiresAt) {
|
|
11802
|
+
this.approved.delete(actionId);
|
|
11803
|
+
return false;
|
|
11804
|
+
}
|
|
11805
|
+
return true;
|
|
11806
|
+
}
|
|
11807
|
+
hasApprovedArtifact(type, value) {
|
|
11808
|
+
return this.findApprovedAction(type, value) !== null;
|
|
11809
|
+
}
|
|
11810
|
+
consumeApprovedArtifact(type, value) {
|
|
11811
|
+
const id = _ApprovalStore.artifactId(type, value);
|
|
11812
|
+
for (const [actionId, entry] of this.approved.entries()) {
|
|
11813
|
+
if (Date.now() >= entry.expiresAt) {
|
|
11814
|
+
this.approved.delete(actionId);
|
|
11815
|
+
continue;
|
|
11816
|
+
}
|
|
11817
|
+
const idx = entry.artifacts.findIndex((a) => _ApprovalStore.artifactId(a.type, a.value) === id);
|
|
11818
|
+
if (idx !== -1) {
|
|
11819
|
+
entry.artifacts.splice(idx, 1);
|
|
11820
|
+
if (entry.artifacts.length === 0) {
|
|
11821
|
+
this.approved.delete(actionId);
|
|
11822
|
+
}
|
|
11823
|
+
return true;
|
|
11824
|
+
}
|
|
11825
|
+
}
|
|
11826
|
+
return false;
|
|
11827
|
+
}
|
|
11828
|
+
findApprovedAction(type, value) {
|
|
11829
|
+
const id = _ApprovalStore.artifactId(type, value);
|
|
11830
|
+
for (const [actionId, entry] of this.approved.entries()) {
|
|
11831
|
+
if (Date.now() >= entry.expiresAt) {
|
|
11832
|
+
this.approved.delete(actionId);
|
|
11833
|
+
continue;
|
|
11834
|
+
}
|
|
11835
|
+
if (entry.artifacts.some((artifact) => _ApprovalStore.artifactId(artifact.type, artifact.value) === id)) {
|
|
11836
|
+
return actionId;
|
|
11837
|
+
}
|
|
11838
|
+
}
|
|
11839
|
+
return null;
|
|
11840
|
+
}
|
|
11841
|
+
cleanup(now = Date.now()) {
|
|
11842
|
+
for (const [actionId, entry] of this.pending.entries()) {
|
|
11843
|
+
if (now - entry.createdAt >= PENDING_STALE_MS) {
|
|
11844
|
+
this.pending.delete(actionId);
|
|
11845
|
+
}
|
|
11846
|
+
}
|
|
11847
|
+
for (const [actionId, entry] of this.approved.entries()) {
|
|
11848
|
+
if (now >= entry.expiresAt) {
|
|
11849
|
+
this.approved.delete(actionId);
|
|
11850
|
+
}
|
|
11851
|
+
}
|
|
11852
|
+
}
|
|
11853
|
+
};
|
|
11609
11854
|
|
|
11610
11855
|
// ../core/dist/audit-log.js
|
|
11611
11856
|
var import_promises = require("node:fs/promises");
|
|
@@ -11618,11 +11863,45 @@ function toolInputSummary(toolName, toolInput) {
|
|
|
11618
11863
|
if (toolName === "WebFetch") {
|
|
11619
11864
|
return String(toolInput.url ?? "").slice(0, MAX_SUMMARY_LEN);
|
|
11620
11865
|
}
|
|
11621
|
-
if (toolName === "Write" || toolName === "Edit") {
|
|
11866
|
+
if (toolName === "Write" || toolName === "Edit" || toolName === "Read" || toolName === "Delete") {
|
|
11622
11867
|
return String(toolInput.file_path ?? "").slice(0, MAX_SUMMARY_LEN);
|
|
11623
11868
|
}
|
|
11624
11869
|
return JSON.stringify(toolInput).slice(0, MAX_SUMMARY_LEN);
|
|
11625
11870
|
}
|
|
11871
|
+
async function rotateIfNeeded(filePath, maxBytes, maxFiles) {
|
|
11872
|
+
if (maxBytes <= 0 || maxFiles <= 0)
|
|
11873
|
+
return;
|
|
11874
|
+
let size;
|
|
11875
|
+
try {
|
|
11876
|
+
const s = await (0, import_promises.stat)(filePath);
|
|
11877
|
+
size = s.size;
|
|
11878
|
+
} catch {
|
|
11879
|
+
return;
|
|
11880
|
+
}
|
|
11881
|
+
if (size < maxBytes)
|
|
11882
|
+
return;
|
|
11883
|
+
try {
|
|
11884
|
+
await (0, import_promises.unlink)(`${filePath}.${maxFiles}`);
|
|
11885
|
+
} catch {
|
|
11886
|
+
}
|
|
11887
|
+
for (let i = maxFiles - 1; i >= 1; i--) {
|
|
11888
|
+
try {
|
|
11889
|
+
await (0, import_promises.rename)(`${filePath}.${i}`, `${filePath}.${i + 1}`);
|
|
11890
|
+
} catch {
|
|
11891
|
+
}
|
|
11892
|
+
}
|
|
11893
|
+
try {
|
|
11894
|
+
await (0, import_promises.rename)(filePath, `${filePath}.1`);
|
|
11895
|
+
} catch {
|
|
11896
|
+
}
|
|
11897
|
+
}
|
|
11898
|
+
async function appendEntry(config, entry) {
|
|
11899
|
+
const path = resolvePath(config.path);
|
|
11900
|
+
await (0, import_promises.mkdir)((0, import_node_path4.dirname)(path), { recursive: true });
|
|
11901
|
+
await rotateIfNeeded(path, config.max_bytes, config.max_files);
|
|
11902
|
+
await (0, import_promises.appendFile)(path, `${JSON.stringify(entry)}
|
|
11903
|
+
`);
|
|
11904
|
+
}
|
|
11626
11905
|
async function logVerdict(config, sessionId, toolName, toolInput, verdict, userOverride = false) {
|
|
11627
11906
|
if (!config.enabled)
|
|
11628
11907
|
return;
|
|
@@ -11641,11 +11920,8 @@ async function logVerdict(config, sessionId, toolName, toolInput, verdict, userO
|
|
|
11641
11920
|
source: verdict.source,
|
|
11642
11921
|
user_override: userOverride
|
|
11643
11922
|
};
|
|
11644
|
-
const path = resolvePath(config.path);
|
|
11645
11923
|
try {
|
|
11646
|
-
await (
|
|
11647
|
-
await (0, import_promises.appendFile)(path, `${JSON.stringify(entry)}
|
|
11648
|
-
`);
|
|
11924
|
+
await appendEntry(config, entry);
|
|
11649
11925
|
} catch {
|
|
11650
11926
|
}
|
|
11651
11927
|
}
|
|
@@ -11660,11 +11936,8 @@ async function logPluginScan(config, pluginKey, pluginVersion, findings) {
|
|
|
11660
11936
|
findings_count: findings.length,
|
|
11661
11937
|
findings
|
|
11662
11938
|
};
|
|
11663
|
-
const path = resolvePath(config.path);
|
|
11664
11939
|
try {
|
|
11665
|
-
await (
|
|
11666
|
-
await (0, import_promises.appendFile)(path, `${JSON.stringify(entry)}
|
|
11667
|
-
`);
|
|
11940
|
+
await appendEntry(config, entry);
|
|
11668
11941
|
} catch {
|
|
11669
11942
|
}
|
|
11670
11943
|
}
|
|
@@ -11807,169 +12080,802 @@ var VerdictCache = class {
|
|
|
11807
12080
|
}
|
|
11808
12081
|
};
|
|
11809
12082
|
|
|
11810
|
-
// ../core/dist/
|
|
11811
|
-
var
|
|
11812
|
-
|
|
11813
|
-
|
|
11814
|
-
var
|
|
11815
|
-
var
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
|
|
12083
|
+
// ../core/dist/clients/amsi.js
|
|
12084
|
+
var import_node_child_process = require("node:child_process");
|
|
12085
|
+
var AMSI_RESULT_DETECTED = 32768;
|
|
12086
|
+
var AMSI_RESULT_BLOCKED_BY_ADMIN_START = 16384;
|
|
12087
|
+
var MAX_SCAN_LENGTH = 1048576;
|
|
12088
|
+
var PS_TIMEOUT = 15e3;
|
|
12089
|
+
function isWSL() {
|
|
12090
|
+
if (process.platform !== "linux")
|
|
12091
|
+
return false;
|
|
12092
|
+
return !!process.env.WSL_DISTRO_NAME;
|
|
11819
12093
|
}
|
|
11820
|
-
|
|
11821
|
-
|
|
11822
|
-
{ name: "Content-Type", value: "application/json" },
|
|
11823
|
-
{ name: "User-Agent", value: SERVICE_NAME }
|
|
11824
|
-
];
|
|
11825
|
-
function getProviderName() {
|
|
11826
|
-
return "avast";
|
|
12094
|
+
function isAmsiSupported() {
|
|
12095
|
+
return process.platform === "win32" || isWSL();
|
|
11827
12096
|
}
|
|
11828
|
-
|
|
11829
|
-
|
|
11830
|
-
|
|
12097
|
+
function interpretAmsiResult(amsiResult, content, contentName) {
|
|
12098
|
+
return {
|
|
12099
|
+
content: content.length > 200 ? `${content.slice(0, 200)}...` : content,
|
|
12100
|
+
contentName,
|
|
12101
|
+
amsiResult,
|
|
12102
|
+
isDetected: amsiResult >= AMSI_RESULT_DETECTED,
|
|
12103
|
+
isBlockedByAdmin: amsiResult >= AMSI_RESULT_BLOCKED_BY_ADMIN_START && amsiResult < AMSI_RESULT_DETECTED
|
|
12104
|
+
};
|
|
12105
|
+
}
|
|
12106
|
+
var KoffiAmsiBackend = class {
|
|
11831
12107
|
logger;
|
|
11832
|
-
|
|
11833
|
-
|
|
11834
|
-
|
|
12108
|
+
context = null;
|
|
12109
|
+
session = null;
|
|
12110
|
+
available = false;
|
|
12111
|
+
/* biome-ignore lint/suspicious/noExplicitAny: koffi FFI functions have dynamic signatures */
|
|
12112
|
+
fnScanBuffer = null;
|
|
12113
|
+
/* biome-ignore lint/suspicious/noExplicitAny: koffi FFI functions have dynamic signatures */
|
|
12114
|
+
fnCloseSession = null;
|
|
12115
|
+
/* biome-ignore lint/suspicious/noExplicitAny: koffi FFI functions have dynamic signatures */
|
|
12116
|
+
fnUninitialize = null;
|
|
12117
|
+
constructor(logger) {
|
|
11835
12118
|
this.logger = logger;
|
|
11836
12119
|
}
|
|
11837
|
-
|
|
11838
|
-
return
|
|
12120
|
+
get isAvailable() {
|
|
12121
|
+
return this.available;
|
|
11839
12122
|
}
|
|
11840
|
-
async
|
|
12123
|
+
async init() {
|
|
11841
12124
|
try {
|
|
11842
|
-
const
|
|
11843
|
-
|
|
11844
|
-
|
|
11845
|
-
|
|
11846
|
-
|
|
11847
|
-
|
|
11848
|
-
|
|
11849
|
-
|
|
11850
|
-
|
|
12125
|
+
const koffiModule = await import("koffi");
|
|
12126
|
+
const koffi = koffiModule.default ?? koffiModule;
|
|
12127
|
+
const lib = koffi.load("amsi.dll");
|
|
12128
|
+
const _HAMSICONTEXT = koffi.pointer("HAMSICONTEXT", koffi.opaque());
|
|
12129
|
+
const _HAMSISESSION = koffi.pointer("HAMSISESSION", koffi.opaque());
|
|
12130
|
+
const AmsiInitialize = lib.func("int32 __stdcall AmsiInitialize(str16 appName, _Out_ HAMSICONTEXT *ctx)");
|
|
12131
|
+
const AmsiOpenSession = lib.func("int32 __stdcall AmsiOpenSession(HAMSICONTEXT ctx, _Out_ HAMSISESSION *session)");
|
|
12132
|
+
this.fnScanBuffer = lib.func("int32 __stdcall AmsiScanBuffer(HAMSICONTEXT ctx, void *buf, uint32 len, str16 contentName, HAMSISESSION session, _Out_ int32 *result)");
|
|
12133
|
+
this.fnCloseSession = lib.func("void __stdcall AmsiCloseSession(HAMSICONTEXT ctx, HAMSISESSION session)");
|
|
12134
|
+
this.fnUninitialize = lib.func("void __stdcall AmsiUninitialize(HAMSICONTEXT ctx)");
|
|
12135
|
+
const ctxOut = [null];
|
|
12136
|
+
const hr = AmsiInitialize("Sage", ctxOut);
|
|
12137
|
+
if (hr !== 0) {
|
|
12138
|
+
this.logger.warn("AMSI: koffi AmsiInitialize failed", { hr });
|
|
12139
|
+
return;
|
|
12140
|
+
}
|
|
12141
|
+
this.context = ctxOut[0];
|
|
12142
|
+
const sessOut = [null];
|
|
12143
|
+
const hr2 = AmsiOpenSession(this.context, sessOut);
|
|
12144
|
+
if (hr2 !== 0) {
|
|
12145
|
+
this.logger.warn("AMSI: koffi AmsiOpenSession failed", { hr: hr2 });
|
|
12146
|
+
try {
|
|
12147
|
+
if (this.fnUninitialize) {
|
|
12148
|
+
this.fnUninitialize(this.context);
|
|
11851
12149
|
}
|
|
11852
|
-
}
|
|
11853
|
-
|
|
11854
|
-
|
|
11855
|
-
|
|
11856
|
-
this.logger.warn(`FileCheck HTTP error: ${response.status}`);
|
|
11857
|
-
return null;
|
|
12150
|
+
} catch {
|
|
12151
|
+
}
|
|
12152
|
+
this.context = null;
|
|
12153
|
+
return;
|
|
11858
12154
|
}
|
|
11859
|
-
|
|
11860
|
-
|
|
12155
|
+
this.session = sessOut[0];
|
|
12156
|
+
this.available = true;
|
|
12157
|
+
this.logger.debug("AMSI: koffi backend initialized");
|
|
11861
12158
|
} catch (e) {
|
|
11862
|
-
this.logger.
|
|
11863
|
-
|
|
12159
|
+
this.logger.debug("AMSI: koffi backend init failed", { error: String(e) });
|
|
12160
|
+
this.available = false;
|
|
11864
12161
|
}
|
|
11865
12162
|
}
|
|
11866
|
-
|
|
11867
|
-
|
|
11868
|
-
|
|
11869
|
-
|
|
11870
|
-
return `https://${SERVICE_NAME}-proxy.${this.buildDomain()}${this.buildPath()}`;
|
|
11871
|
-
}
|
|
11872
|
-
parseResponse(data) {
|
|
12163
|
+
async scanString(content, contentName) {
|
|
12164
|
+
if (!this.available || !this.fnScanBuffer || !this.context || !this.session) {
|
|
12165
|
+
return null;
|
|
12166
|
+
}
|
|
11873
12167
|
try {
|
|
11874
|
-
const
|
|
11875
|
-
|
|
12168
|
+
const truncated = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
|
|
12169
|
+
const buf = Buffer.from(truncated, "utf-8");
|
|
12170
|
+
const resultOut = [0];
|
|
12171
|
+
const hr = this.fnScanBuffer(this.context, buf, buf.length, contentName, this.session, resultOut);
|
|
12172
|
+
if (hr !== 0) {
|
|
12173
|
+
this.logger.warn("AMSI: koffi AmsiScanBuffer failed", { hr, contentName });
|
|
11876
12174
|
return null;
|
|
11877
|
-
|
|
11878
|
-
const
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
return { severity, detectionNames };
|
|
12175
|
+
}
|
|
12176
|
+
const amsiResult = resultOut[0] ?? 0;
|
|
12177
|
+
this.logger.debug("AMSI: koffi scan result", { contentName, amsiResult });
|
|
12178
|
+
return interpretAmsiResult(amsiResult, content, contentName);
|
|
11882
12179
|
} catch (e) {
|
|
11883
|
-
this.logger.warn("
|
|
12180
|
+
this.logger.warn("AMSI: koffi scanBuffer failed", { error: String(e), contentName });
|
|
11884
12181
|
return null;
|
|
11885
12182
|
}
|
|
11886
12183
|
}
|
|
11887
|
-
|
|
11888
|
-
if (hashes.length === 0)
|
|
11889
|
-
return [];
|
|
11890
|
-
const batches = [];
|
|
11891
|
-
for (let i = 0; i < hashes.length; i += MAX_HASHES_PER_REQUEST) {
|
|
11892
|
-
batches.push(hashes.slice(i, i + MAX_HASHES_PER_REQUEST));
|
|
11893
|
-
}
|
|
11894
|
-
const batchResults = await Promise.all(batches.map((batch) => this.checkHashBatch(batch)));
|
|
11895
|
-
return batchResults.flat();
|
|
11896
|
-
}
|
|
11897
|
-
async checkHashBatch(hashes) {
|
|
12184
|
+
close() {
|
|
11898
12185
|
try {
|
|
11899
|
-
|
|
11900
|
-
|
|
11901
|
-
headers: Object.fromEntries(REQUEST_HEADERS.map((h) => [h.name, h.value])),
|
|
11902
|
-
body: JSON.stringify({
|
|
11903
|
-
requests: hashes.map((h) => ({ key: { sha256: h } })),
|
|
11904
|
-
client_info: {
|
|
11905
|
-
product_name: SERVICE_NAME,
|
|
11906
|
-
product_version: VERSION
|
|
11907
|
-
}
|
|
11908
|
-
}),
|
|
11909
|
-
signal: AbortSignal.timeout(this.timeoutMs)
|
|
11910
|
-
});
|
|
11911
|
-
if (!response.ok) {
|
|
11912
|
-
this.logger.warn(`FileCheck batch HTTP error: ${response.status}`);
|
|
11913
|
-
return [];
|
|
12186
|
+
if (this.session && this.context && this.fnCloseSession) {
|
|
12187
|
+
this.fnCloseSession(this.context, this.session);
|
|
11914
12188
|
}
|
|
11915
|
-
|
|
11916
|
-
return this.parseBatchResponse(data);
|
|
11917
|
-
} catch (e) {
|
|
11918
|
-
this.logger.warn("FileCheck batch request failed", { error: String(e) });
|
|
11919
|
-
return [];
|
|
12189
|
+
} catch {
|
|
11920
12190
|
}
|
|
11921
|
-
}
|
|
11922
|
-
parseBatchResponse(data) {
|
|
11923
|
-
const results = [];
|
|
11924
12191
|
try {
|
|
11925
|
-
|
|
11926
|
-
|
|
11927
|
-
const key = entry.key;
|
|
11928
|
-
const sha256 = key?.sha256 ?? "";
|
|
11929
|
-
if (!sha256)
|
|
11930
|
-
continue;
|
|
11931
|
-
const severity = entry.severity ?? "SEVERITY_UNKNOWN";
|
|
11932
|
-
const malwareNames = entry.malware_name ?? [];
|
|
11933
|
-
const detectionNames = malwareNames.filter((n) => typeof n === "string" && n.length > 0);
|
|
11934
|
-
results.push({ sha256, severity, detectionNames });
|
|
12192
|
+
if (this.context && this.fnUninitialize) {
|
|
12193
|
+
this.fnUninitialize(this.context);
|
|
11935
12194
|
}
|
|
11936
|
-
} catch
|
|
11937
|
-
this.logger.warn("Failed to parse FileCheck batch response", { error: String(e) });
|
|
12195
|
+
} catch {
|
|
11938
12196
|
}
|
|
11939
|
-
|
|
11940
|
-
|
|
11941
|
-
|
|
11942
|
-
return [this.getSubdomain(), getProviderName(), getProviderTld()].join(".");
|
|
12197
|
+
this.session = null;
|
|
12198
|
+
this.context = null;
|
|
12199
|
+
this.available = false;
|
|
11943
12200
|
}
|
|
11944
12201
|
};
|
|
12202
|
+
var AMSI_CSHARP_TYPE = `
|
|
12203
|
+
using System;
|
|
12204
|
+
using System.Runtime.InteropServices;
|
|
12205
|
+
using System.Text;
|
|
11945
12206
|
|
|
11946
|
-
|
|
11947
|
-
|
|
11948
|
-
|
|
11949
|
-
|
|
11950
|
-
|
|
11951
|
-
|
|
11952
|
-
|
|
11953
|
-
|
|
11954
|
-
|
|
11955
|
-
|
|
11956
|
-
|
|
11957
|
-
|
|
12207
|
+
public class SageAmsi {
|
|
12208
|
+
[DllImport("amsi.dll", CharSet = CharSet.Unicode)]
|
|
12209
|
+
static extern int AmsiInitialize(string appName, out IntPtr ctx);
|
|
12210
|
+
|
|
12211
|
+
[DllImport("amsi.dll")]
|
|
12212
|
+
static extern int AmsiOpenSession(IntPtr ctx, out IntPtr session);
|
|
12213
|
+
|
|
12214
|
+
[DllImport("amsi.dll", CharSet = CharSet.Unicode)]
|
|
12215
|
+
static extern int AmsiScanBuffer(
|
|
12216
|
+
IntPtr ctx, byte[] buf, uint len,
|
|
12217
|
+
string contentName, IntPtr session, out int result);
|
|
12218
|
+
|
|
12219
|
+
[DllImport("amsi.dll")]
|
|
12220
|
+
static extern void AmsiCloseSession(IntPtr ctx, IntPtr session);
|
|
12221
|
+
|
|
12222
|
+
[DllImport("amsi.dll")]
|
|
12223
|
+
static extern void AmsiUninitialize(IntPtr ctx);
|
|
12224
|
+
|
|
12225
|
+
private static IntPtr _ctx;
|
|
12226
|
+
private static IntPtr _session;
|
|
12227
|
+
private static bool _initialized;
|
|
12228
|
+
|
|
12229
|
+
public static bool Init() {
|
|
12230
|
+
int hr = AmsiInitialize("Sage", out _ctx);
|
|
12231
|
+
if (hr != 0) return false;
|
|
12232
|
+
hr = AmsiOpenSession(_ctx, out _session);
|
|
12233
|
+
if (hr != 0) {
|
|
12234
|
+
AmsiUninitialize(_ctx);
|
|
12235
|
+
return false;
|
|
12236
|
+
}
|
|
12237
|
+
_initialized = true;
|
|
12238
|
+
return true;
|
|
12239
|
+
}
|
|
12240
|
+
|
|
12241
|
+
public static int Scan(string content, string contentName) {
|
|
12242
|
+
if (!_initialized) return -1;
|
|
12243
|
+
byte[] bytes = Encoding.UTF8.GetBytes(content);
|
|
12244
|
+
int result;
|
|
12245
|
+
int hr = AmsiScanBuffer(_ctx, bytes, (uint)bytes.Length,
|
|
12246
|
+
contentName, _session, out result);
|
|
12247
|
+
if (hr != 0) return -1;
|
|
12248
|
+
return result;
|
|
12249
|
+
}
|
|
12250
|
+
|
|
12251
|
+
public static void Shutdown() {
|
|
12252
|
+
if (!_initialized) return;
|
|
12253
|
+
AmsiCloseSession(_ctx, _session);
|
|
12254
|
+
AmsiUninitialize(_ctx);
|
|
12255
|
+
_initialized = false;
|
|
12256
|
+
}
|
|
12257
|
+
}
|
|
12258
|
+
`;
|
|
12259
|
+
var PS_PERSISTENT_SCRIPT = `
|
|
12260
|
+
$ErrorActionPreference = 'Stop'
|
|
12261
|
+
try {
|
|
12262
|
+
Add-Type -TypeDefinition @'
|
|
12263
|
+
${AMSI_CSHARP_TYPE}
|
|
12264
|
+
'@
|
|
12265
|
+
|
|
12266
|
+
if (-not [SageAmsi]::Init()) {
|
|
12267
|
+
[Console]::Error.Write("AMSI initialization failed")
|
|
12268
|
+
exit 1
|
|
12269
|
+
}
|
|
12270
|
+
|
|
12271
|
+
[Console]::Out.WriteLine("READY")
|
|
12272
|
+
[Console]::Out.Flush()
|
|
12273
|
+
|
|
12274
|
+
while ($null -ne ($line = [Console]::In.ReadLine())) {
|
|
12275
|
+
try {
|
|
12276
|
+
$req = $line | ConvertFrom-Json
|
|
12277
|
+
$content = $req.content
|
|
12278
|
+
$cname = $req.contentName
|
|
12279
|
+
if (-not $cname) { $cname = 'sage:test' }
|
|
12280
|
+
$result = [SageAmsi]::Scan($content, $cname)
|
|
12281
|
+
[Console]::Out.WriteLine($result)
|
|
12282
|
+
[Console]::Out.Flush()
|
|
12283
|
+
} catch {
|
|
12284
|
+
[Console]::Out.WriteLine("-1")
|
|
12285
|
+
[Console]::Out.Flush()
|
|
12286
|
+
}
|
|
12287
|
+
}
|
|
12288
|
+
|
|
12289
|
+
[SageAmsi]::Shutdown()
|
|
12290
|
+
} catch {
|
|
12291
|
+
[Console]::Error.Write($_.Exception.Message)
|
|
12292
|
+
exit 1
|
|
12293
|
+
}
|
|
12294
|
+
`;
|
|
12295
|
+
var PS_ONESHOT_SCRIPT = `
|
|
12296
|
+
$ErrorActionPreference = 'Stop'
|
|
12297
|
+
try {
|
|
12298
|
+
Add-Type -TypeDefinition @'
|
|
12299
|
+
${AMSI_CSHARP_TYPE}
|
|
12300
|
+
'@
|
|
12301
|
+
|
|
12302
|
+
if (-not [SageAmsi]::Init()) {
|
|
12303
|
+
[Console]::Out.WriteLine("-1")
|
|
12304
|
+
exit
|
|
12305
|
+
}
|
|
12306
|
+
|
|
12307
|
+
$line = [Console]::In.ReadLine()
|
|
12308
|
+
$req = $line | ConvertFrom-Json
|
|
12309
|
+
$content = $req.content
|
|
12310
|
+
$cname = $req.contentName
|
|
12311
|
+
if (-not $cname) { $cname = 'sage:scan' }
|
|
12312
|
+
$result = [SageAmsi]::Scan($content, $cname)
|
|
12313
|
+
[SageAmsi]::Shutdown()
|
|
12314
|
+
[Console]::Out.WriteLine($result)
|
|
12315
|
+
[Console]::Out.Flush()
|
|
12316
|
+
} catch {
|
|
12317
|
+
[Console]::Out.WriteLine("-1")
|
|
12318
|
+
}
|
|
12319
|
+
`;
|
|
12320
|
+
var PersistentPowershellAmsiBackend = class {
|
|
12321
|
+
logger;
|
|
12322
|
+
process = null;
|
|
12323
|
+
available = false;
|
|
12324
|
+
stdoutBuffer = "";
|
|
12325
|
+
pendingResponse = null;
|
|
12326
|
+
constructor(logger) {
|
|
12327
|
+
this.logger = logger;
|
|
12328
|
+
}
|
|
12329
|
+
get isAvailable() {
|
|
12330
|
+
return this.available;
|
|
12331
|
+
}
|
|
12332
|
+
waitForLine(timeout) {
|
|
12333
|
+
return new Promise((resolve3, reject) => {
|
|
12334
|
+
const timer = setTimeout(() => {
|
|
12335
|
+
this.pendingResponse = null;
|
|
12336
|
+
reject(new Error("timeout"));
|
|
12337
|
+
}, timeout);
|
|
12338
|
+
this.pendingResponse = { resolve: resolve3, reject, timer };
|
|
12339
|
+
});
|
|
12340
|
+
}
|
|
12341
|
+
async init() {
|
|
12342
|
+
try {
|
|
12343
|
+
this.process = (0, import_node_child_process.spawn)("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", PS_PERSISTENT_SCRIPT], {
|
|
12344
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
12345
|
+
windowsHide: true
|
|
12346
|
+
});
|
|
12347
|
+
this.process.on("error", (err) => {
|
|
12348
|
+
this.logger.debug("AMSI: PowerShell process error", { error: String(err) });
|
|
12349
|
+
this.available = false;
|
|
12350
|
+
if (this.pendingResponse) {
|
|
12351
|
+
const { reject, timer } = this.pendingResponse;
|
|
12352
|
+
this.pendingResponse = null;
|
|
12353
|
+
clearTimeout(timer);
|
|
12354
|
+
reject(err);
|
|
12355
|
+
}
|
|
12356
|
+
});
|
|
12357
|
+
this.process.on("exit", () => {
|
|
12358
|
+
this.available = false;
|
|
12359
|
+
if (this.pendingResponse) {
|
|
12360
|
+
const { reject, timer } = this.pendingResponse;
|
|
12361
|
+
this.pendingResponse = null;
|
|
12362
|
+
clearTimeout(timer);
|
|
12363
|
+
reject(new Error("process exited"));
|
|
12364
|
+
}
|
|
12365
|
+
});
|
|
12366
|
+
this.process.stdout?.on("data", (chunk) => {
|
|
12367
|
+
this.stdoutBuffer += chunk.toString();
|
|
12368
|
+
let idx = this.stdoutBuffer.indexOf("\n");
|
|
12369
|
+
while (idx !== -1) {
|
|
12370
|
+
const line = this.stdoutBuffer.slice(0, idx).trim();
|
|
12371
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(idx + 1);
|
|
12372
|
+
if (this.pendingResponse) {
|
|
12373
|
+
const { resolve: resolve3, timer } = this.pendingResponse;
|
|
12374
|
+
this.pendingResponse = null;
|
|
12375
|
+
clearTimeout(timer);
|
|
12376
|
+
resolve3(line);
|
|
12377
|
+
}
|
|
12378
|
+
idx = this.stdoutBuffer.indexOf("\n");
|
|
12379
|
+
}
|
|
12380
|
+
});
|
|
12381
|
+
this.process.stdin?.on("error", () => {
|
|
12382
|
+
});
|
|
12383
|
+
this.process.stderr?.on("data", (chunk) => {
|
|
12384
|
+
this.logger.debug("AMSI: PowerShell stderr", {
|
|
12385
|
+
data: chunk.toString().slice(0, 200)
|
|
12386
|
+
});
|
|
12387
|
+
});
|
|
12388
|
+
const readyLine = await this.waitForLine(PS_TIMEOUT);
|
|
12389
|
+
if (readyLine === "READY") {
|
|
12390
|
+
this.available = true;
|
|
12391
|
+
this.logger.debug("AMSI: PowerShell persistent backend initialized");
|
|
12392
|
+
} else {
|
|
12393
|
+
this.logger.debug("AMSI: PowerShell unexpected ready signal", { readyLine });
|
|
12394
|
+
this.close();
|
|
12395
|
+
}
|
|
12396
|
+
} catch (e) {
|
|
12397
|
+
this.logger.debug("AMSI: PowerShell persistent backend init failed", {
|
|
12398
|
+
error: String(e)
|
|
12399
|
+
});
|
|
12400
|
+
this.close();
|
|
12401
|
+
}
|
|
12402
|
+
}
|
|
12403
|
+
async scanString(content, contentName) {
|
|
12404
|
+
if (!this.available || !this.process)
|
|
12405
|
+
return null;
|
|
12406
|
+
const truncated = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
|
|
12407
|
+
try {
|
|
12408
|
+
const req = JSON.stringify({ content: truncated, contentName });
|
|
12409
|
+
this.process.stdin?.write(`${req}
|
|
12410
|
+
`);
|
|
12411
|
+
const line = await this.waitForLine(PS_TIMEOUT);
|
|
12412
|
+
const amsiResult = parseInt(line, 10);
|
|
12413
|
+
if (Number.isNaN(amsiResult) || amsiResult < 0) {
|
|
12414
|
+
this.logger.warn("AMSI: PowerShell scan returned invalid result", {
|
|
12415
|
+
stdout: line.slice(0, 100),
|
|
12416
|
+
contentName
|
|
12417
|
+
});
|
|
12418
|
+
return null;
|
|
12419
|
+
}
|
|
12420
|
+
this.logger.debug("AMSI: PowerShell scan result", { contentName, amsiResult });
|
|
12421
|
+
return interpretAmsiResult(amsiResult, content, contentName);
|
|
12422
|
+
} catch (e) {
|
|
12423
|
+
this.logger.warn("AMSI: PowerShell scan failed", { error: String(e), contentName });
|
|
12424
|
+
return null;
|
|
12425
|
+
}
|
|
12426
|
+
}
|
|
12427
|
+
close() {
|
|
12428
|
+
if (this.process) {
|
|
12429
|
+
try {
|
|
12430
|
+
this.process.stdin?.end();
|
|
12431
|
+
} catch {
|
|
12432
|
+
}
|
|
12433
|
+
try {
|
|
12434
|
+
this.process.kill();
|
|
12435
|
+
} catch {
|
|
12436
|
+
}
|
|
12437
|
+
this.process = null;
|
|
12438
|
+
}
|
|
12439
|
+
if (this.pendingResponse) {
|
|
12440
|
+
const { reject, timer } = this.pendingResponse;
|
|
12441
|
+
this.pendingResponse = null;
|
|
12442
|
+
clearTimeout(timer);
|
|
12443
|
+
reject(new Error("closed"));
|
|
12444
|
+
}
|
|
12445
|
+
this.available = false;
|
|
12446
|
+
}
|
|
12447
|
+
};
|
|
12448
|
+
var WslPowershellAmsiBackend = class {
|
|
12449
|
+
logger;
|
|
12450
|
+
available = false;
|
|
12451
|
+
constructor(logger) {
|
|
12452
|
+
this.logger = logger;
|
|
12453
|
+
}
|
|
12454
|
+
get isAvailable() {
|
|
12455
|
+
return this.available;
|
|
12456
|
+
}
|
|
12457
|
+
async init() {
|
|
12458
|
+
this.available = true;
|
|
12459
|
+
this.logger.debug("AMSI: PowerShell one-shot backend ready (WSL)");
|
|
12460
|
+
}
|
|
12461
|
+
async scanString(content, contentName) {
|
|
12462
|
+
if (!this.available)
|
|
12463
|
+
return null;
|
|
12464
|
+
const truncated = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
|
|
12465
|
+
return new Promise((resolve3) => {
|
|
12466
|
+
try {
|
|
12467
|
+
const ps = (0, import_node_child_process.spawn)("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", PS_ONESHOT_SCRIPT], { stdio: ["pipe", "pipe", "pipe"] });
|
|
12468
|
+
let stdout = "";
|
|
12469
|
+
const timer = setTimeout(() => {
|
|
12470
|
+
this.logger.warn("AMSI: PowerShell one-shot timeout", { contentName });
|
|
12471
|
+
try {
|
|
12472
|
+
ps.kill();
|
|
12473
|
+
} catch {
|
|
12474
|
+
}
|
|
12475
|
+
resolve3(null);
|
|
12476
|
+
}, PS_TIMEOUT);
|
|
12477
|
+
ps.stdout?.on("data", (chunk) => {
|
|
12478
|
+
stdout += chunk.toString();
|
|
12479
|
+
});
|
|
12480
|
+
ps.stderr?.on("data", (chunk) => {
|
|
12481
|
+
this.logger.debug("AMSI: PowerShell one-shot stderr", {
|
|
12482
|
+
data: chunk.toString().slice(0, 200)
|
|
12483
|
+
});
|
|
12484
|
+
});
|
|
12485
|
+
ps.on("error", (err) => {
|
|
12486
|
+
clearTimeout(timer);
|
|
12487
|
+
this.logger.debug("AMSI: PowerShell one-shot error", { error: String(err) });
|
|
12488
|
+
resolve3(null);
|
|
12489
|
+
});
|
|
12490
|
+
ps.on("exit", () => {
|
|
12491
|
+
clearTimeout(timer);
|
|
12492
|
+
const amsiResult = parseInt(stdout.trim(), 10);
|
|
12493
|
+
if (Number.isNaN(amsiResult) || amsiResult < 0) {
|
|
12494
|
+
this.logger.warn("AMSI: PowerShell one-shot invalid result", {
|
|
12495
|
+
stdout: stdout.slice(0, 100),
|
|
12496
|
+
contentName
|
|
12497
|
+
});
|
|
12498
|
+
resolve3(null);
|
|
12499
|
+
return;
|
|
12500
|
+
}
|
|
12501
|
+
this.logger.debug("AMSI: PowerShell one-shot result", { contentName, amsiResult });
|
|
12502
|
+
resolve3(interpretAmsiResult(amsiResult, content, contentName));
|
|
12503
|
+
});
|
|
12504
|
+
const req = JSON.stringify({ content: truncated, contentName });
|
|
12505
|
+
ps.stdin?.write(`${req}
|
|
12506
|
+
`);
|
|
12507
|
+
ps.stdin?.end();
|
|
12508
|
+
} catch (e) {
|
|
12509
|
+
this.logger.warn("AMSI: PowerShell one-shot failed", { error: String(e), contentName });
|
|
12510
|
+
resolve3(null);
|
|
12511
|
+
}
|
|
12512
|
+
});
|
|
12513
|
+
}
|
|
12514
|
+
close() {
|
|
12515
|
+
this.available = false;
|
|
12516
|
+
}
|
|
12517
|
+
};
|
|
12518
|
+
var AmsiClient = class {
|
|
12519
|
+
logger;
|
|
12520
|
+
backend = null;
|
|
12521
|
+
constructor(logger = nullLogger) {
|
|
12522
|
+
this.logger = logger;
|
|
12523
|
+
}
|
|
12524
|
+
get isAvailable() {
|
|
12525
|
+
return this.backend?.isAvailable ?? false;
|
|
12526
|
+
}
|
|
12527
|
+
async init() {
|
|
12528
|
+
const wsl = isWSL();
|
|
12529
|
+
if (process.platform !== "win32" && !wsl) {
|
|
12530
|
+
this.logger.debug("AMSI: skipping, not Windows");
|
|
12531
|
+
return;
|
|
12532
|
+
}
|
|
12533
|
+
if (!wsl) {
|
|
12534
|
+
const koffi = new KoffiAmsiBackend(this.logger);
|
|
12535
|
+
await koffi.init();
|
|
12536
|
+
if (koffi.isAvailable) {
|
|
12537
|
+
this.backend = koffi;
|
|
12538
|
+
return;
|
|
12539
|
+
}
|
|
12540
|
+
}
|
|
12541
|
+
const ps = wsl ? new WslPowershellAmsiBackend(this.logger) : new PersistentPowershellAmsiBackend(this.logger);
|
|
12542
|
+
await ps.init();
|
|
12543
|
+
if (ps.isAvailable) {
|
|
12544
|
+
this.backend = ps;
|
|
12545
|
+
return;
|
|
12546
|
+
}
|
|
12547
|
+
this.logger.debug("AMSI: no backend available");
|
|
12548
|
+
}
|
|
12549
|
+
async scanString(content, contentName) {
|
|
12550
|
+
return await this.backend?.scanString(content, contentName) ?? null;
|
|
12551
|
+
}
|
|
12552
|
+
close() {
|
|
12553
|
+
this.backend?.close();
|
|
12554
|
+
this.backend = null;
|
|
12555
|
+
}
|
|
12556
|
+
};
|
|
12557
|
+
|
|
12558
|
+
// ../core/dist/version.js
|
|
12559
|
+
var VERSION = true ? "0.5.2" : "dev";
|
|
12560
|
+
|
|
12561
|
+
// ../core/dist/clients/file-check.js
|
|
12562
|
+
var DEFAULT_TIMEOUT = 5;
|
|
12563
|
+
var SERVICE_NAME = "sage";
|
|
12564
|
+
var MAX_HASHES_PER_REQUEST = 50;
|
|
12565
|
+
function getProviderTld() {
|
|
12566
|
+
return "com";
|
|
12567
|
+
}
|
|
12568
|
+
var REQUEST_HEADERS = [
|
|
12569
|
+
{ name: "Accept", value: "application/json" },
|
|
12570
|
+
{ name: "Content-Type", value: "application/json" },
|
|
12571
|
+
{ name: "User-Agent", value: SERVICE_NAME }
|
|
12572
|
+
];
|
|
12573
|
+
function getProviderName() {
|
|
12574
|
+
return "avast";
|
|
12575
|
+
}
|
|
12576
|
+
var FileCheckClient = class {
|
|
12577
|
+
endpoint;
|
|
12578
|
+
timeoutMs;
|
|
12579
|
+
logger;
|
|
12580
|
+
constructor(config, logger = nullLogger) {
|
|
12581
|
+
this.endpoint = config?.endpoint ?? this.resolveEndpoint();
|
|
12582
|
+
this.timeoutMs = (config?.timeout_seconds ?? DEFAULT_TIMEOUT) * 1e3;
|
|
12583
|
+
this.logger = logger;
|
|
12584
|
+
}
|
|
12585
|
+
buildPath() {
|
|
12586
|
+
return "/file-check";
|
|
12587
|
+
}
|
|
12588
|
+
async checkHash(hash) {
|
|
12589
|
+
try {
|
|
12590
|
+
const response = await fetch(this.endpoint, {
|
|
12591
|
+
method: "POST",
|
|
12592
|
+
headers: Object.fromEntries(REQUEST_HEADERS.map((h) => [h.name, h.value])),
|
|
12593
|
+
body: JSON.stringify({
|
|
12594
|
+
requests: [{ key: { sha256: hash } }],
|
|
12595
|
+
client_info: {
|
|
12596
|
+
product: {
|
|
12597
|
+
version_app: VERSION
|
|
12598
|
+
}
|
|
12599
|
+
}
|
|
12600
|
+
}),
|
|
12601
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
12602
|
+
});
|
|
12603
|
+
if (!response.ok) {
|
|
12604
|
+
this.logger.warn(`FileCheck HTTP error: ${response.status}`);
|
|
12605
|
+
return null;
|
|
12606
|
+
}
|
|
12607
|
+
const data = await response.json();
|
|
12608
|
+
return this.parseResponse(data);
|
|
12609
|
+
} catch (e) {
|
|
12610
|
+
this.logger.warn("FileCheck request failed", { error: String(e) });
|
|
12611
|
+
return null;
|
|
12612
|
+
}
|
|
12613
|
+
}
|
|
12614
|
+
getSubdomain() {
|
|
12615
|
+
return "svc";
|
|
12616
|
+
}
|
|
12617
|
+
resolveEndpoint() {
|
|
12618
|
+
return `https://${SERVICE_NAME}-proxy.${this.buildDomain()}${this.buildPath()}`;
|
|
12619
|
+
}
|
|
12620
|
+
parseResponse(data) {
|
|
12621
|
+
try {
|
|
12622
|
+
const responses = data.responses ?? [];
|
|
12623
|
+
if (responses.length === 0)
|
|
12624
|
+
return null;
|
|
12625
|
+
const first = responses[0];
|
|
12626
|
+
const severity = first.severity ?? "SEVERITY_UNKNOWN";
|
|
12627
|
+
const malwareNames = first.malware_name ?? [];
|
|
12628
|
+
const detectionNames = malwareNames.filter((n) => typeof n === "string" && n.length > 0);
|
|
12629
|
+
return { severity, detectionNames };
|
|
12630
|
+
} catch (e) {
|
|
12631
|
+
this.logger.warn("Failed to parse FileCheck response", { error: String(e) });
|
|
12632
|
+
return null;
|
|
12633
|
+
}
|
|
12634
|
+
}
|
|
12635
|
+
async checkHashes(hashes) {
|
|
12636
|
+
if (hashes.length === 0)
|
|
12637
|
+
return [];
|
|
12638
|
+
const batches = [];
|
|
12639
|
+
for (let i = 0; i < hashes.length; i += MAX_HASHES_PER_REQUEST) {
|
|
12640
|
+
batches.push(hashes.slice(i, i + MAX_HASHES_PER_REQUEST));
|
|
12641
|
+
}
|
|
12642
|
+
const batchResults = await Promise.all(batches.map((batch) => this.checkHashBatch(batch)));
|
|
12643
|
+
return batchResults.flat();
|
|
12644
|
+
}
|
|
12645
|
+
async checkHashBatch(hashes) {
|
|
12646
|
+
try {
|
|
12647
|
+
const response = await fetch(this.endpoint, {
|
|
12648
|
+
method: "POST",
|
|
12649
|
+
headers: Object.fromEntries(REQUEST_HEADERS.map((h) => [h.name, h.value])),
|
|
12650
|
+
body: JSON.stringify({
|
|
12651
|
+
requests: hashes.map((h) => ({ key: { sha256: h } })),
|
|
12652
|
+
client_info: {
|
|
12653
|
+
product_name: SERVICE_NAME,
|
|
12654
|
+
product_version: VERSION
|
|
12655
|
+
}
|
|
12656
|
+
}),
|
|
12657
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
12658
|
+
});
|
|
12659
|
+
if (!response.ok) {
|
|
12660
|
+
this.logger.warn(`FileCheck batch HTTP error: ${response.status}`);
|
|
12661
|
+
return [];
|
|
12662
|
+
}
|
|
12663
|
+
const data = await response.json();
|
|
12664
|
+
return this.parseBatchResponse(data);
|
|
12665
|
+
} catch (e) {
|
|
12666
|
+
this.logger.warn("FileCheck batch request failed", { error: String(e) });
|
|
12667
|
+
return [];
|
|
12668
|
+
}
|
|
12669
|
+
}
|
|
12670
|
+
parseBatchResponse(data) {
|
|
12671
|
+
const results = [];
|
|
12672
|
+
try {
|
|
12673
|
+
const responses = data.responses ?? [];
|
|
12674
|
+
for (const entry of responses) {
|
|
12675
|
+
const key = entry.key;
|
|
12676
|
+
const sha256 = key?.sha256 ?? "";
|
|
12677
|
+
if (!sha256)
|
|
12678
|
+
continue;
|
|
12679
|
+
const severity = entry.severity ?? "SEVERITY_UNKNOWN";
|
|
12680
|
+
const malwareNames = entry.malware_name ?? [];
|
|
12681
|
+
const detectionNames = malwareNames.filter((n) => typeof n === "string" && n.length > 0);
|
|
12682
|
+
results.push({ sha256, severity, detectionNames });
|
|
12683
|
+
}
|
|
12684
|
+
} catch (e) {
|
|
12685
|
+
this.logger.warn("Failed to parse FileCheck batch response", { error: String(e) });
|
|
12686
|
+
}
|
|
12687
|
+
return results;
|
|
12688
|
+
}
|
|
12689
|
+
buildDomain() {
|
|
12690
|
+
return [this.getSubdomain(), getProviderName(), getProviderTld()].join(".");
|
|
12691
|
+
}
|
|
12692
|
+
};
|
|
12693
|
+
|
|
12694
|
+
// ../core/dist/clients/package-registry.js
|
|
12695
|
+
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
12696
|
+
var PYPI_REGISTRY = "https://pypi.org/pypi";
|
|
12697
|
+
var DEFAULT_TIMEOUT2 = 5;
|
|
12698
|
+
var NPM_NAME_RE = /^(@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
12699
|
+
var PYPI_NAME_RE = /^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$/;
|
|
12700
|
+
var RegistryClient = class {
|
|
12701
|
+
timeoutMs;
|
|
12702
|
+
logger;
|
|
12703
|
+
constructor(config, logger = nullLogger) {
|
|
12704
|
+
this.timeoutMs = (config?.timeout_seconds ?? DEFAULT_TIMEOUT2) * 1e3;
|
|
12705
|
+
this.logger = logger;
|
|
12706
|
+
}
|
|
12707
|
+
async getPackageMetadata(name, registry, version) {
|
|
12708
|
+
if (registry === "npm")
|
|
12709
|
+
return this.getNpmMetadata(name, version);
|
|
12710
|
+
return this.getPypiMetadata(name, version);
|
|
12711
|
+
}
|
|
12712
|
+
async getNpmMetadata(name, version) {
|
|
12713
|
+
if (!NPM_NAME_RE.test(name)) {
|
|
12714
|
+
this.logger.warn("Invalid npm package name rejected", { name });
|
|
12715
|
+
return null;
|
|
12716
|
+
}
|
|
12717
|
+
const encodedName = name.includes("/") ? `${name.split("/")[0]}%2f${name.split("/")[1]}` : name;
|
|
12718
|
+
const url = `${NPM_REGISTRY}/${encodedName}`;
|
|
12719
|
+
try {
|
|
12720
|
+
const response = await fetch(url, {
|
|
12721
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
12722
|
+
});
|
|
12723
|
+
if (response.status === 404)
|
|
12724
|
+
return null;
|
|
12725
|
+
if (!response.ok) {
|
|
12726
|
+
this.logger.warn(`npm registry HTTP error: ${response.status}`, { name });
|
|
12727
|
+
throw new Error(`npm registry HTTP ${response.status}`);
|
|
12728
|
+
}
|
|
12729
|
+
const data = await response.json();
|
|
12730
|
+
return this.parseNpmResponse(name, data, version);
|
|
12731
|
+
} catch (e) {
|
|
12732
|
+
this.logger.warn("npm registry request failed", { name, error: String(e) });
|
|
12733
|
+
throw e;
|
|
12734
|
+
}
|
|
12735
|
+
}
|
|
12736
|
+
parseNpmResponse(name, data, requestedVersion) {
|
|
12737
|
+
try {
|
|
12738
|
+
const distTags = data["dist-tags"] ?? {};
|
|
12739
|
+
const latestTag = distTags.latest;
|
|
12740
|
+
if (!latestTag)
|
|
12741
|
+
return null;
|
|
12742
|
+
const versions = data.versions ?? {};
|
|
12743
|
+
const effectiveVersion = requestedVersion && distTags[requestedVersion] ? distTags[requestedVersion] : requestedVersion;
|
|
12744
|
+
const requestedVersionFound = !effectiveVersion || Boolean(versions[effectiveVersion]);
|
|
12745
|
+
const resolvedVersion = effectiveVersion && versions[effectiveVersion] ? effectiveVersion : latestTag;
|
|
12746
|
+
const resolvedVersionData = versions[resolvedVersion];
|
|
12747
|
+
if (!resolvedVersionData)
|
|
12748
|
+
return null;
|
|
12749
|
+
const dist = resolvedVersionData.dist ?? {};
|
|
12750
|
+
const shasum = dist.shasum ?? "";
|
|
12751
|
+
const integrity = dist.integrity ?? "";
|
|
12752
|
+
let latestHash = shasum;
|
|
12753
|
+
let hashAlgorithm = "sha1";
|
|
12754
|
+
if (integrity.startsWith("sha256-")) {
|
|
12755
|
+
latestHash = Buffer.from(integrity.slice(7), "base64").toString("hex");
|
|
12756
|
+
hashAlgorithm = "sha256";
|
|
12757
|
+
}
|
|
12758
|
+
const time = data.time ?? {};
|
|
12759
|
+
let firstReleaseDate = null;
|
|
12760
|
+
const created = time.created;
|
|
12761
|
+
if (created) {
|
|
12762
|
+
firstReleaseDate = new Date(created);
|
|
12763
|
+
}
|
|
12764
|
+
return {
|
|
12765
|
+
name,
|
|
12766
|
+
resolvedVersion,
|
|
12767
|
+
latestHash,
|
|
12768
|
+
hashAlgorithm,
|
|
12769
|
+
firstReleaseDate,
|
|
12770
|
+
requestedVersionFound
|
|
12771
|
+
};
|
|
12772
|
+
} catch (e) {
|
|
12773
|
+
this.logger.warn("Failed to parse npm response", { name, error: String(e) });
|
|
12774
|
+
return null;
|
|
12775
|
+
}
|
|
12776
|
+
}
|
|
12777
|
+
async getPypiMetadata(name, version) {
|
|
12778
|
+
if (!PYPI_NAME_RE.test(name)) {
|
|
12779
|
+
this.logger.warn("Invalid PyPI package name rejected", { name });
|
|
12780
|
+
return null;
|
|
12781
|
+
}
|
|
12782
|
+
const url = `${PYPI_REGISTRY}/${name}/json`;
|
|
12783
|
+
try {
|
|
12784
|
+
const response = await fetch(url, {
|
|
12785
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
12786
|
+
});
|
|
12787
|
+
if (response.status === 404)
|
|
12788
|
+
return null;
|
|
12789
|
+
if (!response.ok) {
|
|
12790
|
+
this.logger.warn(`PyPI registry HTTP error: ${response.status}`, { name });
|
|
12791
|
+
throw new Error(`PyPI registry HTTP ${response.status}`);
|
|
12792
|
+
}
|
|
12793
|
+
const data = await response.json();
|
|
12794
|
+
return this.parsePypiResponse(name, data, version);
|
|
12795
|
+
} catch (e) {
|
|
12796
|
+
this.logger.warn("PyPI registry request failed", { name, error: String(e) });
|
|
12797
|
+
throw e;
|
|
12798
|
+
}
|
|
12799
|
+
}
|
|
12800
|
+
parsePypiResponse(name, data, requestedVersion) {
|
|
12801
|
+
try {
|
|
12802
|
+
const info = data.info ?? {};
|
|
12803
|
+
const latestTag = info.version ?? "";
|
|
12804
|
+
if (!latestTag)
|
|
12805
|
+
return null;
|
|
12806
|
+
const releases = data.releases ?? {};
|
|
12807
|
+
const requestedVersionFound = !requestedVersion || Boolean(releases[requestedVersion]?.length);
|
|
12808
|
+
const resolvedVersion = requestedVersion && releases[requestedVersion]?.length ? requestedVersion : latestTag;
|
|
12809
|
+
let latestHash = "";
|
|
12810
|
+
const latestFiles = releases[resolvedVersion] ?? [];
|
|
12811
|
+
for (const file of latestFiles) {
|
|
12812
|
+
const digests = file.digests ?? {};
|
|
12813
|
+
if (digests.sha256) {
|
|
12814
|
+
latestHash = digests.sha256;
|
|
12815
|
+
break;
|
|
12816
|
+
}
|
|
12817
|
+
}
|
|
12818
|
+
let firstReleaseDate = null;
|
|
12819
|
+
for (const [, files] of Object.entries(releases)) {
|
|
12820
|
+
for (const file of files) {
|
|
12821
|
+
const uploadTime = file.upload_time_iso_8601;
|
|
12822
|
+
if (uploadTime) {
|
|
12823
|
+
const date = new Date(uploadTime);
|
|
12824
|
+
if (!firstReleaseDate || date < firstReleaseDate) {
|
|
12825
|
+
firstReleaseDate = date;
|
|
12826
|
+
}
|
|
12827
|
+
}
|
|
12828
|
+
}
|
|
12829
|
+
}
|
|
12830
|
+
return {
|
|
12831
|
+
name,
|
|
12832
|
+
resolvedVersion,
|
|
12833
|
+
latestHash,
|
|
12834
|
+
hashAlgorithm: "sha256",
|
|
12835
|
+
firstReleaseDate,
|
|
12836
|
+
requestedVersionFound
|
|
12837
|
+
};
|
|
12838
|
+
} catch (e) {
|
|
12839
|
+
this.logger.warn("Failed to parse PyPI response", { name, error: String(e) });
|
|
12840
|
+
return null;
|
|
12841
|
+
}
|
|
12842
|
+
}
|
|
12843
|
+
};
|
|
12844
|
+
|
|
12845
|
+
// ../core/dist/clients/url-check.js
|
|
12846
|
+
var DEFAULT_TIMEOUT3 = 5;
|
|
12847
|
+
var MAX_URLS_PER_REQUEST = 50;
|
|
12848
|
+
var SERVICE_NAME2 = "sage";
|
|
12849
|
+
function getProviderTld2() {
|
|
12850
|
+
return "com";
|
|
12851
|
+
}
|
|
12852
|
+
var REQUEST_HEADERS2 = [
|
|
12853
|
+
{ name: "Accept", value: "application/json" },
|
|
12854
|
+
{ name: "Content-Type", value: "application/json" },
|
|
12855
|
+
{ name: "User-Agent", value: SERVICE_NAME2 }
|
|
12856
|
+
];
|
|
11958
12857
|
function getProviderName2() {
|
|
11959
12858
|
return "avast";
|
|
11960
12859
|
}
|
|
12860
|
+
function getSubdomain() {
|
|
12861
|
+
return "svc";
|
|
12862
|
+
}
|
|
12863
|
+
function buildDomain() {
|
|
12864
|
+
return [getSubdomain(), getProviderName2(), getProviderTld2()].join(".");
|
|
12865
|
+
}
|
|
12866
|
+
function resolveEndpoint(path) {
|
|
12867
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
12868
|
+
return `https://${SERVICE_NAME2}-proxy.${buildDomain()}${normalizedPath}`;
|
|
12869
|
+
}
|
|
11961
12870
|
var UrlCheckClient = class {
|
|
11962
12871
|
endpoint;
|
|
11963
12872
|
timeoutMs;
|
|
11964
12873
|
logger;
|
|
11965
12874
|
constructor(config, logger = nullLogger) {
|
|
11966
|
-
this.endpoint = config?.endpoint ??
|
|
11967
|
-
this.timeoutMs = (config?.timeout_seconds ??
|
|
12875
|
+
this.endpoint = config?.endpoint ?? resolveEndpoint("/url-check");
|
|
12876
|
+
this.timeoutMs = (config?.timeout_seconds ?? DEFAULT_TIMEOUT3) * 1e3;
|
|
11968
12877
|
this.logger = logger;
|
|
11969
12878
|
}
|
|
11970
|
-
buildPath() {
|
|
11971
|
-
return "/url-check";
|
|
11972
|
-
}
|
|
11973
12879
|
async checkUrls(urls) {
|
|
11974
12880
|
if (urls.length === 0)
|
|
11975
12881
|
return [];
|
|
@@ -11980,9 +12886,6 @@ var UrlCheckClient = class {
|
|
|
11980
12886
|
const batchResults = await Promise.all(batches.map((batch) => this.checkBatch(batch)));
|
|
11981
12887
|
return batchResults.flat();
|
|
11982
12888
|
}
|
|
11983
|
-
getSubdomain() {
|
|
11984
|
-
return "svc";
|
|
11985
|
-
}
|
|
11986
12889
|
async checkBatch(urls) {
|
|
11987
12890
|
const queries = urls.map((url) => ({ key: { "url-like": url } }));
|
|
11988
12891
|
const payload = {
|
|
@@ -12018,9 +12921,6 @@ var UrlCheckClient = class {
|
|
|
12018
12921
|
return [];
|
|
12019
12922
|
}
|
|
12020
12923
|
}
|
|
12021
|
-
buildDomain() {
|
|
12022
|
-
return [this.getSubdomain(), getProviderName2(), getProviderTld2()].join(".");
|
|
12023
|
-
}
|
|
12024
12924
|
parseAnswer(answer) {
|
|
12025
12925
|
try {
|
|
12026
12926
|
const url = answer.key ?? "";
|
|
@@ -12045,9 +12945,6 @@ var UrlCheckClient = class {
|
|
|
12045
12945
|
return null;
|
|
12046
12946
|
}
|
|
12047
12947
|
}
|
|
12048
|
-
resolveEndpoint() {
|
|
12049
|
-
return `https://${SERVICE_NAME2}-proxy.${this.buildDomain()}${this.buildPath()}`;
|
|
12050
|
-
}
|
|
12051
12948
|
};
|
|
12052
12949
|
|
|
12053
12950
|
// ../core/dist/engine.js
|
|
@@ -12078,17 +12975,8 @@ var DecisionEngine = class {
|
|
|
12078
12975
|
constructor(sensitivity = "balanced") {
|
|
12079
12976
|
this.threshold = SENSITIVITY_THRESHOLDS[sensitivity] ?? CONFIDENCE_THRESHOLD;
|
|
12080
12977
|
}
|
|
12081
|
-
async decide(
|
|
12082
|
-
|
|
12083
|
-
if (Array.isArray(heuristicMatchesOrSources)) {
|
|
12084
|
-
sources = {
|
|
12085
|
-
heuristicMatches: heuristicMatchesOrSources,
|
|
12086
|
-
urlCheckResults: urlCheckResults ?? []
|
|
12087
|
-
};
|
|
12088
|
-
} else {
|
|
12089
|
-
sources = heuristicMatchesOrSources;
|
|
12090
|
-
}
|
|
12091
|
-
const signals = this.collectSignals(sources.heuristicMatches, sources.urlCheckResults, sources.packageCheckResults);
|
|
12978
|
+
async decide(sources) {
|
|
12979
|
+
const signals = this.collectSignals(sources.heuristicMatches, sources.urlCheckResults, sources.packageCheckResults, sources.amsiCheckResults);
|
|
12092
12980
|
if (signals.length === 0) {
|
|
12093
12981
|
return this.allowVerdict();
|
|
12094
12982
|
}
|
|
@@ -12115,7 +13003,7 @@ var DecisionEngine = class {
|
|
|
12115
13003
|
reasons: allReasons
|
|
12116
13004
|
};
|
|
12117
13005
|
}
|
|
12118
|
-
collectSignals(heuristicMatches, urlCheckResults, packageCheckResults) {
|
|
13006
|
+
collectSignals(heuristicMatches, urlCheckResults, packageCheckResults, amsiCheckResults) {
|
|
12119
13007
|
const signals = [];
|
|
12120
13008
|
for (const match of heuristicMatches) {
|
|
12121
13009
|
signals.push({
|
|
@@ -12165,6 +13053,33 @@ var DecisionEngine = class {
|
|
|
12165
13053
|
signals.push(signal);
|
|
12166
13054
|
}
|
|
12167
13055
|
}
|
|
13056
|
+
if (amsiCheckResults) {
|
|
13057
|
+
for (const result of amsiCheckResults) {
|
|
13058
|
+
if (result.isDetected) {
|
|
13059
|
+
signals.push({
|
|
13060
|
+
decision: "deny",
|
|
13061
|
+
category: "malware",
|
|
13062
|
+
confidence: 1,
|
|
13063
|
+
severity: "critical",
|
|
13064
|
+
source: "amsi",
|
|
13065
|
+
threatId: null,
|
|
13066
|
+
reason: `AMSI detected malware in ${result.contentName} (result=${result.amsiResult})`,
|
|
13067
|
+
artifact: result.contentName
|
|
13068
|
+
});
|
|
13069
|
+
} else if (result.isBlockedByAdmin) {
|
|
13070
|
+
signals.push({
|
|
13071
|
+
decision: "deny",
|
|
13072
|
+
category: "malware",
|
|
13073
|
+
confidence: 0.9,
|
|
13074
|
+
severity: "critical",
|
|
13075
|
+
source: "amsi",
|
|
13076
|
+
threatId: null,
|
|
13077
|
+
reason: `AMSI: content blocked by admin policy in ${result.contentName} (result=${result.amsiResult})`,
|
|
13078
|
+
artifact: result.contentName
|
|
13079
|
+
});
|
|
13080
|
+
}
|
|
13081
|
+
}
|
|
13082
|
+
}
|
|
12168
13083
|
return signals;
|
|
12169
13084
|
}
|
|
12170
13085
|
packageVerdictToSignal(pkg) {
|
|
@@ -12346,113 +13261,566 @@ async function loadTrustedDomains(allowlistsDir, logger = nullLogger) {
|
|
|
12346
13261
|
logger.warn(`Failed to parse ${filename}`, { error: String(e) });
|
|
12347
13262
|
continue;
|
|
12348
13263
|
}
|
|
12349
|
-
if (!Array.isArray(data)) {
|
|
12350
|
-
logger.warn(`Expected list in ${filename}, got ${typeof data}`);
|
|
13264
|
+
if (!Array.isArray(data)) {
|
|
13265
|
+
logger.warn(`Expected list in ${filename}, got ${typeof data}`);
|
|
13266
|
+
continue;
|
|
13267
|
+
}
|
|
13268
|
+
for (const entry of data) {
|
|
13269
|
+
if (typeof entry !== "object" || entry === null) {
|
|
13270
|
+
logger.warn(`Skipping non-object entry in ${filename}`);
|
|
13271
|
+
continue;
|
|
13272
|
+
}
|
|
13273
|
+
const record = entry;
|
|
13274
|
+
const domain = record.domain;
|
|
13275
|
+
const reason = record.reason;
|
|
13276
|
+
if (!domain || typeof domain !== "string") {
|
|
13277
|
+
logger.warn(`Skipping entry without valid 'domain' in ${filename}`);
|
|
13278
|
+
continue;
|
|
13279
|
+
}
|
|
13280
|
+
if (!reason || typeof reason !== "string") {
|
|
13281
|
+
logger.warn(`Skipping entry without valid 'reason' in ${filename}`);
|
|
13282
|
+
continue;
|
|
13283
|
+
}
|
|
13284
|
+
domains.push({ domain: domain.toLowerCase(), reason });
|
|
13285
|
+
}
|
|
13286
|
+
}
|
|
13287
|
+
logger.debug(`Loaded ${domains.length} trusted domains from ${allowlistsDir}`);
|
|
13288
|
+
return domains;
|
|
13289
|
+
}
|
|
13290
|
+
function extractDomain(url) {
|
|
13291
|
+
try {
|
|
13292
|
+
const parsed = new URL(url);
|
|
13293
|
+
return parsed.hostname.toLowerCase() || null;
|
|
13294
|
+
} catch {
|
|
13295
|
+
return null;
|
|
13296
|
+
}
|
|
13297
|
+
}
|
|
13298
|
+
function isTrustedDomain(domain, trusted) {
|
|
13299
|
+
const domainLower = domain.toLowerCase();
|
|
13300
|
+
for (const td of trusted) {
|
|
13301
|
+
if (domainLower === td.domain)
|
|
13302
|
+
return true;
|
|
13303
|
+
if (domainLower.endsWith(`.${td.domain}`))
|
|
13304
|
+
return true;
|
|
13305
|
+
}
|
|
13306
|
+
return false;
|
|
13307
|
+
}
|
|
13308
|
+
|
|
13309
|
+
// ../core/dist/heuristics.js
|
|
13310
|
+
var TRUSTED_DOMAIN_SUPPRESSIBLE = /* @__PURE__ */ new Set([
|
|
13311
|
+
"CLT-CMD-001",
|
|
13312
|
+
"CLT-CMD-002",
|
|
13313
|
+
"CLT-SUPPLY-001",
|
|
13314
|
+
"CLT-SUPPLY-004"
|
|
13315
|
+
]);
|
|
13316
|
+
var HeuristicsEngine = class {
|
|
13317
|
+
threatMap = /* @__PURE__ */ new Map();
|
|
13318
|
+
trustedDomains;
|
|
13319
|
+
constructor(threats, trustedDomains) {
|
|
13320
|
+
this.trustedDomains = trustedDomains ?? [];
|
|
13321
|
+
for (const threat of threats) {
|
|
13322
|
+
for (const matchType of threat.matchOn) {
|
|
13323
|
+
const artifactType = matchType === "domain" ? "url" : matchType;
|
|
13324
|
+
const existing = this.threatMap.get(artifactType);
|
|
13325
|
+
if (existing) {
|
|
13326
|
+
existing.push(threat);
|
|
13327
|
+
} else {
|
|
13328
|
+
this.threatMap.set(artifactType, [threat]);
|
|
13329
|
+
}
|
|
13330
|
+
}
|
|
13331
|
+
}
|
|
13332
|
+
}
|
|
13333
|
+
isSuppressedByTrustedDomain(match) {
|
|
13334
|
+
if (this.trustedDomains.length === 0)
|
|
13335
|
+
return false;
|
|
13336
|
+
if (!TRUSTED_DOMAIN_SUPPRESSIBLE.has(match.threat.id))
|
|
13337
|
+
return false;
|
|
13338
|
+
const urls = extractUrls(match.matchValue);
|
|
13339
|
+
if (urls.length === 0)
|
|
13340
|
+
return false;
|
|
13341
|
+
for (const url of urls) {
|
|
13342
|
+
const domain = extractDomain(url);
|
|
13343
|
+
if (!domain)
|
|
13344
|
+
return false;
|
|
13345
|
+
if (!isTrustedDomain(domain, this.trustedDomains))
|
|
13346
|
+
return false;
|
|
13347
|
+
}
|
|
13348
|
+
return true;
|
|
13349
|
+
}
|
|
13350
|
+
match(artifacts) {
|
|
13351
|
+
const matches = [];
|
|
13352
|
+
for (const artifact of artifacts) {
|
|
13353
|
+
const threats = this.threatMap.get(artifact.type) ?? [];
|
|
13354
|
+
for (const threat of threats) {
|
|
13355
|
+
const m = threat.compiledPattern.exec(artifact.value);
|
|
13356
|
+
if (m) {
|
|
13357
|
+
matches.push({
|
|
13358
|
+
threat,
|
|
13359
|
+
artifact: artifact.value,
|
|
13360
|
+
matchValue: m[0]
|
|
13361
|
+
});
|
|
13362
|
+
}
|
|
13363
|
+
}
|
|
13364
|
+
}
|
|
13365
|
+
if (this.trustedDomains.length > 0) {
|
|
13366
|
+
return matches.filter((m) => !this.isSuppressedByTrustedDomain(m));
|
|
13367
|
+
}
|
|
13368
|
+
return matches;
|
|
13369
|
+
}
|
|
13370
|
+
};
|
|
13371
|
+
|
|
13372
|
+
// ../core/dist/package-checker.js
|
|
13373
|
+
var SUSPICIOUS_AGE_DAYS = 7;
|
|
13374
|
+
var MAX_CONCURRENT = 10;
|
|
13375
|
+
var PackageChecker = class {
|
|
13376
|
+
registryClient;
|
|
13377
|
+
fileCheckClient;
|
|
13378
|
+
logger;
|
|
13379
|
+
constructor(config = {}, logger = nullLogger) {
|
|
13380
|
+
this.logger = logger;
|
|
13381
|
+
this.registryClient = new RegistryClient({ timeout_seconds: config.registryTimeoutSeconds }, logger);
|
|
13382
|
+
this.fileCheckClient = config.fileCheckEnabled !== false ? new FileCheckClient({
|
|
13383
|
+
endpoint: config.fileCheckEndpoint,
|
|
13384
|
+
timeout_seconds: config.fileCheckTimeoutSeconds
|
|
13385
|
+
}, logger) : null;
|
|
13386
|
+
}
|
|
13387
|
+
async checkPackages(packages) {
|
|
13388
|
+
const toCheck = [];
|
|
13389
|
+
const results = [];
|
|
13390
|
+
for (const pkg of packages) {
|
|
13391
|
+
if (pkg.name.startsWith("@")) {
|
|
13392
|
+
results.push({
|
|
13393
|
+
packageName: pkg.name,
|
|
13394
|
+
registry: pkg.registry,
|
|
13395
|
+
verdict: "clean",
|
|
13396
|
+
confidence: 1,
|
|
13397
|
+
details: "Scoped package \u2014 skipped (v1)"
|
|
13398
|
+
});
|
|
13399
|
+
} else {
|
|
13400
|
+
toCheck.push(pkg);
|
|
13401
|
+
}
|
|
13402
|
+
}
|
|
13403
|
+
const checkResults = await this.runWithConcurrencyLimit(toCheck, (pkg) => this.checkSinglePackage(pkg), MAX_CONCURRENT);
|
|
13404
|
+
results.push(...checkResults);
|
|
13405
|
+
return results;
|
|
13406
|
+
}
|
|
13407
|
+
async checkSinglePackage(pkg) {
|
|
13408
|
+
let metadata;
|
|
13409
|
+
try {
|
|
13410
|
+
metadata = await this.registryClient.getPackageMetadata(pkg.name, pkg.registry, pkg.version);
|
|
13411
|
+
} catch {
|
|
13412
|
+
return {
|
|
13413
|
+
packageName: pkg.name,
|
|
13414
|
+
registry: pkg.registry,
|
|
13415
|
+
verdict: "unknown",
|
|
13416
|
+
confidence: 0.6,
|
|
13417
|
+
details: `Package check for '${pkg.name}' timed out (verify manually)`
|
|
13418
|
+
};
|
|
13419
|
+
}
|
|
13420
|
+
if (metadata === null) {
|
|
13421
|
+
return {
|
|
13422
|
+
packageName: pkg.name,
|
|
13423
|
+
registry: pkg.registry,
|
|
13424
|
+
verdict: "not_found",
|
|
13425
|
+
confidence: 0.95,
|
|
13426
|
+
details: `Package '${pkg.name}' not found on ${pkg.registry} (non-existent or misspelled)`
|
|
13427
|
+
};
|
|
13428
|
+
}
|
|
13429
|
+
if (!metadata.requestedVersionFound && pkg.version) {
|
|
13430
|
+
return {
|
|
13431
|
+
packageName: pkg.name,
|
|
13432
|
+
registry: pkg.registry,
|
|
13433
|
+
verdict: "not_found",
|
|
13434
|
+
confidence: 0.9,
|
|
13435
|
+
details: `Package '${pkg.name}@${pkg.version}' version not found on ${pkg.registry} (hallucinated or unpublished)`
|
|
13436
|
+
};
|
|
13437
|
+
}
|
|
13438
|
+
if (metadata.latestHash && this.fileCheckClient && metadata.hashAlgorithm === "sha256") {
|
|
13439
|
+
try {
|
|
13440
|
+
const fileResult = await this.fileCheckClient.checkHash(metadata.latestHash);
|
|
13441
|
+
if (fileResult) {
|
|
13442
|
+
const sev = fileResult.severity.toUpperCase();
|
|
13443
|
+
if (sev === "SEVERITY_MALWARE") {
|
|
13444
|
+
const detections = fileResult.detectionNames.length > 0 ? fileResult.detectionNames.join(", ") : sev;
|
|
13445
|
+
return {
|
|
13446
|
+
packageName: pkg.name,
|
|
13447
|
+
registry: pkg.registry,
|
|
13448
|
+
verdict: "malicious",
|
|
13449
|
+
confidence: 1,
|
|
13450
|
+
details: `Malicious package '${pkg.name}' (${detections})`,
|
|
13451
|
+
fileCheckSeverity: sev
|
|
13452
|
+
};
|
|
13453
|
+
}
|
|
13454
|
+
}
|
|
13455
|
+
} catch {
|
|
13456
|
+
this.logger.warn("File-check failed for package", { name: pkg.name });
|
|
13457
|
+
}
|
|
13458
|
+
}
|
|
13459
|
+
const ageDays = metadata.firstReleaseDate ? (Date.now() - metadata.firstReleaseDate.getTime()) / (1e3 * 60 * 60 * 24) : void 0;
|
|
13460
|
+
if (ageDays !== void 0 && ageDays < SUSPICIOUS_AGE_DAYS) {
|
|
13461
|
+
return {
|
|
13462
|
+
packageName: pkg.name,
|
|
13463
|
+
registry: pkg.registry,
|
|
13464
|
+
verdict: "suspicious_age",
|
|
13465
|
+
confidence: 0.75,
|
|
13466
|
+
details: `Package '${pkg.name}' first published ${Math.floor(ageDays)} days ago (suspicious age)`,
|
|
13467
|
+
ageDays
|
|
13468
|
+
};
|
|
13469
|
+
}
|
|
13470
|
+
return {
|
|
13471
|
+
packageName: pkg.name,
|
|
13472
|
+
registry: pkg.registry,
|
|
13473
|
+
verdict: "clean",
|
|
13474
|
+
confidence: 1,
|
|
13475
|
+
details: `Package '${pkg.name}' verified on ${pkg.registry}`,
|
|
13476
|
+
ageDays
|
|
13477
|
+
};
|
|
13478
|
+
}
|
|
13479
|
+
async runWithConcurrencyLimit(items, fn, limit) {
|
|
13480
|
+
const results = [];
|
|
13481
|
+
const executing = [];
|
|
13482
|
+
for (const item of items) {
|
|
13483
|
+
const p = fn(item).then((r) => {
|
|
13484
|
+
results.push(r);
|
|
13485
|
+
});
|
|
13486
|
+
executing.push(p);
|
|
13487
|
+
if (executing.length >= limit) {
|
|
13488
|
+
await Promise.race(executing);
|
|
13489
|
+
for (let i = executing.length - 1; i >= 0; i--) {
|
|
13490
|
+
const settled = await Promise.race([
|
|
13491
|
+
executing[i]?.then(() => true),
|
|
13492
|
+
Promise.resolve(false)
|
|
13493
|
+
]);
|
|
13494
|
+
if (settled)
|
|
13495
|
+
executing.splice(i, 1);
|
|
13496
|
+
}
|
|
13497
|
+
}
|
|
13498
|
+
}
|
|
13499
|
+
await Promise.allSettled(executing);
|
|
13500
|
+
return results;
|
|
13501
|
+
}
|
|
13502
|
+
};
|
|
13503
|
+
|
|
13504
|
+
// ../core/dist/package-extractor.js
|
|
13505
|
+
function extractPackagesFromCommand(command) {
|
|
13506
|
+
const packages = [];
|
|
13507
|
+
const normalized = command.replace(/^(\s*(sudo|env\s+\S+=\S+)\s+)+/g, "").trim();
|
|
13508
|
+
const commands = normalized.split(/\s*(?:&&|;|\|)\s*/);
|
|
13509
|
+
for (const cmd of commands) {
|
|
13510
|
+
const tokens = shellTokenize(cmd.trim());
|
|
13511
|
+
if (tokens.length === 0)
|
|
13512
|
+
continue;
|
|
13513
|
+
const binary = tokens[0]?.replace(/^.*[/\\]/, "") ?? "";
|
|
13514
|
+
if (["npm", "npx"].includes(binary)) {
|
|
13515
|
+
packages.push(...extractNpmPackages(tokens));
|
|
13516
|
+
} else if (binary === "yarn") {
|
|
13517
|
+
packages.push(...extractYarnPackages(tokens));
|
|
13518
|
+
} else if (["pnpm", "bunx"].includes(binary)) {
|
|
13519
|
+
packages.push(...extractPnpmBunPackages(tokens, binary));
|
|
13520
|
+
} else if (binary === "bun") {
|
|
13521
|
+
packages.push(...extractBunPackages(tokens));
|
|
13522
|
+
} else if (["pip", "pip3"].includes(binary)) {
|
|
13523
|
+
packages.push(...extractPipPackages(tokens));
|
|
13524
|
+
} else if (["python", "python3", "python.exe", "python3.exe"].includes(binary) || binary.match(/python3?\.\d+/) || binary.endsWith("/python") || binary.endsWith("/python3")) {
|
|
13525
|
+
if (tokens[1] === "-m" && ["pip", "pip3"].includes(tokens[2] ?? "")) {
|
|
13526
|
+
packages.push(...extractPipPackages(tokens.slice(2)));
|
|
13527
|
+
}
|
|
13528
|
+
}
|
|
13529
|
+
}
|
|
13530
|
+
return packages;
|
|
13531
|
+
}
|
|
13532
|
+
function extractPackagesFromManifest(filePath, content) {
|
|
13533
|
+
const lowerPath = filePath.toLowerCase().replace(/\\/g, "/");
|
|
13534
|
+
if (lowerPath.endsWith("package.json")) {
|
|
13535
|
+
return extractFromPackageJson(content);
|
|
13536
|
+
}
|
|
13537
|
+
if (lowerPath.endsWith("requirements.txt")) {
|
|
13538
|
+
return extractFromRequirementsTxt(content);
|
|
13539
|
+
}
|
|
13540
|
+
return [];
|
|
13541
|
+
}
|
|
13542
|
+
function shellTokenize(cmd) {
|
|
13543
|
+
const tokens = [];
|
|
13544
|
+
let current = "";
|
|
13545
|
+
let inSingle = false;
|
|
13546
|
+
let inDouble = false;
|
|
13547
|
+
for (const ch of cmd) {
|
|
13548
|
+
if (ch === "'" && !inDouble) {
|
|
13549
|
+
inSingle = !inSingle;
|
|
13550
|
+
} else if (ch === '"' && !inSingle) {
|
|
13551
|
+
inDouble = !inDouble;
|
|
13552
|
+
} else if (/\s/.test(ch) && !inSingle && !inDouble) {
|
|
13553
|
+
if (current)
|
|
13554
|
+
tokens.push(current);
|
|
13555
|
+
current = "";
|
|
13556
|
+
} else {
|
|
13557
|
+
current += ch;
|
|
13558
|
+
}
|
|
13559
|
+
}
|
|
13560
|
+
if (current)
|
|
13561
|
+
tokens.push(current);
|
|
13562
|
+
return tokens;
|
|
13563
|
+
}
|
|
13564
|
+
function isPackageName(token) {
|
|
13565
|
+
if (token.startsWith("-"))
|
|
13566
|
+
return false;
|
|
13567
|
+
if (!/^[a-zA-Z@]/.test(token))
|
|
13568
|
+
return false;
|
|
13569
|
+
if (token.startsWith("https://") || token.startsWith("http://"))
|
|
13570
|
+
return false;
|
|
13571
|
+
if (token.startsWith("git+"))
|
|
13572
|
+
return false;
|
|
13573
|
+
if (token.startsWith("./") || token.startsWith("../") || token.startsWith("/"))
|
|
13574
|
+
return false;
|
|
13575
|
+
if (token.startsWith("file:"))
|
|
13576
|
+
return false;
|
|
13577
|
+
if (token === ".")
|
|
13578
|
+
return false;
|
|
13579
|
+
return true;
|
|
13580
|
+
}
|
|
13581
|
+
function splitNameVersion(token) {
|
|
13582
|
+
if (token.startsWith("@")) {
|
|
13583
|
+
const afterScope = token.indexOf("/");
|
|
13584
|
+
if (afterScope === -1)
|
|
13585
|
+
return { name: token };
|
|
13586
|
+
const rest = token.slice(afterScope + 1);
|
|
13587
|
+
const atIdx2 = rest.indexOf("@");
|
|
13588
|
+
if (atIdx2 > 0) {
|
|
13589
|
+
return {
|
|
13590
|
+
name: `${token.slice(0, afterScope + 1)}${rest.slice(0, atIdx2)}`,
|
|
13591
|
+
version: rest.slice(atIdx2 + 1)
|
|
13592
|
+
};
|
|
13593
|
+
}
|
|
13594
|
+
return { name: token };
|
|
13595
|
+
}
|
|
13596
|
+
const atIdx = token.indexOf("@");
|
|
13597
|
+
if (atIdx > 0) {
|
|
13598
|
+
return { name: token.slice(0, atIdx), version: token.slice(atIdx + 1) };
|
|
13599
|
+
}
|
|
13600
|
+
return { name: token };
|
|
13601
|
+
}
|
|
13602
|
+
var NPM_VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
13603
|
+
"--registry",
|
|
13604
|
+
"--tag",
|
|
13605
|
+
"--workspace",
|
|
13606
|
+
"-w",
|
|
13607
|
+
"--prefix",
|
|
13608
|
+
"--cache",
|
|
13609
|
+
"--userconfig",
|
|
13610
|
+
"--globalconfig"
|
|
13611
|
+
]);
|
|
13612
|
+
function extractNpmPackages(tokens) {
|
|
13613
|
+
const packages = [];
|
|
13614
|
+
const binary = tokens[0]?.replace(/^.*[/\\]/, "") ?? "";
|
|
13615
|
+
if (binary === "npx") {
|
|
13616
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
13617
|
+
const t = tokens[i] ?? "";
|
|
13618
|
+
if (t === "--" || t === "-y" || t === "--yes" || t === "-p" || t === "--package")
|
|
13619
|
+
continue;
|
|
13620
|
+
if (t.startsWith("-"))
|
|
13621
|
+
continue;
|
|
13622
|
+
if (!isPackageName(t))
|
|
13623
|
+
break;
|
|
13624
|
+
const { name, version } = splitNameVersion(t);
|
|
13625
|
+
packages.push({ name, version, registry: "npm", source: "command" });
|
|
13626
|
+
break;
|
|
13627
|
+
}
|
|
13628
|
+
return packages;
|
|
13629
|
+
}
|
|
13630
|
+
const subcommand = tokens[1] ?? "";
|
|
13631
|
+
if (!["install", "i", "add", "ci"].includes(subcommand))
|
|
13632
|
+
return packages;
|
|
13633
|
+
let skipNext = false;
|
|
13634
|
+
for (let i = 2; i < tokens.length; i++) {
|
|
13635
|
+
const t = tokens[i] ?? "";
|
|
13636
|
+
if (skipNext) {
|
|
13637
|
+
skipNext = false;
|
|
13638
|
+
continue;
|
|
13639
|
+
}
|
|
13640
|
+
if (NPM_VALUE_FLAGS.has(t)) {
|
|
13641
|
+
skipNext = true;
|
|
12351
13642
|
continue;
|
|
12352
13643
|
}
|
|
12353
|
-
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12358
|
-
|
|
12359
|
-
|
|
12360
|
-
|
|
12361
|
-
|
|
12362
|
-
|
|
13644
|
+
if (t.startsWith("-"))
|
|
13645
|
+
continue;
|
|
13646
|
+
if (!isPackageName(t))
|
|
13647
|
+
break;
|
|
13648
|
+
const { name, version } = splitNameVersion(t);
|
|
13649
|
+
packages.push({ name, version, registry: "npm", source: "command" });
|
|
13650
|
+
}
|
|
13651
|
+
return packages;
|
|
13652
|
+
}
|
|
13653
|
+
function extractYarnPackages(tokens) {
|
|
13654
|
+
const packages = [];
|
|
13655
|
+
const subcommand = tokens[1] ?? "";
|
|
13656
|
+
if (subcommand !== "add")
|
|
13657
|
+
return packages;
|
|
13658
|
+
for (let i = 2; i < tokens.length; i++) {
|
|
13659
|
+
const t = tokens[i] ?? "";
|
|
13660
|
+
if (t.startsWith("-"))
|
|
13661
|
+
continue;
|
|
13662
|
+
if (!isPackageName(t))
|
|
13663
|
+
break;
|
|
13664
|
+
const { name, version } = splitNameVersion(t);
|
|
13665
|
+
packages.push({ name, version, registry: "npm", source: "command" });
|
|
13666
|
+
}
|
|
13667
|
+
return packages;
|
|
13668
|
+
}
|
|
13669
|
+
function extractPnpmBunPackages(tokens, binary) {
|
|
13670
|
+
const packages = [];
|
|
13671
|
+
if (binary === "bunx") {
|
|
13672
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
13673
|
+
const t = tokens[i] ?? "";
|
|
13674
|
+
if (t.startsWith("-"))
|
|
12363
13675
|
continue;
|
|
12364
|
-
|
|
12365
|
-
|
|
12366
|
-
|
|
13676
|
+
if (!isPackageName(t))
|
|
13677
|
+
break;
|
|
13678
|
+
const { name, version } = splitNameVersion(t);
|
|
13679
|
+
packages.push({ name, version, registry: "npm", source: "command" });
|
|
13680
|
+
break;
|
|
13681
|
+
}
|
|
13682
|
+
return packages;
|
|
13683
|
+
}
|
|
13684
|
+
const subcommand = tokens[1] ?? "";
|
|
13685
|
+
if (subcommand === "dlx") {
|
|
13686
|
+
for (let i = 2; i < tokens.length; i++) {
|
|
13687
|
+
const t = tokens[i] ?? "";
|
|
13688
|
+
if (t.startsWith("-"))
|
|
12367
13689
|
continue;
|
|
12368
|
-
|
|
12369
|
-
|
|
13690
|
+
if (!isPackageName(t))
|
|
13691
|
+
break;
|
|
13692
|
+
const { name, version } = splitNameVersion(t);
|
|
13693
|
+
packages.push({ name, version, registry: "npm", source: "command" });
|
|
13694
|
+
break;
|
|
12370
13695
|
}
|
|
13696
|
+
return packages;
|
|
12371
13697
|
}
|
|
12372
|
-
|
|
12373
|
-
|
|
12374
|
-
|
|
12375
|
-
|
|
12376
|
-
|
|
12377
|
-
|
|
12378
|
-
|
|
12379
|
-
|
|
12380
|
-
|
|
13698
|
+
if (!["add", "install", "i"].includes(subcommand))
|
|
13699
|
+
return packages;
|
|
13700
|
+
for (let i = 2; i < tokens.length; i++) {
|
|
13701
|
+
const t = tokens[i] ?? "";
|
|
13702
|
+
if (t.startsWith("-"))
|
|
13703
|
+
continue;
|
|
13704
|
+
if (!isPackageName(t))
|
|
13705
|
+
break;
|
|
13706
|
+
const { name, version } = splitNameVersion(t);
|
|
13707
|
+
packages.push({ name, version, registry: "npm", source: "command" });
|
|
12381
13708
|
}
|
|
13709
|
+
return packages;
|
|
12382
13710
|
}
|
|
12383
|
-
function
|
|
12384
|
-
const
|
|
12385
|
-
|
|
12386
|
-
|
|
12387
|
-
|
|
12388
|
-
|
|
12389
|
-
|
|
13711
|
+
function extractBunPackages(tokens) {
|
|
13712
|
+
const packages = [];
|
|
13713
|
+
const subcommand = tokens[1] ?? "";
|
|
13714
|
+
if (!["add", "install", "i"].includes(subcommand))
|
|
13715
|
+
return packages;
|
|
13716
|
+
for (let i = 2; i < tokens.length; i++) {
|
|
13717
|
+
const t = tokens[i] ?? "";
|
|
13718
|
+
if (t.startsWith("-"))
|
|
13719
|
+
continue;
|
|
13720
|
+
if (!isPackageName(t))
|
|
13721
|
+
break;
|
|
13722
|
+
const { name, version } = splitNameVersion(t);
|
|
13723
|
+
packages.push({ name, version, registry: "npm", source: "command" });
|
|
12390
13724
|
}
|
|
12391
|
-
return
|
|
13725
|
+
return packages;
|
|
12392
13726
|
}
|
|
12393
|
-
|
|
12394
|
-
|
|
12395
|
-
|
|
12396
|
-
"
|
|
12397
|
-
|
|
12398
|
-
|
|
12399
|
-
|
|
12400
|
-
]
|
|
12401
|
-
|
|
12402
|
-
|
|
12403
|
-
|
|
12404
|
-
|
|
12405
|
-
|
|
12406
|
-
|
|
12407
|
-
|
|
12408
|
-
|
|
12409
|
-
|
|
12410
|
-
|
|
12411
|
-
|
|
12412
|
-
|
|
12413
|
-
|
|
12414
|
-
|
|
12415
|
-
|
|
13727
|
+
function extractPipPackages(tokens) {
|
|
13728
|
+
const packages = [];
|
|
13729
|
+
const subcommand = tokens[1] ?? "";
|
|
13730
|
+
if (subcommand !== "install")
|
|
13731
|
+
return packages;
|
|
13732
|
+
let skipNext = false;
|
|
13733
|
+
for (let i = 2; i < tokens.length; i++) {
|
|
13734
|
+
const t = tokens[i] ?? "";
|
|
13735
|
+
if (skipNext) {
|
|
13736
|
+
skipNext = false;
|
|
13737
|
+
continue;
|
|
13738
|
+
}
|
|
13739
|
+
if ([
|
|
13740
|
+
"-r",
|
|
13741
|
+
"--requirement",
|
|
13742
|
+
"-c",
|
|
13743
|
+
"--constraint",
|
|
13744
|
+
"-e",
|
|
13745
|
+
"--editable",
|
|
13746
|
+
"-t",
|
|
13747
|
+
"--target",
|
|
13748
|
+
"--prefix",
|
|
13749
|
+
"-i",
|
|
13750
|
+
"--index-url",
|
|
13751
|
+
"--extra-index-url",
|
|
13752
|
+
"-f",
|
|
13753
|
+
"--find-links"
|
|
13754
|
+
].includes(t)) {
|
|
13755
|
+
skipNext = true;
|
|
13756
|
+
continue;
|
|
13757
|
+
}
|
|
13758
|
+
if (t.startsWith("-"))
|
|
13759
|
+
continue;
|
|
13760
|
+
if (!isPackageName(t))
|
|
13761
|
+
break;
|
|
13762
|
+
const match = t.match(/^([a-zA-Z0-9._-]+)(?:[><=!~]+(.+))?$/);
|
|
13763
|
+
if (match) {
|
|
13764
|
+
packages.push({
|
|
13765
|
+
name: match[1],
|
|
13766
|
+
version: match[2],
|
|
13767
|
+
registry: "pypi",
|
|
13768
|
+
source: "command"
|
|
13769
|
+
});
|
|
12416
13770
|
}
|
|
12417
13771
|
}
|
|
12418
|
-
|
|
12419
|
-
|
|
12420
|
-
|
|
12421
|
-
|
|
12422
|
-
|
|
12423
|
-
const
|
|
12424
|
-
|
|
12425
|
-
|
|
12426
|
-
for (const
|
|
12427
|
-
|
|
12428
|
-
|
|
12429
|
-
|
|
12430
|
-
|
|
12431
|
-
|
|
13772
|
+
return packages;
|
|
13773
|
+
}
|
|
13774
|
+
function extractFromPackageJson(content) {
|
|
13775
|
+
const packages = [];
|
|
13776
|
+
try {
|
|
13777
|
+
const data = JSON.parse(content);
|
|
13778
|
+
const deps = data.dependencies ?? {};
|
|
13779
|
+
const devDeps = data.devDependencies ?? {};
|
|
13780
|
+
for (const [name, version] of Object.entries({ ...deps, ...devDeps })) {
|
|
13781
|
+
if (typeof version !== "string")
|
|
13782
|
+
continue;
|
|
13783
|
+
if (version.startsWith("file:") || version.startsWith("link:"))
|
|
13784
|
+
continue;
|
|
13785
|
+
if (version.startsWith("git+") || version.startsWith("http"))
|
|
13786
|
+
continue;
|
|
13787
|
+
packages.push({
|
|
13788
|
+
name,
|
|
13789
|
+
version: version.replace(/^[\^~>=<]+/, ""),
|
|
13790
|
+
registry: "npm",
|
|
13791
|
+
source: "package.json"
|
|
13792
|
+
});
|
|
12432
13793
|
}
|
|
12433
|
-
|
|
13794
|
+
} catch {
|
|
12434
13795
|
}
|
|
12435
|
-
|
|
12436
|
-
|
|
12437
|
-
|
|
12438
|
-
|
|
12439
|
-
|
|
12440
|
-
|
|
12441
|
-
|
|
12442
|
-
|
|
12443
|
-
|
|
12444
|
-
artifact: artifact.value,
|
|
12445
|
-
matchValue: m[0]
|
|
12446
|
-
});
|
|
12447
|
-
}
|
|
12448
|
-
}
|
|
13796
|
+
return packages;
|
|
13797
|
+
}
|
|
13798
|
+
function extractFromRequirementsTxt(content) {
|
|
13799
|
+
const packages = [];
|
|
13800
|
+
const lines = content.split("\n");
|
|
13801
|
+
for (const rawLine of lines) {
|
|
13802
|
+
const line = rawLine.trim();
|
|
13803
|
+
if (!line || line.startsWith("#") || line.startsWith("-r") || line.startsWith("--requirement") || line.startsWith("-e") || line.startsWith("--editable") || line.startsWith("-i") || line.startsWith("--index-url") || line.startsWith("--extra-index-url") || line.startsWith("-f") || line.startsWith("--find-links") || line.startsWith("-c") || line.startsWith("--constraint")) {
|
|
13804
|
+
continue;
|
|
12449
13805
|
}
|
|
12450
|
-
if (
|
|
12451
|
-
|
|
13806
|
+
if (line.startsWith("git+") || line.startsWith("http://") || line.startsWith("https://")) {
|
|
13807
|
+
continue;
|
|
13808
|
+
}
|
|
13809
|
+
const cleaned = line.split("#")[0]?.split(";")[0]?.trim() ?? "";
|
|
13810
|
+
if (!cleaned)
|
|
13811
|
+
continue;
|
|
13812
|
+
const match = cleaned.match(/^([a-zA-Z0-9._-]+)(?:\[.*\])?(?:[><=!~]+(.+))?$/);
|
|
13813
|
+
if (match) {
|
|
13814
|
+
packages.push({
|
|
13815
|
+
name: match[1],
|
|
13816
|
+
version: match[2]?.trim(),
|
|
13817
|
+
registry: "pypi",
|
|
13818
|
+
source: "requirements.txt"
|
|
13819
|
+
});
|
|
12452
13820
|
}
|
|
12453
|
-
return matches;
|
|
12454
13821
|
}
|
|
12455
|
-
|
|
13822
|
+
return packages;
|
|
13823
|
+
}
|
|
12456
13824
|
|
|
12457
13825
|
// ../core/dist/threat-loader.js
|
|
12458
13826
|
var import_promises3 = require("node:fs/promises");
|
|
@@ -12560,6 +13928,249 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
12560
13928
|
return threats;
|
|
12561
13929
|
}
|
|
12562
13930
|
|
|
13931
|
+
// ../core/dist/evaluator.js
|
|
13932
|
+
function allowVerdict(source = "none") {
|
|
13933
|
+
return {
|
|
13934
|
+
decision: "allow",
|
|
13935
|
+
category: "none",
|
|
13936
|
+
confidence: 1,
|
|
13937
|
+
severity: "info",
|
|
13938
|
+
source,
|
|
13939
|
+
artifacts: [],
|
|
13940
|
+
matchedThreatId: null,
|
|
13941
|
+
reasons: []
|
|
13942
|
+
};
|
|
13943
|
+
}
|
|
13944
|
+
async function evaluateToolCall(request, context) {
|
|
13945
|
+
const logger = context.logger ?? nullLogger;
|
|
13946
|
+
const config = await loadConfig(context.configPath, logger).catch(() => ConfigSchema.parse({}));
|
|
13947
|
+
if (request.artifacts.length === 0) {
|
|
13948
|
+
return allowVerdict("no_artifacts");
|
|
13949
|
+
}
|
|
13950
|
+
try {
|
|
13951
|
+
const allowlist = await loadAllowlist(config.allowlist, logger);
|
|
13952
|
+
if (isAllowlisted(allowlist, request.artifacts)) {
|
|
13953
|
+
const verdict2 = allowVerdict("allowlisted");
|
|
13954
|
+
await logVerdict(config.logging, request.sessionId, request.toolName, request.toolInput, verdict2, true);
|
|
13955
|
+
return verdict2;
|
|
13956
|
+
}
|
|
13957
|
+
} catch {
|
|
13958
|
+
}
|
|
13959
|
+
let cache = null;
|
|
13960
|
+
try {
|
|
13961
|
+
cache = new VerdictCache(config.cache, logger);
|
|
13962
|
+
await cache.load();
|
|
13963
|
+
} catch {
|
|
13964
|
+
cache = null;
|
|
13965
|
+
}
|
|
13966
|
+
const urls = request.artifacts.filter((artifact) => artifact.type === "url").map((artifact) => artifact.value);
|
|
13967
|
+
const { cachedUrlVerdicts, uncachedUrls } = partitionUrlsByCache(urls, cache);
|
|
13968
|
+
let heuristicMatches = [];
|
|
13969
|
+
if (config.heuristics_enabled) {
|
|
13970
|
+
let threats = await loadThreats(context.threatsDir, logger);
|
|
13971
|
+
if (config.disabled_threats.length > 0) {
|
|
13972
|
+
const disabledSet = new Set(config.disabled_threats);
|
|
13973
|
+
threats = threats.filter((t) => !disabledSet.has(t.id));
|
|
13974
|
+
}
|
|
13975
|
+
const trustedDomains = await loadTrustedDomains(context.allowlistsDir, logger);
|
|
13976
|
+
const heuristics = new HeuristicsEngine(threats, trustedDomains);
|
|
13977
|
+
heuristicMatches = heuristics.match(request.artifacts);
|
|
13978
|
+
}
|
|
13979
|
+
let urlCheckResults = [];
|
|
13980
|
+
const urlsToCheck = cache ? uncachedUrls : urls;
|
|
13981
|
+
if (urlsToCheck.length > 0 && config.url_check.enabled) {
|
|
13982
|
+
try {
|
|
13983
|
+
const client = new UrlCheckClient(config.url_check, logger);
|
|
13984
|
+
urlCheckResults = await client.checkUrls(urlsToCheck);
|
|
13985
|
+
} catch {
|
|
13986
|
+
}
|
|
13987
|
+
}
|
|
13988
|
+
const packageCheckResults = await checkPackages(request, config, cache, logger);
|
|
13989
|
+
const amsiCheckResults = [];
|
|
13990
|
+
if (config.amsi_check.enabled && isAmsiSupported()) {
|
|
13991
|
+
let amsiClient = null;
|
|
13992
|
+
try {
|
|
13993
|
+
amsiClient = new AmsiClient(logger);
|
|
13994
|
+
await amsiClient.init();
|
|
13995
|
+
if (amsiClient.isAvailable) {
|
|
13996
|
+
const scans = [];
|
|
13997
|
+
if (request.toolName === "Bash") {
|
|
13998
|
+
const command = request.toolInput.command ?? "";
|
|
13999
|
+
if (command) {
|
|
14000
|
+
scans.push({ content: command, name: `Bash:command` });
|
|
14001
|
+
}
|
|
14002
|
+
} else if (request.toolName === "Write") {
|
|
14003
|
+
const filePath = request.toolInput.file_path ?? "";
|
|
14004
|
+
const content = request.toolInput.content ?? "";
|
|
14005
|
+
if (content) {
|
|
14006
|
+
scans.push({ content, name: `Write:${filePath}` });
|
|
14007
|
+
}
|
|
14008
|
+
} else if (request.toolName === "Edit") {
|
|
14009
|
+
const filePath = request.toolInput.file_path ?? "";
|
|
14010
|
+
const newString = request.toolInput.new_string ?? "";
|
|
14011
|
+
if (newString) {
|
|
14012
|
+
scans.push({ content: newString, name: `Edit:${filePath}` });
|
|
14013
|
+
}
|
|
14014
|
+
}
|
|
14015
|
+
for (const scan of scans) {
|
|
14016
|
+
const result = await amsiClient.scanString(scan.content, scan.name);
|
|
14017
|
+
if (result) {
|
|
14018
|
+
amsiCheckResults.push(result);
|
|
14019
|
+
}
|
|
14020
|
+
}
|
|
14021
|
+
}
|
|
14022
|
+
} catch {
|
|
14023
|
+
} finally {
|
|
14024
|
+
amsiClient?.close();
|
|
14025
|
+
}
|
|
14026
|
+
}
|
|
14027
|
+
const engine = new DecisionEngine(config.sensitivity);
|
|
14028
|
+
let verdict = await engine.decide({
|
|
14029
|
+
heuristicMatches,
|
|
14030
|
+
urlCheckResults,
|
|
14031
|
+
packageCheckResults: packageCheckResults.length > 0 ? packageCheckResults : void 0,
|
|
14032
|
+
amsiCheckResults: amsiCheckResults.length > 0 ? amsiCheckResults : void 0
|
|
14033
|
+
});
|
|
14034
|
+
if (cachedUrlVerdicts.size > 0 && verdict.decision === "allow") {
|
|
14035
|
+
for (const [url, cachedVerdict] of cachedUrlVerdicts) {
|
|
14036
|
+
if (cachedVerdict.verdict === "allow") {
|
|
14037
|
+
continue;
|
|
14038
|
+
}
|
|
14039
|
+
verdict = {
|
|
14040
|
+
decision: cachedVerdict.verdict,
|
|
14041
|
+
category: "network_egress",
|
|
14042
|
+
confidence: 1,
|
|
14043
|
+
severity: cachedVerdict.severity,
|
|
14044
|
+
source: `cache(${cachedVerdict.source})`,
|
|
14045
|
+
artifacts: [url],
|
|
14046
|
+
matchedThreatId: null,
|
|
14047
|
+
reasons: cachedVerdict.reasons
|
|
14048
|
+
};
|
|
14049
|
+
break;
|
|
14050
|
+
}
|
|
14051
|
+
}
|
|
14052
|
+
await cacheUrlResults(urlCheckResults, cache);
|
|
14053
|
+
try {
|
|
14054
|
+
await logVerdict(config.logging, request.sessionId, request.toolName, request.toolInput, verdict);
|
|
14055
|
+
} catch {
|
|
14056
|
+
}
|
|
14057
|
+
return verdict;
|
|
14058
|
+
}
|
|
14059
|
+
function partitionUrlsByCache(urls, cache) {
|
|
14060
|
+
const cachedUrlVerdicts = /* @__PURE__ */ new Map();
|
|
14061
|
+
let uncachedUrls = [];
|
|
14062
|
+
if (cache && urls.length > 0) {
|
|
14063
|
+
try {
|
|
14064
|
+
for (const url of urls) {
|
|
14065
|
+
const cached = cache.getUrl(url);
|
|
14066
|
+
if (cached !== null) {
|
|
14067
|
+
cachedUrlVerdicts.set(url, cached);
|
|
14068
|
+
} else {
|
|
14069
|
+
uncachedUrls.push(url);
|
|
14070
|
+
}
|
|
14071
|
+
}
|
|
14072
|
+
} catch {
|
|
14073
|
+
uncachedUrls = urls;
|
|
14074
|
+
}
|
|
14075
|
+
}
|
|
14076
|
+
return { cachedUrlVerdicts, uncachedUrls };
|
|
14077
|
+
}
|
|
14078
|
+
async function cacheUrlResults(urlCheckResults, cache) {
|
|
14079
|
+
if (!cache)
|
|
14080
|
+
return;
|
|
14081
|
+
try {
|
|
14082
|
+
for (const result of urlCheckResults) {
|
|
14083
|
+
let cachedVerdict;
|
|
14084
|
+
if (result.isMalicious) {
|
|
14085
|
+
cachedVerdict = {
|
|
14086
|
+
verdict: "deny",
|
|
14087
|
+
severity: "critical",
|
|
14088
|
+
reasons: [
|
|
14089
|
+
`URL check: malicious (${result.findings.map((finding) => `${finding.severityName}/${finding.typeName}`).join(", ")})`
|
|
14090
|
+
],
|
|
14091
|
+
source: "url_check"
|
|
14092
|
+
};
|
|
14093
|
+
} else if (result.flags.length > 0) {
|
|
14094
|
+
cachedVerdict = {
|
|
14095
|
+
verdict: "ask",
|
|
14096
|
+
severity: "warning",
|
|
14097
|
+
reasons: [`URL check: suspicious (${result.flags.join(", ")})`],
|
|
14098
|
+
source: "url_check"
|
|
14099
|
+
};
|
|
14100
|
+
} else {
|
|
14101
|
+
cachedVerdict = {
|
|
14102
|
+
verdict: "allow",
|
|
14103
|
+
severity: "info",
|
|
14104
|
+
reasons: [],
|
|
14105
|
+
source: "url_check"
|
|
14106
|
+
};
|
|
14107
|
+
}
|
|
14108
|
+
cache.putUrl(result.url, cachedVerdict, result.isMalicious);
|
|
14109
|
+
}
|
|
14110
|
+
await cache.save();
|
|
14111
|
+
} catch {
|
|
14112
|
+
}
|
|
14113
|
+
}
|
|
14114
|
+
async function checkPackages(request, config, cache, logger) {
|
|
14115
|
+
const results = [];
|
|
14116
|
+
if (!config.package_check.enabled)
|
|
14117
|
+
return results;
|
|
14118
|
+
try {
|
|
14119
|
+
let parsedPackages;
|
|
14120
|
+
if (request.toolName === "Bash") {
|
|
14121
|
+
const command = request.toolInput.command ?? "";
|
|
14122
|
+
parsedPackages = extractPackagesFromCommand(command);
|
|
14123
|
+
} else if (request.toolName === "Write" || request.toolName === "Edit") {
|
|
14124
|
+
const filePath = request.toolInput.file_path ?? "";
|
|
14125
|
+
const content = request.toolInput.content ?? request.toolInput.new_string ?? "";
|
|
14126
|
+
parsedPackages = extractPackagesFromManifest(filePath, content);
|
|
14127
|
+
}
|
|
14128
|
+
if (!parsedPackages || parsedPackages.length === 0)
|
|
14129
|
+
return results;
|
|
14130
|
+
const uncached = [];
|
|
14131
|
+
for (const pkg of parsedPackages) {
|
|
14132
|
+
const cacheKey2 = `${pkg.registry}:${pkg.name}${pkg.version ? `@${pkg.version}` : ""}`;
|
|
14133
|
+
const cached = cache?.getPackage(cacheKey2);
|
|
14134
|
+
if (cached && cached.verdict !== "allow") {
|
|
14135
|
+
results.push({
|
|
14136
|
+
packageName: pkg.name,
|
|
14137
|
+
registry: pkg.registry,
|
|
14138
|
+
verdict: cached.verdict === "deny" ? "malicious" : "suspicious_age",
|
|
14139
|
+
confidence: 1,
|
|
14140
|
+
details: cached.reasons.join("; ")
|
|
14141
|
+
});
|
|
14142
|
+
} else if (!cached) {
|
|
14143
|
+
uncached.push(pkg);
|
|
14144
|
+
}
|
|
14145
|
+
}
|
|
14146
|
+
if (uncached.length > 0) {
|
|
14147
|
+
const checker = new PackageChecker({
|
|
14148
|
+
registryTimeoutSeconds: config.package_check.timeout_seconds,
|
|
14149
|
+
fileCheckEndpoint: config.file_check.endpoint,
|
|
14150
|
+
fileCheckTimeoutSeconds: config.file_check.timeout_seconds,
|
|
14151
|
+
fileCheckEnabled: config.file_check.enabled
|
|
14152
|
+
}, logger);
|
|
14153
|
+
const checked = await checker.checkPackages(uncached);
|
|
14154
|
+
results.push(...checked);
|
|
14155
|
+
if (cache) {
|
|
14156
|
+
for (const result of checked) {
|
|
14157
|
+
const pkg = uncached.find((p) => p.name === result.packageName);
|
|
14158
|
+
const cacheKey2 = `${result.registry}:${result.packageName}${pkg?.version ? `@${pkg.version}` : ""}`;
|
|
14159
|
+
const isCritical = result.verdict === "malicious" || result.verdict === "not_found";
|
|
14160
|
+
cache.putPackage(cacheKey2, {
|
|
14161
|
+
verdict: result.verdict === "clean" ? "allow" : isCritical ? "deny" : "ask",
|
|
14162
|
+
severity: isCritical ? "critical" : "warning",
|
|
14163
|
+
reasons: [result.details],
|
|
14164
|
+
source: "package_check"
|
|
14165
|
+
}, result.ageDays ?? null);
|
|
14166
|
+
}
|
|
14167
|
+
}
|
|
14168
|
+
}
|
|
14169
|
+
} catch {
|
|
14170
|
+
}
|
|
14171
|
+
return results;
|
|
14172
|
+
}
|
|
14173
|
+
|
|
12563
14174
|
// ../core/dist/format.js
|
|
12564
14175
|
var PAD = 12;
|
|
12565
14176
|
var SEPARATOR_WIDTH = 48;
|
|
@@ -12580,6 +14191,14 @@ function separatorLine(headerLength) {
|
|
|
12580
14191
|
function formatUpdateNotice(result) {
|
|
12581
14192
|
return `\u2B06\uFE0F Update available: v${result.currentVersion} \u2192 v${result.latestVersion} (https://github.com/avast/sage)`;
|
|
12582
14193
|
}
|
|
14194
|
+
function formatStartupClean(version, versionCheck) {
|
|
14195
|
+
const base = `\u{1F6E1}\uFE0F Sage v${version} by Gen Digital \u2705 No threats found`;
|
|
14196
|
+
if (versionCheck?.updateAvailable) {
|
|
14197
|
+
return `${base}
|
|
14198
|
+
${formatUpdateNotice(versionCheck)}`;
|
|
14199
|
+
}
|
|
14200
|
+
return base;
|
|
14201
|
+
}
|
|
12583
14202
|
function formatThreatBanner(version, results, versionCheck) {
|
|
12584
14203
|
const header = `\u{1F6E1}\uFE0F Sage v${version} by Gen Digital \u2014 Threat Detected`;
|
|
12585
14204
|
const lines = [" ", header, separatorLine(SEPARATOR_WIDTH)];
|
|
@@ -12616,31 +14235,203 @@ function formatThreatBanner(version, results, versionCheck) {
|
|
|
12616
14235
|
}
|
|
12617
14236
|
return lines.join("\n");
|
|
12618
14237
|
}
|
|
14238
|
+
function formatSessionStartMessage(version, result) {
|
|
14239
|
+
if (result.scanResults.length > 0) {
|
|
14240
|
+
return formatThreatBanner(version, result.scanResults, result.versionCheck);
|
|
14241
|
+
}
|
|
14242
|
+
return formatStartupClean(version, result.versionCheck);
|
|
14243
|
+
}
|
|
12619
14244
|
|
|
12620
|
-
// ../core/dist/
|
|
12621
|
-
|
|
14245
|
+
// ../core/dist/guard.js
|
|
14246
|
+
async function guardToolCall(request, context, approvalStore) {
|
|
14247
|
+
const actionId = ApprovalStore.actionId(request.toolName, request.toolInput, request.sessionId);
|
|
14248
|
+
if (approvalStore.isApproved(actionId)) {
|
|
14249
|
+
return { verdict: allowVerdict("approved") };
|
|
14250
|
+
}
|
|
14251
|
+
const verdict = await evaluateToolCall(request, context);
|
|
14252
|
+
if (verdict.decision === "allow" || verdict.decision === "deny") {
|
|
14253
|
+
return { verdict };
|
|
14254
|
+
}
|
|
14255
|
+
const logger = context.logger ?? nullLogger;
|
|
14256
|
+
const config = await loadConfig(context.configPath, logger);
|
|
14257
|
+
if (config.sensitivity === "paranoid") {
|
|
14258
|
+
return { verdict: { ...verdict, decision: "deny" } };
|
|
14259
|
+
}
|
|
14260
|
+
approvalStore.setPending(actionId, {
|
|
14261
|
+
artifacts: request.artifacts,
|
|
14262
|
+
createdAt: Date.now()
|
|
14263
|
+
});
|
|
14264
|
+
return { verdict, actionId };
|
|
14265
|
+
}
|
|
14266
|
+
var MAX_ARTIFACT_VALUE_LEN = 120;
|
|
14267
|
+
function truncateValue(value) {
|
|
14268
|
+
const singleLine = value.replaceAll("\n", "\\n");
|
|
14269
|
+
if (singleLine.length <= MAX_ARTIFACT_VALUE_LEN)
|
|
14270
|
+
return singleLine;
|
|
14271
|
+
return `${singleLine.slice(0, MAX_ARTIFACT_VALUE_LEN)}\u2026`;
|
|
14272
|
+
}
|
|
14273
|
+
function summarizeArtifacts(artifacts) {
|
|
14274
|
+
if (artifacts.length === 0)
|
|
14275
|
+
return "none";
|
|
14276
|
+
return artifacts.slice(0, 3).map((artifact) => `${artifact.type} '${truncateValue(artifact.value)}'`).join(", ");
|
|
14277
|
+
}
|
|
14278
|
+
function formatDenyMessage(verdict) {
|
|
14279
|
+
const reasons = verdict.reasons.length > 0 ? verdict.reasons.slice(0, 5).join("; ") : verdict.category;
|
|
14280
|
+
return [
|
|
14281
|
+
"Sage blocked this action.",
|
|
14282
|
+
`Severity: ${verdict.severity}`,
|
|
14283
|
+
`Category: ${verdict.category}`,
|
|
14284
|
+
`Reason: ${reasons}`
|
|
14285
|
+
].join("\n");
|
|
14286
|
+
}
|
|
14287
|
+
function formatAskMessage(actionId, verdict, artifacts) {
|
|
14288
|
+
const reasons = verdict.reasons.length > 0 ? verdict.reasons.slice(0, 3).join("; ") : verdict.category;
|
|
14289
|
+
return [
|
|
14290
|
+
"Sage flagged this action and requires explicit user approval.",
|
|
14291
|
+
`Severity: ${verdict.severity}`,
|
|
14292
|
+
`Category: ${verdict.category}`,
|
|
14293
|
+
`Reason: ${reasons}`,
|
|
14294
|
+
`Artifacts: ${summarizeArtifacts(artifacts)}`,
|
|
14295
|
+
"",
|
|
14296
|
+
"Ask the user for confirmation to proceed, DO NOT auto-approve.",
|
|
14297
|
+
`if the user confirms, call:`,
|
|
14298
|
+
` sage_approve({ actionId: "${actionId}", approved: true })`,
|
|
14299
|
+
"If the user declines, call:",
|
|
14300
|
+
` sage_approve({ actionId: "${actionId}", approved: false })`
|
|
14301
|
+
].join("\n");
|
|
14302
|
+
}
|
|
14303
|
+
async function approveAction(store, actionId) {
|
|
14304
|
+
const entry = store.approve(actionId);
|
|
14305
|
+
if (!entry) {
|
|
14306
|
+
return "No pending Sage approval found for this action ID.";
|
|
14307
|
+
}
|
|
14308
|
+
return [
|
|
14309
|
+
`Approved action ${actionId}. Retry the original tool call now.`,
|
|
14310
|
+
"",
|
|
14311
|
+
"Inform the user that they can ask you to permanently allowlist this artifact. Do NOT add it to the allowlist unless the user explicitly requests it."
|
|
14312
|
+
].join("\n");
|
|
14313
|
+
}
|
|
14314
|
+
async function addToAllowlist(store, type, value, reason, configPath, logger = nullLogger) {
|
|
14315
|
+
if (!store.hasApprovedArtifact(type, value)) {
|
|
14316
|
+
return "Cannot add to allowlist: no recent user approval found for this artifact.";
|
|
14317
|
+
}
|
|
14318
|
+
const config = await loadConfig(configPath, logger);
|
|
14319
|
+
const allowlist = await loadAllowlist(config.allowlist, logger);
|
|
14320
|
+
const entryReason = reason ?? "Approved by user via sage_approve";
|
|
14321
|
+
if (type === "url") {
|
|
14322
|
+
addUrl(allowlist, value, entryReason, "ask");
|
|
14323
|
+
} else if (type === "command") {
|
|
14324
|
+
addCommand(allowlist, value, entryReason, "ask");
|
|
14325
|
+
} else {
|
|
14326
|
+
addFilePath(allowlist, value, entryReason, "ask");
|
|
14327
|
+
}
|
|
14328
|
+
const saved = await saveAllowlist(allowlist, config.allowlist, logger);
|
|
14329
|
+
if (!saved) {
|
|
14330
|
+
return "Failed to save allowlist. Approval preserved \u2014 retry is safe.";
|
|
14331
|
+
}
|
|
14332
|
+
store.consumeApprovedArtifact(type, value);
|
|
14333
|
+
return `Added ${type} to Sage allowlist.`;
|
|
14334
|
+
}
|
|
14335
|
+
async function removeFromAllowlist(type, value, configPath, logger = nullLogger) {
|
|
14336
|
+
const config = await loadConfig(configPath, logger);
|
|
14337
|
+
const allowlist = await loadAllowlist(config.allowlist, logger);
|
|
14338
|
+
let removed;
|
|
14339
|
+
let removedViaHash = false;
|
|
14340
|
+
switch (type) {
|
|
14341
|
+
case "url":
|
|
14342
|
+
removed = removeUrl(allowlist, value);
|
|
14343
|
+
break;
|
|
14344
|
+
case "command":
|
|
14345
|
+
removed = removeCommand(allowlist, value);
|
|
14346
|
+
if (!removed) {
|
|
14347
|
+
removed = removeCommand(allowlist, hashCommand(value));
|
|
14348
|
+
removedViaHash = true;
|
|
14349
|
+
}
|
|
14350
|
+
break;
|
|
14351
|
+
case "file_path":
|
|
14352
|
+
removed = removeFilePath(allowlist, value);
|
|
14353
|
+
break;
|
|
14354
|
+
}
|
|
14355
|
+
if (!removed) {
|
|
14356
|
+
return "Artifact not found in Sage allowlist.";
|
|
14357
|
+
}
|
|
14358
|
+
const saved = await saveAllowlist(allowlist, config.allowlist, logger);
|
|
14359
|
+
if (!saved) {
|
|
14360
|
+
return "Failed to save allowlist after removal. Please retry.";
|
|
14361
|
+
}
|
|
14362
|
+
let rendered;
|
|
14363
|
+
switch (type) {
|
|
14364
|
+
case "url":
|
|
14365
|
+
rendered = normalizeUrl(value);
|
|
14366
|
+
break;
|
|
14367
|
+
case "command":
|
|
14368
|
+
rendered = `${(removedViaHash ? hashCommand(value) : value).slice(0, 12)}...`;
|
|
14369
|
+
break;
|
|
14370
|
+
case "file_path":
|
|
14371
|
+
rendered = normalizeFilePath(value);
|
|
14372
|
+
break;
|
|
14373
|
+
}
|
|
14374
|
+
return `Removed ${type} from Sage allowlist: ${rendered}`;
|
|
14375
|
+
}
|
|
14376
|
+
|
|
14377
|
+
// ../core/dist/installation-id.js
|
|
14378
|
+
var import_node_crypto4 = require("node:crypto");
|
|
12622
14379
|
var import_promises4 = require("node:fs/promises");
|
|
12623
|
-
var import_node_os3 = require("node:os");
|
|
12624
14380
|
var import_node_path7 = require("node:path");
|
|
12625
|
-
|
|
14381
|
+
async function getInstallationId(sageDirPath) {
|
|
14382
|
+
const sageDir = sageDirPath ?? resolvePath("~/.sage");
|
|
14383
|
+
const idPath = (0, import_node_path7.join)(sageDir, "installation-id");
|
|
14384
|
+
let fileExists = false;
|
|
14385
|
+
try {
|
|
14386
|
+
const existing = await (0, import_promises4.readFile)(idPath, "utf-8");
|
|
14387
|
+
const trimmed = existing.trim();
|
|
14388
|
+
if (trimmed.length > 0)
|
|
14389
|
+
return trimmed;
|
|
14390
|
+
fileExists = true;
|
|
14391
|
+
} catch {
|
|
14392
|
+
}
|
|
14393
|
+
try {
|
|
14394
|
+
const id = (0, import_node_crypto4.randomUUID)();
|
|
14395
|
+
await (0, import_promises4.mkdir)(sageDir, { recursive: true, mode: 448 });
|
|
14396
|
+
await (0, import_promises4.writeFile)(idPath, id, { encoding: "utf-8", mode: 384, flag: fileExists ? "w" : "wx" });
|
|
14397
|
+
return id;
|
|
14398
|
+
} catch (err) {
|
|
14399
|
+
if (err.code === "EEXIST") {
|
|
14400
|
+
try {
|
|
14401
|
+
const existing = await (0, import_promises4.readFile)(idPath, "utf-8");
|
|
14402
|
+
return existing.trim() || void 0;
|
|
14403
|
+
} catch {
|
|
14404
|
+
return void 0;
|
|
14405
|
+
}
|
|
14406
|
+
}
|
|
14407
|
+
return void 0;
|
|
14408
|
+
}
|
|
14409
|
+
}
|
|
14410
|
+
|
|
14411
|
+
// ../core/dist/plugin-scan-cache.js
|
|
14412
|
+
var import_node_crypto5 = require("node:crypto");
|
|
14413
|
+
var import_promises5 = require("node:fs/promises");
|
|
14414
|
+
var import_node_os3 = require("node:os");
|
|
14415
|
+
var import_node_path8 = require("node:path");
|
|
14416
|
+
var DEFAULT_CACHE_PATH = (0, import_node_path8.join)((0, import_node_os3.homedir)(), ".sage", "plugin_scan_cache.json");
|
|
12626
14417
|
var CACHE_TTL_DAYS = 7;
|
|
12627
14418
|
function cacheKey(pluginKey, version, lastUpdated) {
|
|
12628
14419
|
return `${pluginKey}:${version}:${lastUpdated}`;
|
|
12629
14420
|
}
|
|
12630
14421
|
async function computeConfigHash(sageVersion, ...dirs) {
|
|
12631
|
-
const h = (0,
|
|
14422
|
+
const h = (0, import_node_crypto5.createHash)("sha256");
|
|
12632
14423
|
if (sageVersion)
|
|
12633
14424
|
h.update(sageVersion);
|
|
12634
14425
|
for (const dir of dirs) {
|
|
12635
14426
|
let files;
|
|
12636
14427
|
try {
|
|
12637
|
-
files = (await (0,
|
|
14428
|
+
files = (await (0, import_promises5.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
|
|
12638
14429
|
} catch {
|
|
12639
14430
|
continue;
|
|
12640
14431
|
}
|
|
12641
14432
|
for (const file of files) {
|
|
12642
14433
|
try {
|
|
12643
|
-
const content = await getFileContent((0,
|
|
14434
|
+
const content = await getFileContent((0, import_node_path8.join)(dir, file));
|
|
12644
14435
|
h.update(content);
|
|
12645
14436
|
} catch {
|
|
12646
14437
|
}
|
|
@@ -12683,7 +14474,6 @@ async function loadScanCache(configHash = "", cachePath = DEFAULT_CACHE_PATH, lo
|
|
|
12683
14474
|
}
|
|
12684
14475
|
async function saveScanCache(cache, cachePath = DEFAULT_CACHE_PATH, logger = nullLogger) {
|
|
12685
14476
|
try {
|
|
12686
|
-
await (0, import_promises4.mkdir)((0, import_node_path7.dirname)(cachePath), { recursive: true });
|
|
12687
14477
|
const data = {
|
|
12688
14478
|
config_hash: cache.configHash,
|
|
12689
14479
|
entries: Object.fromEntries(Object.entries(cache.entries).map(([key, entry]) => [
|
|
@@ -12696,7 +14486,7 @@ async function saveScanCache(cache, cachePath = DEFAULT_CACHE_PATH, logger = nul
|
|
|
12696
14486
|
}
|
|
12697
14487
|
]))
|
|
12698
14488
|
};
|
|
12699
|
-
await (
|
|
14489
|
+
await atomicWriteJson(cachePath, data);
|
|
12700
14490
|
} catch (e) {
|
|
12701
14491
|
logger.warn(`Failed to save scan cache to ${cachePath}`, { error: String(e) });
|
|
12702
14492
|
}
|
|
@@ -12731,11 +14521,11 @@ function storeResult(cache, pluginKey, version, lastUpdated, findings) {
|
|
|
12731
14521
|
}
|
|
12732
14522
|
|
|
12733
14523
|
// ../core/dist/plugin-scanner.js
|
|
12734
|
-
var
|
|
12735
|
-
var
|
|
14524
|
+
var import_node_crypto6 = require("node:crypto");
|
|
14525
|
+
var import_promises6 = require("node:fs/promises");
|
|
12736
14526
|
var import_node_os4 = require("node:os");
|
|
12737
|
-
var
|
|
12738
|
-
var DEFAULT_PLUGINS_REGISTRY = (0,
|
|
14527
|
+
var import_node_path9 = require("node:path");
|
|
14528
|
+
var DEFAULT_PLUGINS_REGISTRY = (0, import_node_path9.join)((0, import_node_os4.homedir)(), ".claude", "plugins", "installed_plugins.json");
|
|
12739
14529
|
var SCANNABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
12740
14530
|
".py",
|
|
12741
14531
|
".js",
|
|
@@ -12757,38 +14547,38 @@ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"])
|
|
|
12757
14547
|
var MAX_FILE_SIZE = 512 * 1024;
|
|
12758
14548
|
async function walkPluginFiles(installPath, logger) {
|
|
12759
14549
|
const files = [];
|
|
12760
|
-
async function walk(
|
|
12761
|
-
let
|
|
14550
|
+
async function walk(dirOrFile) {
|
|
14551
|
+
let stats;
|
|
12762
14552
|
try {
|
|
12763
|
-
|
|
14553
|
+
stats = await (0, import_promises6.stat)(dirOrFile);
|
|
12764
14554
|
} catch {
|
|
12765
14555
|
return;
|
|
12766
14556
|
}
|
|
12767
|
-
|
|
12768
|
-
if (
|
|
12769
|
-
|
|
12770
|
-
|
|
12771
|
-
|
|
14557
|
+
if (stats.isFile()) {
|
|
14558
|
+
if (SCANNABLE_EXTENSIONS.has((0, import_node_path9.extname)(dirOrFile).toLowerCase()) && stats.size <= MAX_FILE_SIZE) {
|
|
14559
|
+
files.push(dirOrFile);
|
|
14560
|
+
}
|
|
14561
|
+
return;
|
|
14562
|
+
}
|
|
14563
|
+
if (stats.isDirectory()) {
|
|
14564
|
+
let entries;
|
|
12772
14565
|
try {
|
|
12773
|
-
|
|
14566
|
+
entries = await (0, import_promises6.readdir)(dirOrFile);
|
|
12774
14567
|
} catch {
|
|
12775
|
-
|
|
14568
|
+
return;
|
|
12776
14569
|
}
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
} else if (stats.isFile()) {
|
|
12780
|
-
if (!SCANNABLE_EXTENSIONS.has((0, import_node_path8.extname)(fullPath).toLowerCase()))
|
|
14570
|
+
for (const entry of entries) {
|
|
14571
|
+
if (SKIP_DIRS.has(entry))
|
|
12781
14572
|
continue;
|
|
12782
|
-
|
|
12783
|
-
|
|
12784
|
-
files.push(fullPath);
|
|
14573
|
+
const fullPath = (0, import_node_path9.join)(dirOrFile, entry);
|
|
14574
|
+
await walk(fullPath);
|
|
12785
14575
|
}
|
|
12786
14576
|
}
|
|
12787
14577
|
}
|
|
12788
14578
|
try {
|
|
12789
14579
|
await walk(installPath);
|
|
12790
14580
|
} catch (e) {
|
|
12791
|
-
logger.warn(`Error walking plugin
|
|
14581
|
+
logger.warn(`Error walking plugin path ${installPath}`, { error: String(e) });
|
|
12792
14582
|
}
|
|
12793
14583
|
return files;
|
|
12794
14584
|
}
|
|
@@ -12806,7 +14596,7 @@ function extractArtifactsFromFile(filePath, content) {
|
|
|
12806
14596
|
continue;
|
|
12807
14597
|
artifacts.push({ type: "url", value: url, context: `plugin_file:${fileName}` });
|
|
12808
14598
|
}
|
|
12809
|
-
const ext = (0,
|
|
14599
|
+
const ext = (0, import_node_path9.extname)(filePath).toLowerCase();
|
|
12810
14600
|
if ([".sh", ".bash", ".zsh", ".py"].includes(ext)) {
|
|
12811
14601
|
for (const line of content.split("\n")) {
|
|
12812
14602
|
const trimmed = line.trim();
|
|
@@ -12851,7 +14641,7 @@ async function scanPlugin(plugin, threats, options = {}) {
|
|
|
12851
14641
|
confidence: match.threat.confidence,
|
|
12852
14642
|
action: match.threat.action,
|
|
12853
14643
|
artifact: match.artifact.slice(0, 200),
|
|
12854
|
-
sourceFile: (0,
|
|
14644
|
+
sourceFile: (0, import_node_path9.relative)(plugin.installPath, filePath)
|
|
12855
14645
|
});
|
|
12856
14646
|
}
|
|
12857
14647
|
}
|
|
@@ -12859,7 +14649,7 @@ async function scanPlugin(plugin, threats, options = {}) {
|
|
|
12859
14649
|
allUrls.push(...extractUrls(content));
|
|
12860
14650
|
}
|
|
12861
14651
|
if (checkFileHashes) {
|
|
12862
|
-
const sha256 = (0,
|
|
14652
|
+
const sha256 = (0, import_node_crypto6.createHash)("sha256").update(rawBytes).digest("hex");
|
|
12863
14653
|
const existing = hashToFiles.get(sha256);
|
|
12864
14654
|
if (existing) {
|
|
12865
14655
|
existing.push(filePath);
|
|
@@ -12906,7 +14696,7 @@ async function scanPlugin(plugin, threats, options = {}) {
|
|
|
12906
14696
|
confidence: 1,
|
|
12907
14697
|
action: "block",
|
|
12908
14698
|
artifact: fr.sha256,
|
|
12909
|
-
sourceFile: (0,
|
|
14699
|
+
sourceFile: (0, import_node_path9.relative)(plugin.installPath, filePath)
|
|
12910
14700
|
});
|
|
12911
14701
|
}
|
|
12912
14702
|
}
|
|
@@ -12942,18 +14732,63 @@ function toFindingData(finding) {
|
|
|
12942
14732
|
};
|
|
12943
14733
|
}
|
|
12944
14734
|
function toAuditFindingData(finding) {
|
|
12945
|
-
|
|
12946
|
-
|
|
12947
|
-
|
|
12948
|
-
|
|
12949
|
-
|
|
12950
|
-
|
|
12951
|
-
|
|
12952
|
-
|
|
14735
|
+
const { action: _, ...rest } = toFindingData(finding);
|
|
14736
|
+
return rest;
|
|
14737
|
+
}
|
|
14738
|
+
async function runSessionStartScan(context) {
|
|
14739
|
+
const logger = context.logger ?? nullLogger;
|
|
14740
|
+
const threats = await loadThreats(context.threatsDir, logger);
|
|
14741
|
+
const trustedDomains = await loadTrustedDomains(context.allowlistsDir, logger);
|
|
14742
|
+
if (threats.length === 0) {
|
|
14743
|
+
return [];
|
|
14744
|
+
}
|
|
14745
|
+
const plugins = context.plugins;
|
|
14746
|
+
if (plugins.length === 0) {
|
|
14747
|
+
return [];
|
|
14748
|
+
}
|
|
14749
|
+
const configHash = await computeConfigHash(context.sageVersion ?? "", context.threatsDir, context.allowlistsDir);
|
|
14750
|
+
const cache = await loadScanCache(configHash, context.scanCachePath, logger);
|
|
14751
|
+
const resultsWithFindings = [];
|
|
14752
|
+
let cacheModified = false;
|
|
14753
|
+
for (const plugin of plugins) {
|
|
14754
|
+
const cached = getCached(cache, plugin.key, plugin.version, plugin.lastUpdated);
|
|
14755
|
+
if (cached && cached.findings.length === 0) {
|
|
14756
|
+
continue;
|
|
14757
|
+
}
|
|
14758
|
+
if (cached && cached.findings.length > 0) {
|
|
14759
|
+
resultsWithFindings.push({
|
|
14760
|
+
plugin,
|
|
14761
|
+
findings: cached.findings.map(fromCachedFinding)
|
|
14762
|
+
});
|
|
14763
|
+
continue;
|
|
14764
|
+
}
|
|
14765
|
+
const result = await scanPlugin(plugin, threats, {
|
|
14766
|
+
checkUrls: context.checkUrls ?? true,
|
|
14767
|
+
checkFileHashes: context.checkFileHashes ?? true,
|
|
14768
|
+
trustedDomains,
|
|
14769
|
+
logger
|
|
14770
|
+
});
|
|
14771
|
+
storeResult(cache, plugin.key, plugin.version, plugin.lastUpdated, result.findings.map(toFindingData));
|
|
14772
|
+
cacheModified = true;
|
|
14773
|
+
if (result.findings.length > 0) {
|
|
14774
|
+
resultsWithFindings.push(result);
|
|
14775
|
+
}
|
|
14776
|
+
}
|
|
14777
|
+
if (cacheModified) {
|
|
14778
|
+
await saveScanCache(cache, context.scanCachePath, logger);
|
|
14779
|
+
}
|
|
14780
|
+
try {
|
|
14781
|
+
const sageConfig = await loadConfig(context.configPath, logger);
|
|
14782
|
+
for (const result of resultsWithFindings) {
|
|
14783
|
+
await logPluginScan(sageConfig.logging, result.plugin.key, result.plugin.version, result.findings.map(toAuditFindingData));
|
|
14784
|
+
}
|
|
14785
|
+
} catch {
|
|
14786
|
+
}
|
|
14787
|
+
return resultsWithFindings;
|
|
12953
14788
|
}
|
|
12954
14789
|
|
|
12955
14790
|
// ../core/dist/version-check.js
|
|
12956
|
-
var
|
|
14791
|
+
var import_node_os5 = require("node:os");
|
|
12957
14792
|
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
12958
14793
|
function isNewerVersion(current, latest) {
|
|
12959
14794
|
const parse = (v) => v.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
@@ -12969,19 +14804,29 @@ function isNewerVersion(current, latest) {
|
|
|
12969
14804
|
}
|
|
12970
14805
|
return false;
|
|
12971
14806
|
}
|
|
12972
|
-
async function checkForUpdate(currentVersion, logger = nullLogger, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
14807
|
+
async function checkForUpdate(currentVersion, logger = nullLogger, timeoutMs = DEFAULT_TIMEOUT_MS, context) {
|
|
12973
14808
|
if (currentVersion === "dev") {
|
|
12974
14809
|
logger.debug("Skipping version check for dev build");
|
|
12975
14810
|
return null;
|
|
12976
14811
|
}
|
|
12977
14812
|
try {
|
|
12978
|
-
const
|
|
12979
|
-
|
|
12980
|
-
|
|
12981
|
-
|
|
12982
|
-
|
|
14813
|
+
const response = await fetch(resolveEndpoint("/v1/version-check"), {
|
|
14814
|
+
method: "POST",
|
|
14815
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
14816
|
+
headers: {
|
|
14817
|
+
Accept: "application/json",
|
|
14818
|
+
"Content-Type": "application/json"
|
|
14819
|
+
},
|
|
14820
|
+
body: JSON.stringify({
|
|
14821
|
+
sage_version: currentVersion,
|
|
14822
|
+
agent_runtime: context?.agentRuntime,
|
|
14823
|
+
agent_runtime_version: context?.agentRuntimeVersion,
|
|
14824
|
+
os: process.platform,
|
|
14825
|
+
os_version: (0, import_node_os5.release)(),
|
|
14826
|
+
arch: process.arch,
|
|
14827
|
+
iid: context?.iid
|
|
14828
|
+
})
|
|
12983
14829
|
});
|
|
12984
|
-
clearTimeout(timer);
|
|
12985
14830
|
if (!response.ok) {
|
|
12986
14831
|
logger.debug(`Version check HTTP ${response.status}`);
|
|
12987
14832
|
return null;
|
|
@@ -13003,61 +14848,147 @@ async function checkForUpdate(currentVersion, logger = nullLogger, timeoutMs = D
|
|
|
13003
14848
|
}
|
|
13004
14849
|
}
|
|
13005
14850
|
|
|
13006
|
-
//
|
|
13007
|
-
|
|
13008
|
-
|
|
13009
|
-
|
|
13010
|
-
|
|
13011
|
-
|
|
13012
|
-
|
|
13013
|
-
|
|
13014
|
-
|
|
13015
|
-
|
|
13016
|
-
|
|
13017
|
-
|
|
14851
|
+
// ../core/dist/session-start.js
|
|
14852
|
+
async function runSessionStart(ctx) {
|
|
14853
|
+
const logger = ctx.logger ?? nullLogger;
|
|
14854
|
+
const sageDirPath = resolvePath(ctx.sageDirPath ?? "~/.sage");
|
|
14855
|
+
pruneOrphanedTmpFiles(sageDirPath).catch(() => {
|
|
14856
|
+
});
|
|
14857
|
+
const iidPromise = getInstallationId(sageDirPath).catch(() => void 0);
|
|
14858
|
+
const [scanResults, versionCheck] = await Promise.all([
|
|
14859
|
+
runSessionStartScan({
|
|
14860
|
+
plugins: ctx.plugins,
|
|
14861
|
+
threatsDir: ctx.threatsDir,
|
|
14862
|
+
allowlistsDir: ctx.allowlistsDir,
|
|
14863
|
+
sageVersion: ctx.version,
|
|
14864
|
+
logger,
|
|
14865
|
+
configPath: ctx.configPath,
|
|
14866
|
+
scanCachePath: ctx.scanCachePath,
|
|
14867
|
+
checkUrls: ctx.checkUrls,
|
|
14868
|
+
checkFileHashes: ctx.checkFileHashes
|
|
14869
|
+
}),
|
|
14870
|
+
iidPromise.then((iid) => checkForUpdate(ctx.version, logger, void 0, {
|
|
14871
|
+
agentRuntime: ctx.agentRuntime,
|
|
14872
|
+
agentRuntimeVersion: ctx.agentRuntimeVersion,
|
|
14873
|
+
iid
|
|
14874
|
+
})).catch(() => null)
|
|
14875
|
+
]);
|
|
14876
|
+
return { scanResults, versionCheck };
|
|
14877
|
+
}
|
|
14878
|
+
|
|
14879
|
+
// ../core/dist/scan-handler.js
|
|
14880
|
+
async function runPluginScan(logger, context, plugins, threatsDir, allowlistsDir, version, agentRuntime) {
|
|
14881
|
+
logger.info(`Sage plugin scan started (${context})`, { threatsDir, allowlistsDir });
|
|
14882
|
+
const result = await runSessionStart({
|
|
14883
|
+
plugins,
|
|
14884
|
+
threatsDir,
|
|
14885
|
+
allowlistsDir,
|
|
14886
|
+
version,
|
|
14887
|
+
logger,
|
|
14888
|
+
agentRuntime
|
|
14889
|
+
});
|
|
14890
|
+
logger.info(`Sage plugin scan (${context}) complete`, {
|
|
14891
|
+
findings: result.scanResults.length,
|
|
14892
|
+
updateAvailable: result.versionCheck?.updateAvailable ?? false
|
|
14893
|
+
});
|
|
14894
|
+
return formatSessionStartMessage(version, result);
|
|
14895
|
+
}
|
|
14896
|
+
function createScanHandler(options) {
|
|
14897
|
+
const { logger, context, discoverPlugins: discoverPlugins2, selfPrefix, threatsDir, allowlistsDir, version, agentRuntime, onResult } = options;
|
|
14898
|
+
return async () => {
|
|
13018
14899
|
try {
|
|
13019
|
-
|
|
13020
|
-
|
|
13021
|
-
|
|
13022
|
-
|
|
13023
|
-
for (const [id, entry] of Object.entries(parsed)) {
|
|
13024
|
-
if (entry && new Date(entry.expiresAt).getTime() > now) {
|
|
13025
|
-
this.data[id] = entry;
|
|
13026
|
-
}
|
|
14900
|
+
let plugins = await discoverPlugins2();
|
|
14901
|
+
plugins = plugins.filter((p) => !p.key.startsWith(selfPrefix));
|
|
14902
|
+
if (plugins.length === 0) {
|
|
14903
|
+
logger.warn(`Sage plugin scan (${context}): no plugins to scan after filtering`);
|
|
13027
14904
|
}
|
|
13028
|
-
|
|
13029
|
-
|
|
14905
|
+
const msg = await runPluginScan(logger, context, plugins, threatsDir, allowlistsDir, version, agentRuntime);
|
|
14906
|
+
onResult?.(msg);
|
|
14907
|
+
} catch (e) {
|
|
14908
|
+
logger.error(`Sage ${context} scan failed`, { error: String(e) });
|
|
13030
14909
|
}
|
|
13031
|
-
}
|
|
13032
|
-
|
|
13033
|
-
|
|
13034
|
-
|
|
13035
|
-
|
|
13036
|
-
|
|
13037
|
-
|
|
14910
|
+
};
|
|
14911
|
+
}
|
|
14912
|
+
|
|
14913
|
+
// src/allowlist-tools.ts
|
|
14914
|
+
function createAllowlistAddTool(approvalStore, logger) {
|
|
14915
|
+
return {
|
|
14916
|
+
name: "sage_allowlist_add",
|
|
14917
|
+
description: "Permanently allow a URL, command, or file path after recent user approval through Sage.",
|
|
14918
|
+
parameters: {
|
|
14919
|
+
type: "object",
|
|
14920
|
+
properties: {
|
|
14921
|
+
type: {
|
|
14922
|
+
type: "string",
|
|
14923
|
+
enum: ["url", "command", "file_path"],
|
|
14924
|
+
description: "Artifact type"
|
|
14925
|
+
},
|
|
14926
|
+
value: {
|
|
14927
|
+
type: "string",
|
|
14928
|
+
description: "Exact URL, command, or file path to allowlist"
|
|
14929
|
+
},
|
|
14930
|
+
reason: {
|
|
14931
|
+
type: "string",
|
|
14932
|
+
description: "Optional reason for allowlist entry"
|
|
14933
|
+
}
|
|
14934
|
+
},
|
|
14935
|
+
required: ["type", "value"]
|
|
14936
|
+
},
|
|
14937
|
+
async execute(_toolCallId, params) {
|
|
14938
|
+
const { type, value, reason } = params;
|
|
14939
|
+
const msg = await addToAllowlist(approvalStore, type, value, reason, void 0, logger);
|
|
14940
|
+
return { content: [{ type: "text", text: msg }] };
|
|
13038
14941
|
}
|
|
13039
|
-
|
|
13040
|
-
|
|
13041
|
-
|
|
13042
|
-
|
|
13043
|
-
|
|
13044
|
-
|
|
13045
|
-
|
|
13046
|
-
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
13051
|
-
|
|
13052
|
-
|
|
13053
|
-
|
|
14942
|
+
};
|
|
14943
|
+
}
|
|
14944
|
+
function createAllowlistRemoveTool(logger) {
|
|
14945
|
+
return {
|
|
14946
|
+
name: "sage_allowlist_remove",
|
|
14947
|
+
description: "Remove a URL, command, or file path from the Sage allowlist.",
|
|
14948
|
+
parameters: {
|
|
14949
|
+
type: "object",
|
|
14950
|
+
properties: {
|
|
14951
|
+
type: {
|
|
14952
|
+
type: "string",
|
|
14953
|
+
enum: ["url", "command", "file_path"],
|
|
14954
|
+
description: "Artifact type"
|
|
14955
|
+
},
|
|
14956
|
+
value: {
|
|
14957
|
+
type: "string",
|
|
14958
|
+
description: "URL/file path, or command text/command hash for command entries"
|
|
14959
|
+
}
|
|
14960
|
+
},
|
|
14961
|
+
required: ["type", "value"]
|
|
14962
|
+
},
|
|
14963
|
+
async execute(_toolCallId, params) {
|
|
14964
|
+
const { type, value } = params;
|
|
14965
|
+
const msg = await removeFromAllowlist(type, value, void 0, logger);
|
|
14966
|
+
return { content: [{ type: "text", text: msg }] };
|
|
13054
14967
|
}
|
|
14968
|
+
};
|
|
14969
|
+
}
|
|
14970
|
+
|
|
14971
|
+
// src/bundled-dirs.ts
|
|
14972
|
+
var import_node_fs = require("node:fs");
|
|
14973
|
+
var import_node_path10 = require("node:path");
|
|
14974
|
+
var packageRoot = (0, import_node_path10.resolve)(__dirname, "..");
|
|
14975
|
+
function getBundledDataDirs() {
|
|
14976
|
+
return {
|
|
14977
|
+
threatsDir: (0, import_node_path10.join)(packageRoot, "resources", "threats"),
|
|
14978
|
+
allowlistsDir: (0, import_node_path10.join)(packageRoot, "resources", "allowlists")
|
|
14979
|
+
};
|
|
14980
|
+
}
|
|
14981
|
+
var cachedVersion;
|
|
14982
|
+
function getSageVersion() {
|
|
14983
|
+
if (cachedVersion) return cachedVersion;
|
|
14984
|
+
try {
|
|
14985
|
+
const pkg = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path10.join)(packageRoot, "package.json"), "utf-8"));
|
|
14986
|
+
cachedVersion = pkg.version ?? "0.0.0";
|
|
14987
|
+
} catch {
|
|
14988
|
+
cachedVersion = "0.0.0";
|
|
13055
14989
|
}
|
|
13056
|
-
|
|
13057
|
-
|
|
13058
|
-
return (0, import_node_crypto5.createHash)("sha256").update(payload).digest("hex");
|
|
13059
|
-
}
|
|
13060
|
-
};
|
|
14990
|
+
return cachedVersion;
|
|
14991
|
+
}
|
|
13061
14992
|
|
|
13062
14993
|
// src/gate-tool.ts
|
|
13063
14994
|
function createSageApproveTool(approvalStore) {
|
|
@@ -13079,14 +15010,15 @@ function createSageApproveTool(approvalStore) {
|
|
|
13079
15010
|
required: ["actionId", "approved"]
|
|
13080
15011
|
},
|
|
13081
15012
|
async execute(_toolCallId, params) {
|
|
13082
|
-
if (params.approved) {
|
|
13083
|
-
|
|
15013
|
+
if (!params.approved) {
|
|
15014
|
+
approvalStore.deletePending(params.actionId);
|
|
13084
15015
|
return {
|
|
13085
|
-
content: [{ type: "text", text: "
|
|
15016
|
+
content: [{ type: "text", text: "Rejected by user." }]
|
|
13086
15017
|
};
|
|
13087
15018
|
}
|
|
15019
|
+
const msg = await approveAction(approvalStore, params.actionId);
|
|
13088
15020
|
return {
|
|
13089
|
-
content: [{ type: "text", text:
|
|
15021
|
+
content: [{ type: "text", text: msg }]
|
|
13090
15022
|
};
|
|
13091
15023
|
}
|
|
13092
15024
|
};
|
|
@@ -13113,30 +15045,8 @@ function createLogger(pluginLogger) {
|
|
|
13113
15045
|
};
|
|
13114
15046
|
}
|
|
13115
15047
|
|
|
13116
|
-
// src/bundled-dirs.ts
|
|
13117
|
-
var import_node_fs = require("node:fs");
|
|
13118
|
-
var import_node_path10 = require("node:path");
|
|
13119
|
-
var packageRoot = (0, import_node_path10.resolve)(__dirname, "..");
|
|
13120
|
-
function getBundledDataDirs() {
|
|
13121
|
-
return {
|
|
13122
|
-
threatsDir: (0, import_node_path10.join)(packageRoot, "resources", "threats"),
|
|
13123
|
-
allowlistsDir: (0, import_node_path10.join)(packageRoot, "resources", "allowlists")
|
|
13124
|
-
};
|
|
13125
|
-
}
|
|
13126
|
-
var cachedVersion;
|
|
13127
|
-
function getSageVersion() {
|
|
13128
|
-
if (cachedVersion) return cachedVersion;
|
|
13129
|
-
try {
|
|
13130
|
-
const pkg = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path10.join)(packageRoot, "package.json"), "utf-8"));
|
|
13131
|
-
cachedVersion = pkg.version ?? "0.0.0";
|
|
13132
|
-
} catch {
|
|
13133
|
-
cachedVersion = "0.0.0";
|
|
13134
|
-
}
|
|
13135
|
-
return cachedVersion;
|
|
13136
|
-
}
|
|
13137
|
-
|
|
13138
15048
|
// src/plugin-discovery.ts
|
|
13139
|
-
var
|
|
15049
|
+
var import_promises7 = require("node:fs/promises");
|
|
13140
15050
|
var import_node_os6 = require("node:os");
|
|
13141
15051
|
var import_node_path11 = require("node:path");
|
|
13142
15052
|
var DEFAULT_EXTENSIONS_DIR = (0, import_node_path11.join)((0, import_node_os6.homedir)(), ".openclaw", "extensions");
|
|
@@ -13144,7 +15054,7 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
|
|
|
13144
15054
|
logger.info("Sage plugin discovery: scanning extensions directory", { path: extensionsDir });
|
|
13145
15055
|
let entries;
|
|
13146
15056
|
try {
|
|
13147
|
-
entries = await (0,
|
|
15057
|
+
entries = await (0, import_promises7.readdir)(extensionsDir);
|
|
13148
15058
|
} catch {
|
|
13149
15059
|
logger.debug("OpenClaw extensions directory not found", { path: extensionsDir });
|
|
13150
15060
|
return [];
|
|
@@ -13154,7 +15064,7 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
|
|
|
13154
15064
|
const extDir = (0, import_node_path11.join)(extensionsDir, entry);
|
|
13155
15065
|
let stats;
|
|
13156
15066
|
try {
|
|
13157
|
-
stats = await (0,
|
|
15067
|
+
stats = await (0, import_promises7.stat)(extDir);
|
|
13158
15068
|
if (!stats.isDirectory()) continue;
|
|
13159
15069
|
} catch {
|
|
13160
15070
|
continue;
|
|
@@ -13184,110 +15094,26 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
|
|
|
13184
15094
|
}
|
|
13185
15095
|
|
|
13186
15096
|
// src/startup-scan.ts
|
|
13187
|
-
|
|
13188
|
-
async function runScan(logger, context) {
|
|
15097
|
+
function createOpenClawScanHandler(logger, context, onResult) {
|
|
13189
15098
|
const { threatsDir, allowlistsDir } = getBundledDataDirs();
|
|
13190
15099
|
const version = getSageVersion();
|
|
13191
|
-
|
|
13192
|
-
|
|
13193
|
-
|
|
13194
|
-
|
|
13195
|
-
|
|
13196
|
-
|
|
13197
|
-
|
|
13198
|
-
|
|
13199
|
-
|
|
13200
|
-
|
|
13201
|
-
}
|
|
13202
|
-
logger.info(`Sage plugin scan (${context}): loaded ${threats.length} threat definitions`);
|
|
13203
|
-
let plugins = await discoverOpenClawPlugins(logger);
|
|
13204
|
-
plugins = plugins.filter((p) => !p.key.startsWith(`${SAGE_PLUGIN_ID}@`));
|
|
13205
|
-
if (plugins.length === 0) {
|
|
13206
|
-
logger.warn(`Sage plugin scan (${context}): no plugins to scan after filtering`);
|
|
13207
|
-
return updateNotice;
|
|
13208
|
-
}
|
|
13209
|
-
logger.info(`Sage plugin scan (${context}): ${plugins.length} plugin(s) to scan`, {
|
|
13210
|
-
keys: plugins.map((p) => p.key)
|
|
15100
|
+
return createScanHandler({
|
|
15101
|
+
logger,
|
|
15102
|
+
context,
|
|
15103
|
+
discoverPlugins: () => discoverOpenClawPlugins(logger),
|
|
15104
|
+
selfPrefix: "sage-openclaw@",
|
|
15105
|
+
threatsDir,
|
|
15106
|
+
allowlistsDir,
|
|
15107
|
+
version,
|
|
15108
|
+
agentRuntime: "openclaw",
|
|
15109
|
+
onResult
|
|
13211
15110
|
});
|
|
13212
|
-
const configHash = await computeConfigHash("", threatsDir, allowlistsDir);
|
|
13213
|
-
const cache = await loadScanCache(configHash, void 0, logger);
|
|
13214
|
-
const resultsWithFindings = [];
|
|
13215
|
-
let cacheModified = false;
|
|
13216
|
-
let scannedCount = 0;
|
|
13217
|
-
for (const plugin of plugins) {
|
|
13218
|
-
const cached = getCached(cache, plugin.key, plugin.version, plugin.lastUpdated);
|
|
13219
|
-
if (cached && cached.findings.length === 0) {
|
|
13220
|
-
logger.info(`Sage plugin scan (${context}): cache hit (clean) for ${plugin.key}`);
|
|
13221
|
-
continue;
|
|
13222
|
-
}
|
|
13223
|
-
if (cached && cached.findings.length > 0) {
|
|
13224
|
-
logger.info(
|
|
13225
|
-
`Sage plugin scan (${context}): cache hit with ${cached.findings.length} finding(s) for ${plugin.key}`
|
|
13226
|
-
);
|
|
13227
|
-
resultsWithFindings.push({ plugin, findings: cached.findings.map(fromCachedFinding) });
|
|
13228
|
-
continue;
|
|
13229
|
-
}
|
|
13230
|
-
logger.info(`Sage plugin scan (${context}): scanning ${plugin.key}`);
|
|
13231
|
-
const result = await scanPlugin(plugin, threats, {
|
|
13232
|
-
checkUrls: true,
|
|
13233
|
-
trustedDomains,
|
|
13234
|
-
logger
|
|
13235
|
-
});
|
|
13236
|
-
scannedCount++;
|
|
13237
|
-
storeResult(
|
|
13238
|
-
cache,
|
|
13239
|
-
plugin.key,
|
|
13240
|
-
plugin.version,
|
|
13241
|
-
plugin.lastUpdated,
|
|
13242
|
-
result.findings.map(toFindingData)
|
|
13243
|
-
);
|
|
13244
|
-
cacheModified = true;
|
|
13245
|
-
if (result.findings.length > 0) {
|
|
13246
|
-
resultsWithFindings.push(result);
|
|
13247
|
-
}
|
|
13248
|
-
}
|
|
13249
|
-
if (cacheModified) {
|
|
13250
|
-
await saveScanCache(cache, void 0, logger);
|
|
13251
|
-
}
|
|
13252
|
-
logger.info(
|
|
13253
|
-
`Sage plugin scan (${context}) complete: ${scannedCount} scanned, ${resultsWithFindings.length} with findings, cache ${cacheModified ? "updated" : "unchanged"}`
|
|
13254
|
-
);
|
|
13255
|
-
try {
|
|
13256
|
-
const sageConfig = await loadConfig(void 0, logger);
|
|
13257
|
-
for (const result of resultsWithFindings) {
|
|
13258
|
-
await logPluginScan(
|
|
13259
|
-
sageConfig.logging,
|
|
13260
|
-
result.plugin.key,
|
|
13261
|
-
result.plugin.version,
|
|
13262
|
-
result.findings.map(toAuditFindingData)
|
|
13263
|
-
);
|
|
13264
|
-
}
|
|
13265
|
-
} catch {
|
|
13266
|
-
}
|
|
13267
|
-
if (resultsWithFindings.length > 0) {
|
|
13268
|
-
const banner = formatThreatBanner(version, resultsWithFindings, versionCheck);
|
|
13269
|
-
logger.warn(`Sage: threat findings detected`, {
|
|
13270
|
-
plugins: resultsWithFindings.map((r) => r.plugin.key)
|
|
13271
|
-
});
|
|
13272
|
-
return banner;
|
|
13273
|
-
}
|
|
13274
|
-
return updateNotice;
|
|
13275
|
-
}
|
|
13276
|
-
function createScanHandler(logger, context, onFindings) {
|
|
13277
|
-
return async () => {
|
|
13278
|
-
try {
|
|
13279
|
-
const findings = await runScan(logger, context);
|
|
13280
|
-
onFindings?.(findings);
|
|
13281
|
-
} catch (e) {
|
|
13282
|
-
logger.error(`Sage ${context} scan failed`, { error: String(e) });
|
|
13283
|
-
}
|
|
13284
|
-
};
|
|
13285
15111
|
}
|
|
13286
|
-
function createStartupScanHandler(logger,
|
|
13287
|
-
return
|
|
15112
|
+
function createStartupScanHandler(logger, onResult) {
|
|
15113
|
+
return createOpenClawScanHandler(logger, "startup", onResult);
|
|
13288
15114
|
}
|
|
13289
|
-
function createSessionScanHandler(logger,
|
|
13290
|
-
return
|
|
15115
|
+
function createSessionScanHandler(logger, onResult) {
|
|
15116
|
+
return createOpenClawScanHandler(logger, "session", onResult);
|
|
13291
15117
|
}
|
|
13292
15118
|
function createBeforeAgentStartHandler(getFindings, clearFindings, logger) {
|
|
13293
15119
|
return () => {
|
|
@@ -13341,153 +15167,32 @@ function mapToolToArtifacts(toolName, params) {
|
|
|
13341
15167
|
return null;
|
|
13342
15168
|
}
|
|
13343
15169
|
}
|
|
13344
|
-
function createToolCallHandler(approvalStore, logger,
|
|
13345
|
-
|
|
15170
|
+
function createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir) {
|
|
15171
|
+
async function handleToolCall(event, ctx) {
|
|
13346
15172
|
try {
|
|
13347
15173
|
const { toolName, params } = event;
|
|
13348
15174
|
const artifacts = mapToolToArtifacts(toolName, params);
|
|
13349
15175
|
if (!artifacts || artifacts.length === 0) return void 0;
|
|
13350
|
-
const
|
|
13351
|
-
|
|
13352
|
-
|
|
13353
|
-
|
|
15176
|
+
const sessionId = ctx?.sessionKey ?? "unknown";
|
|
15177
|
+
const { verdict, actionId } = await guardToolCall(
|
|
15178
|
+
{ sessionId, toolName, toolInput: params, artifacts },
|
|
15179
|
+
{ threatsDir, allowlistsDir, logger },
|
|
15180
|
+
approvalStore
|
|
13354
15181
|
);
|
|
13355
|
-
try {
|
|
13356
|
-
const allowlist = await loadAllowlist(config.allowlist, logger);
|
|
13357
|
-
if (isAllowlisted(allowlist, artifacts)) {
|
|
13358
|
-
await logVerdict(
|
|
13359
|
-
config.logging,
|
|
13360
|
-
sessionKey,
|
|
13361
|
-
toolName,
|
|
13362
|
-
params,
|
|
13363
|
-
{
|
|
13364
|
-
decision: "allow",
|
|
13365
|
-
category: "none",
|
|
13366
|
-
confidence: 1,
|
|
13367
|
-
severity: "info",
|
|
13368
|
-
source: "allowlisted",
|
|
13369
|
-
artifacts: [],
|
|
13370
|
-
matchedThreatId: null,
|
|
13371
|
-
reasons: []
|
|
13372
|
-
},
|
|
13373
|
-
true
|
|
13374
|
-
);
|
|
13375
|
-
return void 0;
|
|
13376
|
-
}
|
|
13377
|
-
} catch {
|
|
13378
|
-
}
|
|
13379
|
-
let cache = null;
|
|
13380
|
-
try {
|
|
13381
|
-
cache = new VerdictCache(config.cache, logger);
|
|
13382
|
-
await cache.load();
|
|
13383
|
-
} catch {
|
|
13384
|
-
cache = null;
|
|
13385
|
-
}
|
|
13386
|
-
const urls = artifacts.filter((a) => a.type === "url").map((a) => a.value);
|
|
13387
|
-
const cachedUrlVerdicts = /* @__PURE__ */ new Map();
|
|
13388
|
-
let uncachedUrls = [];
|
|
13389
|
-
if (cache && urls.length > 0) {
|
|
13390
|
-
try {
|
|
13391
|
-
for (const url of urls) {
|
|
13392
|
-
const cached = cache.getUrl(url);
|
|
13393
|
-
if (cached) {
|
|
13394
|
-
cachedUrlVerdicts.set(url, cached);
|
|
13395
|
-
} else {
|
|
13396
|
-
uncachedUrls.push(url);
|
|
13397
|
-
}
|
|
13398
|
-
}
|
|
13399
|
-
} catch {
|
|
13400
|
-
uncachedUrls = urls;
|
|
13401
|
-
}
|
|
13402
|
-
}
|
|
13403
|
-
const { threatsDir, allowlistsDir } = getBundledDataDirs();
|
|
13404
|
-
let heuristicMatches = [];
|
|
13405
|
-
if (config.heuristics_enabled) {
|
|
13406
|
-
const threats = await loadThreats(threatsDir, logger);
|
|
13407
|
-
const trustedDomains = await loadTrustedDomains(allowlistsDir, logger);
|
|
13408
|
-
const heuristics = new HeuristicsEngine(threats, trustedDomains);
|
|
13409
|
-
heuristicMatches = heuristics.match(artifacts);
|
|
13410
|
-
}
|
|
13411
|
-
let urlCheckResults = [];
|
|
13412
|
-
const urlsToCheck = cache ? uncachedUrls : urls;
|
|
13413
|
-
if (urlsToCheck.length > 0 && config.url_check.enabled) {
|
|
13414
|
-
try {
|
|
13415
|
-
const client = new UrlCheckClient(config.url_check, logger);
|
|
13416
|
-
urlCheckResults = await client.checkUrls(urlsToCheck);
|
|
13417
|
-
} catch {
|
|
13418
|
-
}
|
|
13419
|
-
}
|
|
13420
|
-
const engine = new DecisionEngine(config.sensitivity);
|
|
13421
|
-
let verdict = await engine.decide(heuristicMatches, urlCheckResults);
|
|
13422
|
-
if (cachedUrlVerdicts.size > 0 && verdict.decision === "allow") {
|
|
13423
|
-
for (const [url, cv] of cachedUrlVerdicts) {
|
|
13424
|
-
if (cv.verdict !== "allow") {
|
|
13425
|
-
verdict = {
|
|
13426
|
-
decision: cv.verdict,
|
|
13427
|
-
category: "network_egress",
|
|
13428
|
-
confidence: 1,
|
|
13429
|
-
severity: cv.severity,
|
|
13430
|
-
source: `cache(${cv.source})`,
|
|
13431
|
-
artifacts: [url],
|
|
13432
|
-
matchedThreatId: null,
|
|
13433
|
-
reasons: cv.reasons
|
|
13434
|
-
};
|
|
13435
|
-
break;
|
|
13436
|
-
}
|
|
13437
|
-
}
|
|
13438
|
-
}
|
|
13439
|
-
if (cache) {
|
|
13440
|
-
try {
|
|
13441
|
-
for (const result of urlCheckResults) {
|
|
13442
|
-
let cv;
|
|
13443
|
-
if (result.isMalicious) {
|
|
13444
|
-
cv = {
|
|
13445
|
-
verdict: "deny",
|
|
13446
|
-
severity: "critical",
|
|
13447
|
-
reasons: [
|
|
13448
|
-
`URL check: malicious (${result.findings.map((f) => `${f.severityName}/${f.typeName}`).join(", ")})`
|
|
13449
|
-
],
|
|
13450
|
-
source: "url_check"
|
|
13451
|
-
};
|
|
13452
|
-
} else if (result.flags.length > 0) {
|
|
13453
|
-
cv = {
|
|
13454
|
-
verdict: "ask",
|
|
13455
|
-
severity: "warning",
|
|
13456
|
-
reasons: [`URL check: suspicious (${result.flags.join(", ")})`],
|
|
13457
|
-
source: "url_check"
|
|
13458
|
-
};
|
|
13459
|
-
} else {
|
|
13460
|
-
cv = {
|
|
13461
|
-
verdict: "allow",
|
|
13462
|
-
severity: "info",
|
|
13463
|
-
reasons: [],
|
|
13464
|
-
source: "url_check"
|
|
13465
|
-
};
|
|
13466
|
-
}
|
|
13467
|
-
cache.putUrl(result.url, cv, result.isMalicious);
|
|
13468
|
-
}
|
|
13469
|
-
await cache.save();
|
|
13470
|
-
} catch {
|
|
13471
|
-
}
|
|
13472
|
-
}
|
|
13473
|
-
try {
|
|
13474
|
-
await logVerdict(config.logging, sessionKey, toolName, params, verdict);
|
|
13475
|
-
} catch {
|
|
13476
|
-
}
|
|
13477
15182
|
if (verdict.decision === "allow") return void 0;
|
|
13478
|
-
const reasonText = verdict.reasons.length > 0 ? verdict.reasons.slice(0, 5).join("; ") : verdict.category;
|
|
13479
15183
|
if (verdict.decision === "deny") {
|
|
13480
|
-
return { block: true, blockReason:
|
|
15184
|
+
return { block: true, blockReason: formatDenyMessage(verdict) };
|
|
13481
15185
|
}
|
|
13482
15186
|
return {
|
|
13483
15187
|
block: true,
|
|
13484
|
-
blockReason:
|
|
15188
|
+
blockReason: formatAskMessage(actionId, verdict, artifacts)
|
|
13485
15189
|
};
|
|
13486
15190
|
} catch (e) {
|
|
13487
15191
|
logger.error("tool-handler error, failing open", { error: String(e) });
|
|
13488
15192
|
return void 0;
|
|
13489
15193
|
}
|
|
13490
|
-
}
|
|
15194
|
+
}
|
|
15195
|
+
return handleToolCall;
|
|
13491
15196
|
}
|
|
13492
15197
|
|
|
13493
15198
|
// src/index.ts
|
|
@@ -13500,15 +15205,25 @@ var index_default = {
|
|
|
13500
15205
|
},
|
|
13501
15206
|
register(api) {
|
|
13502
15207
|
const logger = createLogger(api.logger);
|
|
13503
|
-
const approvalStore = new ApprovalStore(
|
|
15208
|
+
const approvalStore = new ApprovalStore();
|
|
15209
|
+
const { threatsDir, allowlistsDir } = getBundledDataDirs();
|
|
15210
|
+
const CLEANUP_INTERVAL_MS = 5 * 60 * 1e3;
|
|
15211
|
+
const interval = setInterval(() => approvalStore.cleanup(), CLEANUP_INTERVAL_MS);
|
|
15212
|
+
if (typeof interval.unref === "function") interval.unref();
|
|
13504
15213
|
let pendingFindings = null;
|
|
13505
15214
|
const onFindings = (msg) => {
|
|
13506
|
-
|
|
15215
|
+
pendingFindings = msg;
|
|
13507
15216
|
};
|
|
13508
|
-
api.on(
|
|
13509
|
-
|
|
13510
|
-
|
|
15217
|
+
api.on(
|
|
15218
|
+
"before_tool_call",
|
|
15219
|
+
createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir),
|
|
15220
|
+
{ priority: 100 }
|
|
15221
|
+
);
|
|
13511
15222
|
api.registerTool(createSageApproveTool(approvalStore));
|
|
15223
|
+
api.registerTool(
|
|
15224
|
+
createAllowlistAddTool(approvalStore, logger)
|
|
15225
|
+
);
|
|
15226
|
+
api.registerTool(createAllowlistRemoveTool(logger));
|
|
13512
15227
|
api.on("gateway_start", createStartupScanHandler(logger, onFindings));
|
|
13513
15228
|
api.on("session_start", createSessionScanHandler(logger, onFindings));
|
|
13514
15229
|
api.on(
|