@gendigital/sage-openclaw 0.4.4 → 0.5.3
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 +9 -7
- package/dist/index.cjs +2154 -496
- package/dist/index.cjs.map +4 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +18 -2
- 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");
|
|
@@ -11487,10 +11482,14 @@ var PackageCheckConfigSchema = external_exports.object({
|
|
|
11487
11482
|
// v1: all scoped packages (@scope/pkg) are skipped automatically.
|
|
11488
11483
|
// Future: add private_scopes / public_scopes config for fine-grained control.
|
|
11489
11484
|
});
|
|
11485
|
+
var AmsiCheckConfigSchema = external_exports.object({
|
|
11486
|
+
enabled: external_exports.boolean().default(true)
|
|
11487
|
+
});
|
|
11490
11488
|
var ConfigSchema = external_exports.object({
|
|
11491
11489
|
url_check: UrlCheckConfigSchema.default({}),
|
|
11492
11490
|
file_check: FileCheckConfigSchema.default({}),
|
|
11493
11491
|
package_check: PackageCheckConfigSchema.default({}),
|
|
11492
|
+
amsi_check: AmsiCheckConfigSchema.default({}),
|
|
11494
11493
|
heuristics_enabled: external_exports.boolean().default(true),
|
|
11495
11494
|
cache: CacheConfigSchema.default({}),
|
|
11496
11495
|
allowlist: AllowlistConfigSchema.default({}),
|
|
@@ -11500,37 +11499,112 @@ var ConfigSchema = external_exports.object({
|
|
|
11500
11499
|
});
|
|
11501
11500
|
|
|
11502
11501
|
// ../core/dist/config.js
|
|
11503
|
-
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
|
+
}
|
|
11504
11522
|
function resolvePath(pathStr) {
|
|
11505
11523
|
if (pathStr.startsWith("~/") || pathStr === "~") {
|
|
11506
|
-
return (0, import_node_path2.join)((
|
|
11524
|
+
return (0, import_node_path2.join)(currentHomeDir(), pathStr.slice(1));
|
|
11507
11525
|
}
|
|
11508
11526
|
return pathStr;
|
|
11509
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
|
+
}
|
|
11510
11584
|
async function loadConfig(configPath, logger = nullLogger) {
|
|
11511
|
-
const path = configPath ??
|
|
11585
|
+
const path = configPath ?? defaultConfigPath();
|
|
11512
11586
|
let raw;
|
|
11513
11587
|
try {
|
|
11514
11588
|
raw = await getFileContent(path);
|
|
11515
11589
|
} catch {
|
|
11516
|
-
return ConfigSchema.parse({});
|
|
11590
|
+
return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
|
|
11517
11591
|
}
|
|
11518
11592
|
let data;
|
|
11519
11593
|
try {
|
|
11520
11594
|
data = JSON.parse(raw);
|
|
11521
11595
|
} catch (e) {
|
|
11522
11596
|
logger.warn(`Failed to parse config from ${path}`, { error: String(e) });
|
|
11523
|
-
return ConfigSchema.parse({});
|
|
11597
|
+
return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
|
|
11524
11598
|
}
|
|
11525
11599
|
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
11526
11600
|
logger.warn(`Config file ${path} does not contain a JSON object`);
|
|
11527
|
-
return ConfigSchema.parse({});
|
|
11601
|
+
return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
|
|
11528
11602
|
}
|
|
11529
11603
|
try {
|
|
11530
|
-
return ConfigSchema.parse(data);
|
|
11604
|
+
return sanitizeConfigPaths(ConfigSchema.parse(data), logger);
|
|
11531
11605
|
} catch (e) {
|
|
11532
11606
|
logger.warn(`Config validation failed, using defaults`, { error: String(e) });
|
|
11533
|
-
return ConfigSchema.parse({});
|
|
11607
|
+
return sanitizeConfigPaths(ConfigSchema.parse({}), logger);
|
|
11534
11608
|
}
|
|
11535
11609
|
}
|
|
11536
11610
|
|
|
@@ -11560,6 +11634,16 @@ function normalizeFilePath(raw) {
|
|
|
11560
11634
|
function emptyAllowlist() {
|
|
11561
11635
|
return { urls: {}, commands: {}, filePaths: {} };
|
|
11562
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
|
+
}
|
|
11563
11647
|
function parseEntries(raw) {
|
|
11564
11648
|
const entries = {};
|
|
11565
11649
|
for (const [key, entryData] of Object.entries(raw)) {
|
|
@@ -11612,6 +11696,29 @@ async function loadAllowlist(config, logger = nullLogger) {
|
|
|
11612
11696
|
filePaths
|
|
11613
11697
|
};
|
|
11614
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
|
+
}
|
|
11615
11722
|
function isAllowlisted(allowlist, artifacts) {
|
|
11616
11723
|
for (const artifact of artifacts) {
|
|
11617
11724
|
if (artifact.type !== "command") {
|
|
@@ -11635,6 +11742,115 @@ function isAllowlisted(allowlist, artifacts) {
|
|
|
11635
11742
|
}
|
|
11636
11743
|
return false;
|
|
11637
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
|
+
};
|
|
11638
11854
|
|
|
11639
11855
|
// ../core/dist/audit-log.js
|
|
11640
11856
|
var import_promises = require("node:fs/promises");
|
|
@@ -11647,7 +11863,7 @@ function toolInputSummary(toolName, toolInput) {
|
|
|
11647
11863
|
if (toolName === "WebFetch") {
|
|
11648
11864
|
return String(toolInput.url ?? "").slice(0, MAX_SUMMARY_LEN);
|
|
11649
11865
|
}
|
|
11650
|
-
if (toolName === "Write" || toolName === "Edit") {
|
|
11866
|
+
if (toolName === "Write" || toolName === "Edit" || toolName === "Read" || toolName === "Delete") {
|
|
11651
11867
|
return String(toolInput.file_path ?? "").slice(0, MAX_SUMMARY_LEN);
|
|
11652
11868
|
}
|
|
11653
11869
|
return JSON.stringify(toolInput).slice(0, MAX_SUMMARY_LEN);
|
|
@@ -11864,8 +12080,483 @@ var VerdictCache = class {
|
|
|
11864
12080
|
}
|
|
11865
12081
|
};
|
|
11866
12082
|
|
|
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;
|
|
12093
|
+
}
|
|
12094
|
+
function isAmsiSupported() {
|
|
12095
|
+
return process.platform === "win32" || isWSL();
|
|
12096
|
+
}
|
|
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 {
|
|
12107
|
+
logger;
|
|
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) {
|
|
12118
|
+
this.logger = logger;
|
|
12119
|
+
}
|
|
12120
|
+
get isAvailable() {
|
|
12121
|
+
return this.available;
|
|
12122
|
+
}
|
|
12123
|
+
async init() {
|
|
12124
|
+
try {
|
|
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);
|
|
12149
|
+
}
|
|
12150
|
+
} catch {
|
|
12151
|
+
}
|
|
12152
|
+
this.context = null;
|
|
12153
|
+
return;
|
|
12154
|
+
}
|
|
12155
|
+
this.session = sessOut[0];
|
|
12156
|
+
this.available = true;
|
|
12157
|
+
this.logger.debug("AMSI: koffi backend initialized");
|
|
12158
|
+
} catch (e) {
|
|
12159
|
+
this.logger.debug("AMSI: koffi backend init failed", { error: String(e) });
|
|
12160
|
+
this.available = false;
|
|
12161
|
+
}
|
|
12162
|
+
}
|
|
12163
|
+
async scanString(content, contentName) {
|
|
12164
|
+
if (!this.available || !this.fnScanBuffer || !this.context || !this.session) {
|
|
12165
|
+
return null;
|
|
12166
|
+
}
|
|
12167
|
+
try {
|
|
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 });
|
|
12174
|
+
return null;
|
|
12175
|
+
}
|
|
12176
|
+
const amsiResult = resultOut[0] ?? 0;
|
|
12177
|
+
this.logger.debug("AMSI: koffi scan result", { contentName, amsiResult });
|
|
12178
|
+
return interpretAmsiResult(amsiResult, content, contentName);
|
|
12179
|
+
} catch (e) {
|
|
12180
|
+
this.logger.warn("AMSI: koffi scanBuffer failed", { error: String(e), contentName });
|
|
12181
|
+
return null;
|
|
12182
|
+
}
|
|
12183
|
+
}
|
|
12184
|
+
close() {
|
|
12185
|
+
try {
|
|
12186
|
+
if (this.session && this.context && this.fnCloseSession) {
|
|
12187
|
+
this.fnCloseSession(this.context, this.session);
|
|
12188
|
+
}
|
|
12189
|
+
} catch {
|
|
12190
|
+
}
|
|
12191
|
+
try {
|
|
12192
|
+
if (this.context && this.fnUninitialize) {
|
|
12193
|
+
this.fnUninitialize(this.context);
|
|
12194
|
+
}
|
|
12195
|
+
} catch {
|
|
12196
|
+
}
|
|
12197
|
+
this.session = null;
|
|
12198
|
+
this.context = null;
|
|
12199
|
+
this.available = false;
|
|
12200
|
+
}
|
|
12201
|
+
};
|
|
12202
|
+
var AMSI_CSHARP_TYPE = `
|
|
12203
|
+
using System;
|
|
12204
|
+
using System.Runtime.InteropServices;
|
|
12205
|
+
using System.Text;
|
|
12206
|
+
|
|
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
|
+
|
|
11867
12558
|
// ../core/dist/version.js
|
|
11868
|
-
var VERSION = true ? "0.
|
|
12559
|
+
var VERSION = true ? "0.5.2" : "dev";
|
|
11869
12560
|
|
|
11870
12561
|
// ../core/dist/clients/file-check.js
|
|
11871
12562
|
var DEFAULT_TIMEOUT = 5;
|
|
@@ -12000,32 +12691,190 @@ var FileCheckClient = class {
|
|
|
12000
12691
|
}
|
|
12001
12692
|
};
|
|
12002
12693
|
|
|
12003
|
-
// ../core/dist/clients/
|
|
12694
|
+
// ../core/dist/clients/package-registry.js
|
|
12695
|
+
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
12696
|
+
var PYPI_REGISTRY = "https://pypi.org/pypi";
|
|
12004
12697
|
var DEFAULT_TIMEOUT2 = 5;
|
|
12005
|
-
var
|
|
12006
|
-
var
|
|
12007
|
-
|
|
12008
|
-
return "com";
|
|
12009
|
-
}
|
|
12010
|
-
var REQUEST_HEADERS2 = [
|
|
12011
|
-
{ name: "Accept", value: "application/json" },
|
|
12012
|
-
{ name: "Content-Type", value: "application/json" },
|
|
12013
|
-
{ name: "User-Agent", value: SERVICE_NAME2 }
|
|
12014
|
-
];
|
|
12015
|
-
function getProviderName2() {
|
|
12016
|
-
return "avast";
|
|
12017
|
-
}
|
|
12018
|
-
var UrlCheckClient = class {
|
|
12019
|
-
endpoint;
|
|
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 {
|
|
12020
12701
|
timeoutMs;
|
|
12021
12702
|
logger;
|
|
12022
12703
|
constructor(config, logger = nullLogger) {
|
|
12023
|
-
this.endpoint = config?.endpoint ?? this.resolveEndpoint();
|
|
12024
12704
|
this.timeoutMs = (config?.timeout_seconds ?? DEFAULT_TIMEOUT2) * 1e3;
|
|
12025
12705
|
this.logger = logger;
|
|
12026
12706
|
}
|
|
12027
|
-
|
|
12028
|
-
|
|
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
|
+
];
|
|
12857
|
+
function getProviderName2() {
|
|
12858
|
+
return "avast";
|
|
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
|
+
}
|
|
12870
|
+
var UrlCheckClient = class {
|
|
12871
|
+
endpoint;
|
|
12872
|
+
timeoutMs;
|
|
12873
|
+
logger;
|
|
12874
|
+
constructor(config, logger = nullLogger) {
|
|
12875
|
+
this.endpoint = config?.endpoint ?? resolveEndpoint("/url-check");
|
|
12876
|
+
this.timeoutMs = (config?.timeout_seconds ?? DEFAULT_TIMEOUT3) * 1e3;
|
|
12877
|
+
this.logger = logger;
|
|
12029
12878
|
}
|
|
12030
12879
|
async checkUrls(urls) {
|
|
12031
12880
|
if (urls.length === 0)
|
|
@@ -12037,9 +12886,6 @@ var UrlCheckClient = class {
|
|
|
12037
12886
|
const batchResults = await Promise.all(batches.map((batch) => this.checkBatch(batch)));
|
|
12038
12887
|
return batchResults.flat();
|
|
12039
12888
|
}
|
|
12040
|
-
getSubdomain() {
|
|
12041
|
-
return "svc";
|
|
12042
|
-
}
|
|
12043
12889
|
async checkBatch(urls) {
|
|
12044
12890
|
const queries = urls.map((url) => ({ key: { "url-like": url } }));
|
|
12045
12891
|
const payload = {
|
|
@@ -12075,9 +12921,6 @@ var UrlCheckClient = class {
|
|
|
12075
12921
|
return [];
|
|
12076
12922
|
}
|
|
12077
12923
|
}
|
|
12078
|
-
buildDomain() {
|
|
12079
|
-
return [this.getSubdomain(), getProviderName2(), getProviderTld2()].join(".");
|
|
12080
|
-
}
|
|
12081
12924
|
parseAnswer(answer) {
|
|
12082
12925
|
try {
|
|
12083
12926
|
const url = answer.key ?? "";
|
|
@@ -12102,9 +12945,6 @@ var UrlCheckClient = class {
|
|
|
12102
12945
|
return null;
|
|
12103
12946
|
}
|
|
12104
12947
|
}
|
|
12105
|
-
resolveEndpoint() {
|
|
12106
|
-
return `https://${SERVICE_NAME2}-proxy.${this.buildDomain()}${this.buildPath()}`;
|
|
12107
|
-
}
|
|
12108
12948
|
};
|
|
12109
12949
|
|
|
12110
12950
|
// ../core/dist/engine.js
|
|
@@ -12135,17 +12975,8 @@ var DecisionEngine = class {
|
|
|
12135
12975
|
constructor(sensitivity = "balanced") {
|
|
12136
12976
|
this.threshold = SENSITIVITY_THRESHOLDS[sensitivity] ?? CONFIDENCE_THRESHOLD;
|
|
12137
12977
|
}
|
|
12138
|
-
async decide(
|
|
12139
|
-
|
|
12140
|
-
if (Array.isArray(heuristicMatchesOrSources)) {
|
|
12141
|
-
sources = {
|
|
12142
|
-
heuristicMatches: heuristicMatchesOrSources,
|
|
12143
|
-
urlCheckResults: urlCheckResults ?? []
|
|
12144
|
-
};
|
|
12145
|
-
} else {
|
|
12146
|
-
sources = heuristicMatchesOrSources;
|
|
12147
|
-
}
|
|
12148
|
-
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);
|
|
12149
12980
|
if (signals.length === 0) {
|
|
12150
12981
|
return this.allowVerdict();
|
|
12151
12982
|
}
|
|
@@ -12172,7 +13003,7 @@ var DecisionEngine = class {
|
|
|
12172
13003
|
reasons: allReasons
|
|
12173
13004
|
};
|
|
12174
13005
|
}
|
|
12175
|
-
collectSignals(heuristicMatches, urlCheckResults, packageCheckResults) {
|
|
13006
|
+
collectSignals(heuristicMatches, urlCheckResults, packageCheckResults, amsiCheckResults) {
|
|
12176
13007
|
const signals = [];
|
|
12177
13008
|
for (const match of heuristicMatches) {
|
|
12178
13009
|
signals.push({
|
|
@@ -12222,6 +13053,33 @@ var DecisionEngine = class {
|
|
|
12222
13053
|
signals.push(signal);
|
|
12223
13054
|
}
|
|
12224
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
|
+
}
|
|
12225
13083
|
return signals;
|
|
12226
13084
|
}
|
|
12227
13085
|
packageVerdictToSignal(pkg) {
|
|
@@ -12511,6 +13369,459 @@ var HeuristicsEngine = class {
|
|
|
12511
13369
|
}
|
|
12512
13370
|
};
|
|
12513
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;
|
|
13642
|
+
continue;
|
|
13643
|
+
}
|
|
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("-"))
|
|
13675
|
+
continue;
|
|
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("-"))
|
|
13689
|
+
continue;
|
|
13690
|
+
if (!isPackageName(t))
|
|
13691
|
+
break;
|
|
13692
|
+
const { name, version } = splitNameVersion(t);
|
|
13693
|
+
packages.push({ name, version, registry: "npm", source: "command" });
|
|
13694
|
+
break;
|
|
13695
|
+
}
|
|
13696
|
+
return packages;
|
|
13697
|
+
}
|
|
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" });
|
|
13708
|
+
}
|
|
13709
|
+
return packages;
|
|
13710
|
+
}
|
|
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" });
|
|
13724
|
+
}
|
|
13725
|
+
return packages;
|
|
13726
|
+
}
|
|
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
|
+
});
|
|
13770
|
+
}
|
|
13771
|
+
}
|
|
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
|
+
});
|
|
13793
|
+
}
|
|
13794
|
+
} catch {
|
|
13795
|
+
}
|
|
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;
|
|
13805
|
+
}
|
|
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
|
+
});
|
|
13820
|
+
}
|
|
13821
|
+
}
|
|
13822
|
+
return packages;
|
|
13823
|
+
}
|
|
13824
|
+
|
|
12514
13825
|
// ../core/dist/threat-loader.js
|
|
12515
13826
|
var import_promises3 = require("node:fs/promises");
|
|
12516
13827
|
var import_node_path6 = require("node:path");
|
|
@@ -12617,32 +13928,283 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
12617
13928
|
return threats;
|
|
12618
13929
|
}
|
|
12619
13930
|
|
|
12620
|
-
// ../core/dist/
|
|
12621
|
-
|
|
12622
|
-
|
|
12623
|
-
|
|
12624
|
-
|
|
12625
|
-
|
|
12626
|
-
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
|
|
12630
|
-
|
|
12631
|
-
|
|
12632
|
-
return ` ${key.padEnd(PAD)}${value}`;
|
|
12633
|
-
}
|
|
12634
|
-
function separatorLine(headerLength) {
|
|
12635
|
-
return "\u2501".repeat(headerLength);
|
|
12636
|
-
}
|
|
12637
|
-
function formatUpdateNotice(result) {
|
|
12638
|
-
return `\u2B06\uFE0F Update available: v${result.currentVersion} \u2192 v${result.latestVersion} (https://github.com/avast/sage)`;
|
|
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
|
+
};
|
|
12639
13943
|
}
|
|
12640
|
-
function
|
|
12641
|
-
const
|
|
12642
|
-
const
|
|
12643
|
-
|
|
12644
|
-
|
|
12645
|
-
|
|
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
|
+
|
|
14174
|
+
// ../core/dist/format.js
|
|
14175
|
+
var PAD = 12;
|
|
14176
|
+
var SEPARATOR_WIDTH = 48;
|
|
14177
|
+
function severityEmoji(severity) {
|
|
14178
|
+
const s = severity.toLowerCase();
|
|
14179
|
+
if (s === "critical" || s === "high")
|
|
14180
|
+
return "\u{1F6A8}";
|
|
14181
|
+
if (s === "medium" || s === "warn" || s === "warning")
|
|
14182
|
+
return "\u26A0\uFE0F";
|
|
14183
|
+
return "\u2139\uFE0F";
|
|
14184
|
+
}
|
|
14185
|
+
function kv(key, value) {
|
|
14186
|
+
return ` ${key.padEnd(PAD)}${value}`;
|
|
14187
|
+
}
|
|
14188
|
+
function separatorLine(headerLength) {
|
|
14189
|
+
return "\u2501".repeat(headerLength);
|
|
14190
|
+
}
|
|
14191
|
+
function formatUpdateNotice(result) {
|
|
14192
|
+
return `\u2B06\uFE0F Update available: v${result.currentVersion} \u2192 v${result.latestVersion} (https://github.com/avast/sage)`;
|
|
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
|
+
}
|
|
14202
|
+
function formatThreatBanner(version, results, versionCheck) {
|
|
14203
|
+
const header = `\u{1F6E1}\uFE0F Sage v${version} by Gen Digital \u2014 Threat Detected`;
|
|
14204
|
+
const lines = [" ", header, separatorLine(SEPARATOR_WIDTH)];
|
|
14205
|
+
const MAX_FINDINGS = 5;
|
|
14206
|
+
let first = true;
|
|
14207
|
+
for (const result of results) {
|
|
12646
14208
|
const highCrit = result.findings.filter((f) => f.severity === "critical" || f.severity === "high");
|
|
12647
14209
|
if (highCrit.length === 0)
|
|
12648
14210
|
continue;
|
|
@@ -12673,31 +14235,203 @@ function formatThreatBanner(version, results, versionCheck) {
|
|
|
12673
14235
|
}
|
|
12674
14236
|
return lines.join("\n");
|
|
12675
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
|
+
}
|
|
12676
14244
|
|
|
12677
|
-
// ../core/dist/
|
|
12678
|
-
|
|
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");
|
|
12679
14379
|
var import_promises4 = require("node:fs/promises");
|
|
12680
|
-
var import_node_os3 = require("node:os");
|
|
12681
14380
|
var import_node_path7 = require("node:path");
|
|
12682
|
-
|
|
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");
|
|
12683
14417
|
var CACHE_TTL_DAYS = 7;
|
|
12684
14418
|
function cacheKey(pluginKey, version, lastUpdated) {
|
|
12685
14419
|
return `${pluginKey}:${version}:${lastUpdated}`;
|
|
12686
14420
|
}
|
|
12687
14421
|
async function computeConfigHash(sageVersion, ...dirs) {
|
|
12688
|
-
const h = (0,
|
|
14422
|
+
const h = (0, import_node_crypto5.createHash)("sha256");
|
|
12689
14423
|
if (sageVersion)
|
|
12690
14424
|
h.update(sageVersion);
|
|
12691
14425
|
for (const dir of dirs) {
|
|
12692
14426
|
let files;
|
|
12693
14427
|
try {
|
|
12694
|
-
files = (await (0,
|
|
14428
|
+
files = (await (0, import_promises5.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
|
|
12695
14429
|
} catch {
|
|
12696
14430
|
continue;
|
|
12697
14431
|
}
|
|
12698
14432
|
for (const file of files) {
|
|
12699
14433
|
try {
|
|
12700
|
-
const content = await getFileContent((0,
|
|
14434
|
+
const content = await getFileContent((0, import_node_path8.join)(dir, file));
|
|
12701
14435
|
h.update(content);
|
|
12702
14436
|
} catch {
|
|
12703
14437
|
}
|
|
@@ -12787,11 +14521,11 @@ function storeResult(cache, pluginKey, version, lastUpdated, findings) {
|
|
|
12787
14521
|
}
|
|
12788
14522
|
|
|
12789
14523
|
// ../core/dist/plugin-scanner.js
|
|
12790
|
-
var
|
|
12791
|
-
var
|
|
14524
|
+
var import_node_crypto6 = require("node:crypto");
|
|
14525
|
+
var import_promises6 = require("node:fs/promises");
|
|
12792
14526
|
var import_node_os4 = require("node:os");
|
|
12793
|
-
var
|
|
12794
|
-
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");
|
|
12795
14529
|
var SCANNABLE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
12796
14530
|
".py",
|
|
12797
14531
|
".js",
|
|
@@ -12813,38 +14547,38 @@ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"])
|
|
|
12813
14547
|
var MAX_FILE_SIZE = 512 * 1024;
|
|
12814
14548
|
async function walkPluginFiles(installPath, logger) {
|
|
12815
14549
|
const files = [];
|
|
12816
|
-
async function walk(
|
|
12817
|
-
let
|
|
14550
|
+
async function walk(dirOrFile) {
|
|
14551
|
+
let stats;
|
|
12818
14552
|
try {
|
|
12819
|
-
|
|
14553
|
+
stats = await (0, import_promises6.stat)(dirOrFile);
|
|
12820
14554
|
} catch {
|
|
12821
14555
|
return;
|
|
12822
14556
|
}
|
|
12823
|
-
|
|
12824
|
-
if (
|
|
12825
|
-
|
|
12826
|
-
|
|
12827
|
-
|
|
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;
|
|
12828
14565
|
try {
|
|
12829
|
-
|
|
14566
|
+
entries = await (0, import_promises6.readdir)(dirOrFile);
|
|
12830
14567
|
} catch {
|
|
12831
|
-
|
|
14568
|
+
return;
|
|
12832
14569
|
}
|
|
12833
|
-
|
|
12834
|
-
|
|
12835
|
-
} else if (stats.isFile()) {
|
|
12836
|
-
if (!SCANNABLE_EXTENSIONS.has((0, import_node_path8.extname)(fullPath).toLowerCase()))
|
|
14570
|
+
for (const entry of entries) {
|
|
14571
|
+
if (SKIP_DIRS.has(entry))
|
|
12837
14572
|
continue;
|
|
12838
|
-
|
|
12839
|
-
|
|
12840
|
-
files.push(fullPath);
|
|
14573
|
+
const fullPath = (0, import_node_path9.join)(dirOrFile, entry);
|
|
14574
|
+
await walk(fullPath);
|
|
12841
14575
|
}
|
|
12842
14576
|
}
|
|
12843
14577
|
}
|
|
12844
14578
|
try {
|
|
12845
14579
|
await walk(installPath);
|
|
12846
14580
|
} catch (e) {
|
|
12847
|
-
logger.warn(`Error walking plugin
|
|
14581
|
+
logger.warn(`Error walking plugin path ${installPath}`, { error: String(e) });
|
|
12848
14582
|
}
|
|
12849
14583
|
return files;
|
|
12850
14584
|
}
|
|
@@ -12862,7 +14596,7 @@ function extractArtifactsFromFile(filePath, content) {
|
|
|
12862
14596
|
continue;
|
|
12863
14597
|
artifacts.push({ type: "url", value: url, context: `plugin_file:${fileName}` });
|
|
12864
14598
|
}
|
|
12865
|
-
const ext = (0,
|
|
14599
|
+
const ext = (0, import_node_path9.extname)(filePath).toLowerCase();
|
|
12866
14600
|
if ([".sh", ".bash", ".zsh", ".py"].includes(ext)) {
|
|
12867
14601
|
for (const line of content.split("\n")) {
|
|
12868
14602
|
const trimmed = line.trim();
|
|
@@ -12907,7 +14641,7 @@ async function scanPlugin(plugin, threats, options = {}) {
|
|
|
12907
14641
|
confidence: match.threat.confidence,
|
|
12908
14642
|
action: match.threat.action,
|
|
12909
14643
|
artifact: match.artifact.slice(0, 200),
|
|
12910
|
-
sourceFile: (0,
|
|
14644
|
+
sourceFile: (0, import_node_path9.relative)(plugin.installPath, filePath)
|
|
12911
14645
|
});
|
|
12912
14646
|
}
|
|
12913
14647
|
}
|
|
@@ -12915,7 +14649,7 @@ async function scanPlugin(plugin, threats, options = {}) {
|
|
|
12915
14649
|
allUrls.push(...extractUrls(content));
|
|
12916
14650
|
}
|
|
12917
14651
|
if (checkFileHashes) {
|
|
12918
|
-
const sha256 = (0,
|
|
14652
|
+
const sha256 = (0, import_node_crypto6.createHash)("sha256").update(rawBytes).digest("hex");
|
|
12919
14653
|
const existing = hashToFiles.get(sha256);
|
|
12920
14654
|
if (existing) {
|
|
12921
14655
|
existing.push(filePath);
|
|
@@ -12962,7 +14696,7 @@ async function scanPlugin(plugin, threats, options = {}) {
|
|
|
12962
14696
|
confidence: 1,
|
|
12963
14697
|
action: "block",
|
|
12964
14698
|
artifact: fr.sha256,
|
|
12965
|
-
sourceFile: (0,
|
|
14699
|
+
sourceFile: (0, import_node_path9.relative)(plugin.installPath, filePath)
|
|
12966
14700
|
});
|
|
12967
14701
|
}
|
|
12968
14702
|
}
|
|
@@ -12998,18 +14732,63 @@ function toFindingData(finding) {
|
|
|
12998
14732
|
};
|
|
12999
14733
|
}
|
|
13000
14734
|
function toAuditFindingData(finding) {
|
|
13001
|
-
|
|
13002
|
-
|
|
13003
|
-
|
|
13004
|
-
|
|
13005
|
-
|
|
13006
|
-
|
|
13007
|
-
|
|
13008
|
-
|
|
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;
|
|
13009
14788
|
}
|
|
13010
14789
|
|
|
13011
14790
|
// ../core/dist/version-check.js
|
|
13012
|
-
var
|
|
14791
|
+
var import_node_os5 = require("node:os");
|
|
13013
14792
|
var DEFAULT_TIMEOUT_MS = 5e3;
|
|
13014
14793
|
function isNewerVersion(current, latest) {
|
|
13015
14794
|
const parse = (v) => v.replace(/^v/, "").split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
@@ -13025,19 +14804,29 @@ function isNewerVersion(current, latest) {
|
|
|
13025
14804
|
}
|
|
13026
14805
|
return false;
|
|
13027
14806
|
}
|
|
13028
|
-
async function checkForUpdate(currentVersion, logger = nullLogger, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
14807
|
+
async function checkForUpdate(currentVersion, logger = nullLogger, timeoutMs = DEFAULT_TIMEOUT_MS, context) {
|
|
13029
14808
|
if (currentVersion === "dev") {
|
|
13030
14809
|
logger.debug("Skipping version check for dev build");
|
|
13031
14810
|
return null;
|
|
13032
14811
|
}
|
|
13033
14812
|
try {
|
|
13034
|
-
const
|
|
13035
|
-
|
|
13036
|
-
|
|
13037
|
-
|
|
13038
|
-
|
|
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
|
+
})
|
|
13039
14829
|
});
|
|
13040
|
-
clearTimeout(timer);
|
|
13041
14830
|
if (!response.ok) {
|
|
13042
14831
|
logger.debug(`Version check HTTP ${response.status}`);
|
|
13043
14832
|
return null;
|
|
@@ -13059,61 +14848,147 @@ async function checkForUpdate(currentVersion, logger = nullLogger, timeoutMs = D
|
|
|
13059
14848
|
}
|
|
13060
14849
|
}
|
|
13061
14850
|
|
|
13062
|
-
//
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
|
|
13067
|
-
|
|
13068
|
-
|
|
13069
|
-
|
|
13070
|
-
|
|
13071
|
-
|
|
13072
|
-
|
|
13073
|
-
|
|
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 () => {
|
|
13074
14899
|
try {
|
|
13075
|
-
|
|
13076
|
-
|
|
13077
|
-
|
|
13078
|
-
|
|
13079
|
-
for (const [id, entry] of Object.entries(parsed)) {
|
|
13080
|
-
if (entry && new Date(entry.expiresAt).getTime() > now) {
|
|
13081
|
-
this.data[id] = entry;
|
|
13082
|
-
}
|
|
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`);
|
|
13083
14904
|
}
|
|
13084
|
-
|
|
13085
|
-
|
|
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) });
|
|
13086
14909
|
}
|
|
13087
|
-
}
|
|
13088
|
-
|
|
13089
|
-
|
|
13090
|
-
|
|
13091
|
-
|
|
13092
|
-
|
|
13093
|
-
|
|
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 }] };
|
|
13094
14941
|
}
|
|
13095
|
-
|
|
13096
|
-
|
|
13097
|
-
|
|
13098
|
-
|
|
13099
|
-
|
|
13100
|
-
|
|
13101
|
-
|
|
13102
|
-
|
|
13103
|
-
|
|
13104
|
-
|
|
13105
|
-
|
|
13106
|
-
|
|
13107
|
-
|
|
13108
|
-
|
|
13109
|
-
|
|
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 }] };
|
|
13110
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";
|
|
13111
14989
|
}
|
|
13112
|
-
|
|
13113
|
-
|
|
13114
|
-
return (0, import_node_crypto5.createHash)("sha256").update(payload).digest("hex");
|
|
13115
|
-
}
|
|
13116
|
-
};
|
|
14990
|
+
return cachedVersion;
|
|
14991
|
+
}
|
|
13117
14992
|
|
|
13118
14993
|
// src/gate-tool.ts
|
|
13119
14994
|
function createSageApproveTool(approvalStore) {
|
|
@@ -13135,14 +15010,15 @@ function createSageApproveTool(approvalStore) {
|
|
|
13135
15010
|
required: ["actionId", "approved"]
|
|
13136
15011
|
},
|
|
13137
15012
|
async execute(_toolCallId, params) {
|
|
13138
|
-
if (params.approved) {
|
|
13139
|
-
|
|
15013
|
+
if (!params.approved) {
|
|
15014
|
+
approvalStore.deletePending(params.actionId);
|
|
13140
15015
|
return {
|
|
13141
|
-
content: [{ type: "text", text: "
|
|
15016
|
+
content: [{ type: "text", text: "Rejected by user." }]
|
|
13142
15017
|
};
|
|
13143
15018
|
}
|
|
15019
|
+
const msg = await approveAction(approvalStore, params.actionId);
|
|
13144
15020
|
return {
|
|
13145
|
-
content: [{ type: "text", text:
|
|
15021
|
+
content: [{ type: "text", text: msg }]
|
|
13146
15022
|
};
|
|
13147
15023
|
}
|
|
13148
15024
|
};
|
|
@@ -13169,30 +15045,8 @@ function createLogger(pluginLogger) {
|
|
|
13169
15045
|
};
|
|
13170
15046
|
}
|
|
13171
15047
|
|
|
13172
|
-
// src/bundled-dirs.ts
|
|
13173
|
-
var import_node_fs = require("node:fs");
|
|
13174
|
-
var import_node_path10 = require("node:path");
|
|
13175
|
-
var packageRoot = (0, import_node_path10.resolve)(__dirname, "..");
|
|
13176
|
-
function getBundledDataDirs() {
|
|
13177
|
-
return {
|
|
13178
|
-
threatsDir: (0, import_node_path10.join)(packageRoot, "resources", "threats"),
|
|
13179
|
-
allowlistsDir: (0, import_node_path10.join)(packageRoot, "resources", "allowlists")
|
|
13180
|
-
};
|
|
13181
|
-
}
|
|
13182
|
-
var cachedVersion;
|
|
13183
|
-
function getSageVersion() {
|
|
13184
|
-
if (cachedVersion) return cachedVersion;
|
|
13185
|
-
try {
|
|
13186
|
-
const pkg = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path10.join)(packageRoot, "package.json"), "utf-8"));
|
|
13187
|
-
cachedVersion = pkg.version ?? "0.0.0";
|
|
13188
|
-
} catch {
|
|
13189
|
-
cachedVersion = "0.0.0";
|
|
13190
|
-
}
|
|
13191
|
-
return cachedVersion;
|
|
13192
|
-
}
|
|
13193
|
-
|
|
13194
15048
|
// src/plugin-discovery.ts
|
|
13195
|
-
var
|
|
15049
|
+
var import_promises7 = require("node:fs/promises");
|
|
13196
15050
|
var import_node_os6 = require("node:os");
|
|
13197
15051
|
var import_node_path11 = require("node:path");
|
|
13198
15052
|
var DEFAULT_EXTENSIONS_DIR = (0, import_node_path11.join)((0, import_node_os6.homedir)(), ".openclaw", "extensions");
|
|
@@ -13200,7 +15054,7 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
|
|
|
13200
15054
|
logger.info("Sage plugin discovery: scanning extensions directory", { path: extensionsDir });
|
|
13201
15055
|
let entries;
|
|
13202
15056
|
try {
|
|
13203
|
-
entries = await (0,
|
|
15057
|
+
entries = await (0, import_promises7.readdir)(extensionsDir);
|
|
13204
15058
|
} catch {
|
|
13205
15059
|
logger.debug("OpenClaw extensions directory not found", { path: extensionsDir });
|
|
13206
15060
|
return [];
|
|
@@ -13210,7 +15064,7 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
|
|
|
13210
15064
|
const extDir = (0, import_node_path11.join)(extensionsDir, entry);
|
|
13211
15065
|
let stats;
|
|
13212
15066
|
try {
|
|
13213
|
-
stats = await (0,
|
|
15067
|
+
stats = await (0, import_promises7.stat)(extDir);
|
|
13214
15068
|
if (!stats.isDirectory()) continue;
|
|
13215
15069
|
} catch {
|
|
13216
15070
|
continue;
|
|
@@ -13240,111 +15094,26 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
|
|
|
13240
15094
|
}
|
|
13241
15095
|
|
|
13242
15096
|
// src/startup-scan.ts
|
|
13243
|
-
|
|
13244
|
-
async function runScan(logger, context) {
|
|
13245
|
-
await pruneOrphanedTmpFiles(resolvePath("~/.sage"));
|
|
15097
|
+
function createOpenClawScanHandler(logger, context, onResult) {
|
|
13246
15098
|
const { threatsDir, allowlistsDir } = getBundledDataDirs();
|
|
13247
15099
|
const version = getSageVersion();
|
|
13248
|
-
|
|
13249
|
-
|
|
13250
|
-
|
|
13251
|
-
|
|
13252
|
-
|
|
13253
|
-
|
|
13254
|
-
|
|
13255
|
-
|
|
13256
|
-
|
|
13257
|
-
|
|
13258
|
-
}
|
|
13259
|
-
logger.info(`Sage plugin scan (${context}): loaded ${threats.length} threat definitions`);
|
|
13260
|
-
let plugins = await discoverOpenClawPlugins(logger);
|
|
13261
|
-
plugins = plugins.filter((p) => !p.key.startsWith(`${SAGE_PLUGIN_ID}@`));
|
|
13262
|
-
if (plugins.length === 0) {
|
|
13263
|
-
logger.warn(`Sage plugin scan (${context}): no plugins to scan after filtering`);
|
|
13264
|
-
return updateNotice;
|
|
13265
|
-
}
|
|
13266
|
-
logger.info(`Sage plugin scan (${context}): ${plugins.length} plugin(s) to scan`, {
|
|
13267
|
-
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
|
|
13268
15110
|
});
|
|
13269
|
-
const configHash = await computeConfigHash("", threatsDir, allowlistsDir);
|
|
13270
|
-
const cache = await loadScanCache(configHash, void 0, logger);
|
|
13271
|
-
const resultsWithFindings = [];
|
|
13272
|
-
let cacheModified = false;
|
|
13273
|
-
let scannedCount = 0;
|
|
13274
|
-
for (const plugin of plugins) {
|
|
13275
|
-
const cached = getCached(cache, plugin.key, plugin.version, plugin.lastUpdated);
|
|
13276
|
-
if (cached && cached.findings.length === 0) {
|
|
13277
|
-
logger.info(`Sage plugin scan (${context}): cache hit (clean) for ${plugin.key}`);
|
|
13278
|
-
continue;
|
|
13279
|
-
}
|
|
13280
|
-
if (cached && cached.findings.length > 0) {
|
|
13281
|
-
logger.info(
|
|
13282
|
-
`Sage plugin scan (${context}): cache hit with ${cached.findings.length} finding(s) for ${plugin.key}`
|
|
13283
|
-
);
|
|
13284
|
-
resultsWithFindings.push({ plugin, findings: cached.findings.map(fromCachedFinding) });
|
|
13285
|
-
continue;
|
|
13286
|
-
}
|
|
13287
|
-
logger.info(`Sage plugin scan (${context}): scanning ${plugin.key}`);
|
|
13288
|
-
const result = await scanPlugin(plugin, threats, {
|
|
13289
|
-
checkUrls: true,
|
|
13290
|
-
trustedDomains,
|
|
13291
|
-
logger
|
|
13292
|
-
});
|
|
13293
|
-
scannedCount++;
|
|
13294
|
-
storeResult(
|
|
13295
|
-
cache,
|
|
13296
|
-
plugin.key,
|
|
13297
|
-
plugin.version,
|
|
13298
|
-
plugin.lastUpdated,
|
|
13299
|
-
result.findings.map(toFindingData)
|
|
13300
|
-
);
|
|
13301
|
-
cacheModified = true;
|
|
13302
|
-
if (result.findings.length > 0) {
|
|
13303
|
-
resultsWithFindings.push(result);
|
|
13304
|
-
}
|
|
13305
|
-
}
|
|
13306
|
-
if (cacheModified) {
|
|
13307
|
-
await saveScanCache(cache, void 0, logger);
|
|
13308
|
-
}
|
|
13309
|
-
logger.info(
|
|
13310
|
-
`Sage plugin scan (${context}) complete: ${scannedCount} scanned, ${resultsWithFindings.length} with findings, cache ${cacheModified ? "updated" : "unchanged"}`
|
|
13311
|
-
);
|
|
13312
|
-
try {
|
|
13313
|
-
const sageConfig = await loadConfig(void 0, logger);
|
|
13314
|
-
for (const result of resultsWithFindings) {
|
|
13315
|
-
await logPluginScan(
|
|
13316
|
-
sageConfig.logging,
|
|
13317
|
-
result.plugin.key,
|
|
13318
|
-
result.plugin.version,
|
|
13319
|
-
result.findings.map(toAuditFindingData)
|
|
13320
|
-
);
|
|
13321
|
-
}
|
|
13322
|
-
} catch {
|
|
13323
|
-
}
|
|
13324
|
-
if (resultsWithFindings.length > 0) {
|
|
13325
|
-
const banner = formatThreatBanner(version, resultsWithFindings, versionCheck);
|
|
13326
|
-
logger.warn(`Sage: threat findings detected`, {
|
|
13327
|
-
plugins: resultsWithFindings.map((r) => r.plugin.key)
|
|
13328
|
-
});
|
|
13329
|
-
return banner;
|
|
13330
|
-
}
|
|
13331
|
-
return updateNotice;
|
|
13332
15111
|
}
|
|
13333
|
-
function
|
|
13334
|
-
return
|
|
13335
|
-
try {
|
|
13336
|
-
const findings = await runScan(logger, context);
|
|
13337
|
-
onFindings?.(findings);
|
|
13338
|
-
} catch (e) {
|
|
13339
|
-
logger.error(`Sage ${context} scan failed`, { error: String(e) });
|
|
13340
|
-
}
|
|
13341
|
-
};
|
|
15112
|
+
function createStartupScanHandler(logger, onResult) {
|
|
15113
|
+
return createOpenClawScanHandler(logger, "startup", onResult);
|
|
13342
15114
|
}
|
|
13343
|
-
function
|
|
13344
|
-
return
|
|
13345
|
-
}
|
|
13346
|
-
function createSessionScanHandler(logger, onFindings) {
|
|
13347
|
-
return createScanHandler(logger, "session", onFindings);
|
|
15115
|
+
function createSessionScanHandler(logger, onResult) {
|
|
15116
|
+
return createOpenClawScanHandler(logger, "session", onResult);
|
|
13348
15117
|
}
|
|
13349
15118
|
function createBeforeAgentStartHandler(getFindings, clearFindings, logger) {
|
|
13350
15119
|
return () => {
|
|
@@ -13398,153 +15167,32 @@ function mapToolToArtifacts(toolName, params) {
|
|
|
13398
15167
|
return null;
|
|
13399
15168
|
}
|
|
13400
15169
|
}
|
|
13401
|
-
function createToolCallHandler(approvalStore, logger,
|
|
13402
|
-
|
|
15170
|
+
function createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir) {
|
|
15171
|
+
async function handleToolCall(event, ctx) {
|
|
13403
15172
|
try {
|
|
13404
15173
|
const { toolName, params } = event;
|
|
13405
15174
|
const artifacts = mapToolToArtifacts(toolName, params);
|
|
13406
15175
|
if (!artifacts || artifacts.length === 0) return void 0;
|
|
13407
|
-
const
|
|
13408
|
-
|
|
13409
|
-
|
|
13410
|
-
|
|
15176
|
+
const sessionId = ctx?.sessionKey ?? "unknown";
|
|
15177
|
+
const { verdict, actionId } = await guardToolCall(
|
|
15178
|
+
{ sessionId, toolName, toolInput: params, artifacts },
|
|
15179
|
+
{ threatsDir, allowlistsDir, logger },
|
|
15180
|
+
approvalStore
|
|
13411
15181
|
);
|
|
13412
|
-
try {
|
|
13413
|
-
const allowlist = await loadAllowlist(config.allowlist, logger);
|
|
13414
|
-
if (isAllowlisted(allowlist, artifacts)) {
|
|
13415
|
-
await logVerdict(
|
|
13416
|
-
config.logging,
|
|
13417
|
-
sessionKey,
|
|
13418
|
-
toolName,
|
|
13419
|
-
params,
|
|
13420
|
-
{
|
|
13421
|
-
decision: "allow",
|
|
13422
|
-
category: "none",
|
|
13423
|
-
confidence: 1,
|
|
13424
|
-
severity: "info",
|
|
13425
|
-
source: "allowlisted",
|
|
13426
|
-
artifacts: [],
|
|
13427
|
-
matchedThreatId: null,
|
|
13428
|
-
reasons: []
|
|
13429
|
-
},
|
|
13430
|
-
true
|
|
13431
|
-
);
|
|
13432
|
-
return void 0;
|
|
13433
|
-
}
|
|
13434
|
-
} catch {
|
|
13435
|
-
}
|
|
13436
|
-
let cache = null;
|
|
13437
|
-
try {
|
|
13438
|
-
cache = new VerdictCache(config.cache, logger);
|
|
13439
|
-
await cache.load();
|
|
13440
|
-
} catch {
|
|
13441
|
-
cache = null;
|
|
13442
|
-
}
|
|
13443
|
-
const urls = artifacts.filter((a) => a.type === "url").map((a) => a.value);
|
|
13444
|
-
const cachedUrlVerdicts = /* @__PURE__ */ new Map();
|
|
13445
|
-
let uncachedUrls = [];
|
|
13446
|
-
if (cache && urls.length > 0) {
|
|
13447
|
-
try {
|
|
13448
|
-
for (const url of urls) {
|
|
13449
|
-
const cached = cache.getUrl(url);
|
|
13450
|
-
if (cached) {
|
|
13451
|
-
cachedUrlVerdicts.set(url, cached);
|
|
13452
|
-
} else {
|
|
13453
|
-
uncachedUrls.push(url);
|
|
13454
|
-
}
|
|
13455
|
-
}
|
|
13456
|
-
} catch {
|
|
13457
|
-
uncachedUrls = urls;
|
|
13458
|
-
}
|
|
13459
|
-
}
|
|
13460
|
-
const { threatsDir, allowlistsDir } = getBundledDataDirs();
|
|
13461
|
-
let heuristicMatches = [];
|
|
13462
|
-
if (config.heuristics_enabled) {
|
|
13463
|
-
const threats = await loadThreats(threatsDir, logger);
|
|
13464
|
-
const trustedDomains = await loadTrustedDomains(allowlistsDir, logger);
|
|
13465
|
-
const heuristics = new HeuristicsEngine(threats, trustedDomains);
|
|
13466
|
-
heuristicMatches = heuristics.match(artifacts);
|
|
13467
|
-
}
|
|
13468
|
-
let urlCheckResults = [];
|
|
13469
|
-
const urlsToCheck = cache ? uncachedUrls : urls;
|
|
13470
|
-
if (urlsToCheck.length > 0 && config.url_check.enabled) {
|
|
13471
|
-
try {
|
|
13472
|
-
const client = new UrlCheckClient(config.url_check, logger);
|
|
13473
|
-
urlCheckResults = await client.checkUrls(urlsToCheck);
|
|
13474
|
-
} catch {
|
|
13475
|
-
}
|
|
13476
|
-
}
|
|
13477
|
-
const engine = new DecisionEngine(config.sensitivity);
|
|
13478
|
-
let verdict = await engine.decide(heuristicMatches, urlCheckResults);
|
|
13479
|
-
if (cachedUrlVerdicts.size > 0 && verdict.decision === "allow") {
|
|
13480
|
-
for (const [url, cv] of cachedUrlVerdicts) {
|
|
13481
|
-
if (cv.verdict !== "allow") {
|
|
13482
|
-
verdict = {
|
|
13483
|
-
decision: cv.verdict,
|
|
13484
|
-
category: "network_egress",
|
|
13485
|
-
confidence: 1,
|
|
13486
|
-
severity: cv.severity,
|
|
13487
|
-
source: `cache(${cv.source})`,
|
|
13488
|
-
artifacts: [url],
|
|
13489
|
-
matchedThreatId: null,
|
|
13490
|
-
reasons: cv.reasons
|
|
13491
|
-
};
|
|
13492
|
-
break;
|
|
13493
|
-
}
|
|
13494
|
-
}
|
|
13495
|
-
}
|
|
13496
|
-
if (cache) {
|
|
13497
|
-
try {
|
|
13498
|
-
for (const result of urlCheckResults) {
|
|
13499
|
-
let cv;
|
|
13500
|
-
if (result.isMalicious) {
|
|
13501
|
-
cv = {
|
|
13502
|
-
verdict: "deny",
|
|
13503
|
-
severity: "critical",
|
|
13504
|
-
reasons: [
|
|
13505
|
-
`URL check: malicious (${result.findings.map((f) => `${f.severityName}/${f.typeName}`).join(", ")})`
|
|
13506
|
-
],
|
|
13507
|
-
source: "url_check"
|
|
13508
|
-
};
|
|
13509
|
-
} else if (result.flags.length > 0) {
|
|
13510
|
-
cv = {
|
|
13511
|
-
verdict: "ask",
|
|
13512
|
-
severity: "warning",
|
|
13513
|
-
reasons: [`URL check: suspicious (${result.flags.join(", ")})`],
|
|
13514
|
-
source: "url_check"
|
|
13515
|
-
};
|
|
13516
|
-
} else {
|
|
13517
|
-
cv = {
|
|
13518
|
-
verdict: "allow",
|
|
13519
|
-
severity: "info",
|
|
13520
|
-
reasons: [],
|
|
13521
|
-
source: "url_check"
|
|
13522
|
-
};
|
|
13523
|
-
}
|
|
13524
|
-
cache.putUrl(result.url, cv, result.isMalicious);
|
|
13525
|
-
}
|
|
13526
|
-
await cache.save();
|
|
13527
|
-
} catch {
|
|
13528
|
-
}
|
|
13529
|
-
}
|
|
13530
|
-
try {
|
|
13531
|
-
await logVerdict(config.logging, sessionKey, toolName, params, verdict);
|
|
13532
|
-
} catch {
|
|
13533
|
-
}
|
|
13534
15182
|
if (verdict.decision === "allow") return void 0;
|
|
13535
|
-
const reasonText = verdict.reasons.length > 0 ? verdict.reasons.slice(0, 5).join("; ") : verdict.category;
|
|
13536
15183
|
if (verdict.decision === "deny") {
|
|
13537
|
-
return { block: true, blockReason:
|
|
15184
|
+
return { block: true, blockReason: formatDenyMessage(verdict) };
|
|
13538
15185
|
}
|
|
13539
15186
|
return {
|
|
13540
15187
|
block: true,
|
|
13541
|
-
blockReason:
|
|
15188
|
+
blockReason: formatAskMessage(actionId, verdict, artifacts)
|
|
13542
15189
|
};
|
|
13543
15190
|
} catch (e) {
|
|
13544
15191
|
logger.error("tool-handler error, failing open", { error: String(e) });
|
|
13545
15192
|
return void 0;
|
|
13546
15193
|
}
|
|
13547
|
-
}
|
|
15194
|
+
}
|
|
15195
|
+
return handleToolCall;
|
|
13548
15196
|
}
|
|
13549
15197
|
|
|
13550
15198
|
// src/index.ts
|
|
@@ -13557,15 +15205,25 @@ var index_default = {
|
|
|
13557
15205
|
},
|
|
13558
15206
|
register(api) {
|
|
13559
15207
|
const logger = createLogger(api.logger);
|
|
13560
|
-
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();
|
|
13561
15213
|
let pendingFindings = null;
|
|
13562
15214
|
const onFindings = (msg) => {
|
|
13563
|
-
|
|
15215
|
+
pendingFindings = msg;
|
|
13564
15216
|
};
|
|
13565
|
-
api.on(
|
|
13566
|
-
|
|
13567
|
-
|
|
15217
|
+
api.on(
|
|
15218
|
+
"before_tool_call",
|
|
15219
|
+
createToolCallHandler(approvalStore, logger, threatsDir, allowlistsDir),
|
|
15220
|
+
{ priority: 100 }
|
|
15221
|
+
);
|
|
13568
15222
|
api.registerTool(createSageApproveTool(approvalStore));
|
|
15223
|
+
api.registerTool(
|
|
15224
|
+
createAllowlistAddTool(approvalStore, logger)
|
|
15225
|
+
);
|
|
15226
|
+
api.registerTool(createAllowlistRemoveTool(logger));
|
|
13569
15227
|
api.on("gateway_start", createStartupScanHandler(logger, onFindings));
|
|
13570
15228
|
api.on("session_start", createSessionScanHandler(logger, onFindings));
|
|
13571
15229
|
api.on(
|