@gendigital/sage-openclaw 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1070 -373
- package/dist/index.cjs.map +4 -4
- package/dist/mcp-server.cjs +425 -98
- package/dist/mcp-server.cjs.map +4 -4
- package/dist/model-download-worker.cjs +1 -1
- package/dist/model-download-worker.cjs.map +2 -2
- package/dist/skill-upload-worker.cjs +129 -42
- package/dist/skill-upload-worker.cjs.map +4 -4
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -3
- package/resources/skills/security-awareness/SKILL.md +2 -2
- package/resources/threats/credentials.yaml +1 -1
- package/resources/threats/dummy.yaml +37 -4
- package/resources/threats/prompt-injection.yaml +13 -0
package/dist/index.cjs
CHANGED
|
@@ -63,7 +63,7 @@ async function renameWithRetry(from, to, attempts = 5) {
|
|
|
63
63
|
const transient = code === "EPERM" || code === "EACCES" || code === "EBUSY";
|
|
64
64
|
if (!transient || i === attempts - 1)
|
|
65
65
|
throw err;
|
|
66
|
-
await new Promise((
|
|
66
|
+
await new Promise((resolve11) => setTimeout(resolve11, 10 * 2 ** i));
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -4292,7 +4292,10 @@ var init_zod = __esm({
|
|
|
4292
4292
|
});
|
|
4293
4293
|
|
|
4294
4294
|
// ../core/dist/types.js
|
|
4295
|
-
|
|
4295
|
+
function isSubmitOnlyThreat(threat) {
|
|
4296
|
+
return threat.flags?.includes(SUBMIT_ONLY_FLAG) ?? false;
|
|
4297
|
+
}
|
|
4298
|
+
var nullLogger, ArtifactTypeSchema, ArtifactSchema, VerdictSeveritySchema, ThreatSchema, SUBMIT_ONLY_FLAG, DecisionSchema, SensitivitySchema, UrlCheckConfigSchema, CacheConfigSchema, LoggingConfigSchema, OperationalLogLevelSchema, OperationalLoggingConfigSchema, FileCheckConfigSchema, PackageCheckConfigSchema, AmsiCheckConfigSchema, SkillCheckConfigSchema, DEFAULT_PI_HIGH_RISK_THRESHOLD, DEFAULT_PI_MEDIUM_RISK_THRESHOLD, DEFAULT_PI_TELEMETRY_THRESHOLD, PiCheckConfigSchema, ExceptionDecisionSchema, ExceptionMatchSchema, ExceptionRuleSchema, ExceptionsFileSchema, ExceptionsConfigSchema, ConfigSchema, HookTypeSchema;
|
|
4296
4299
|
var init_types2 = __esm({
|
|
4297
4300
|
"../core/dist/types.js"() {
|
|
4298
4301
|
"use strict";
|
|
@@ -4328,6 +4331,7 @@ var init_types2 = __esm({
|
|
|
4328
4331
|
revoked: external_exports.boolean().optional().default(false),
|
|
4329
4332
|
flags: external_exports.array(external_exports.string()).optional().default([])
|
|
4330
4333
|
});
|
|
4334
|
+
SUBMIT_ONLY_FLAG = "submit_only";
|
|
4331
4335
|
DecisionSchema = external_exports.enum(["allow", "deny", "ask"]);
|
|
4332
4336
|
SensitivitySchema = external_exports.enum(["paranoid", "balanced", "relaxed"]);
|
|
4333
4337
|
UrlCheckConfigSchema = external_exports.object({
|
|
@@ -6891,7 +6895,7 @@ async function ensurePiDeps(modelPath, logger = nullLogger) {
|
|
|
6891
6895
|
}
|
|
6892
6896
|
}
|
|
6893
6897
|
function runNpm(cwd, args, logger) {
|
|
6894
|
-
return new Promise((
|
|
6898
|
+
return new Promise((resolve11, reject) => {
|
|
6895
6899
|
const isWindows = process.platform === "win32";
|
|
6896
6900
|
const npmCmd = isWindows ? "npm.cmd" : "npm";
|
|
6897
6901
|
const child = (0, import_node_child_process.execFile)(npmCmd, args, { cwd, timeout: 3e4, shell: isWindows }, (error, _stdout, stderr) => {
|
|
@@ -6899,7 +6903,7 @@ function runNpm(cwd, args, logger) {
|
|
|
6899
6903
|
logger.warn(`npm install error: ${stderr || error.message}`);
|
|
6900
6904
|
reject(error);
|
|
6901
6905
|
} else {
|
|
6902
|
-
|
|
6906
|
+
resolve11();
|
|
6903
6907
|
}
|
|
6904
6908
|
});
|
|
6905
6909
|
child.unref();
|
|
@@ -14323,7 +14327,7 @@ var ApprovalStore = class {
|
|
|
14323
14327
|
};
|
|
14324
14328
|
|
|
14325
14329
|
// ../core/dist/audit-log.js
|
|
14326
|
-
var
|
|
14330
|
+
var import_node_crypto4 = require("node:crypto");
|
|
14327
14331
|
init_config();
|
|
14328
14332
|
|
|
14329
14333
|
// ../core/dist/content-snapshot.js
|
|
@@ -14500,12 +14504,143 @@ function buildContentSnapshot(toolType, toolInput, artifacts = [], signals = {})
|
|
|
14500
14504
|
return content;
|
|
14501
14505
|
}
|
|
14502
14506
|
|
|
14507
|
+
// ../core/dist/detection-names.js
|
|
14508
|
+
var import_node_crypto3 = require("node:crypto");
|
|
14509
|
+
|
|
14510
|
+
// ../core/dist/model-storage.js
|
|
14511
|
+
var import_node_fs = require("node:fs");
|
|
14512
|
+
var import_node_path4 = require("node:path");
|
|
14513
|
+
init_config();
|
|
14514
|
+
var MODEL_SCHEMA_VERSION = "v2";
|
|
14515
|
+
var REQUIRED_MODELS_BY_SCHEMA = {
|
|
14516
|
+
v1: ["pi-model"],
|
|
14517
|
+
v2: ["pi-model"]
|
|
14518
|
+
};
|
|
14519
|
+
var REQUIRED_MODEL_FILES = {
|
|
14520
|
+
"pi-model": [
|
|
14521
|
+
"model_int8.onnx",
|
|
14522
|
+
"tokenizer.json",
|
|
14523
|
+
"tokenizer_config.json",
|
|
14524
|
+
"special_tokens_map.json",
|
|
14525
|
+
"vocab.txt",
|
|
14526
|
+
"config.json"
|
|
14527
|
+
]
|
|
14528
|
+
};
|
|
14529
|
+
function getModelStorageRoot(sageDir = resolvePath("~/.sage")) {
|
|
14530
|
+
return (0, import_node_path4.join)(resolvePath(sageDir), "models");
|
|
14531
|
+
}
|
|
14532
|
+
function getModelDir(modelName, schema = MODEL_SCHEMA_VERSION, sageDir) {
|
|
14533
|
+
return (0, import_node_path4.join)(getModelStorageRoot(sageDir), schema, modelName);
|
|
14534
|
+
}
|
|
14535
|
+
function isModelPresent(modelName, schema = MODEL_SCHEMA_VERSION, sageDir) {
|
|
14536
|
+
const dir = getModelDir(modelName, schema, sageDir);
|
|
14537
|
+
const files = REQUIRED_MODEL_FILES[modelName];
|
|
14538
|
+
if (!files || files.length === 0)
|
|
14539
|
+
return false;
|
|
14540
|
+
return files.every((f) => (0, import_node_fs.existsSync)((0, import_node_path4.resolve)(dir, f)));
|
|
14541
|
+
}
|
|
14542
|
+
function missingRequiredModels(schema = MODEL_SCHEMA_VERSION, sageDir) {
|
|
14543
|
+
const required = REQUIRED_MODELS_BY_SCHEMA[schema] ?? [];
|
|
14544
|
+
return required.filter((name) => !isModelPresent(name, schema, sageDir));
|
|
14545
|
+
}
|
|
14546
|
+
|
|
14547
|
+
// ../core/dist/detection-names.js
|
|
14548
|
+
init_types2();
|
|
14549
|
+
var PI_DETECTION_NAME = "Other:SagePromptInjectionML-A [Susp]";
|
|
14550
|
+
var REPORTING_ENGINE_SHORTHANDS = {
|
|
14551
|
+
package: "sgpk",
|
|
14552
|
+
amsi: "sgam",
|
|
14553
|
+
heuristics: "sghe",
|
|
14554
|
+
ml: "sgml",
|
|
14555
|
+
skill: "skan"
|
|
14556
|
+
};
|
|
14557
|
+
var REPORTING_DETECTION_SUFFIX = "sage";
|
|
14558
|
+
function formatReportingDetectionName(canonicalName, engine, optionalData = "") {
|
|
14559
|
+
const engineMetadata = optionalData ? `${REPORTING_ENGINE_SHORTHANDS[engine]}:${optionalData}` : REPORTING_ENGINE_SHORTHANDS[engine];
|
|
14560
|
+
return `${canonicalName}|${engineMetadata}|${REPORTING_DETECTION_SUFFIX}`;
|
|
14561
|
+
}
|
|
14562
|
+
function sha256Hex(text) {
|
|
14563
|
+
return (0, import_node_crypto3.createHash)("sha256").update(text).digest("hex");
|
|
14564
|
+
}
|
|
14565
|
+
function formatOnlineClassifierDetectionName(postfix, modelVersion, textHash) {
|
|
14566
|
+
return `Text:OnlineClassifier [${postfix}]|${modelVersion}-${textHash}|sgpi`;
|
|
14567
|
+
}
|
|
14568
|
+
function piReportingDetectionName(r) {
|
|
14569
|
+
if (r.risk >= DEFAULT_PI_MEDIUM_RISK_THRESHOLD && r.modelVersion && r.contentSnippet) {
|
|
14570
|
+
return formatOnlineClassifierDetectionName("Misc", r.modelVersion, sha256Hex(r.contentSnippet));
|
|
14571
|
+
}
|
|
14572
|
+
return formatReportingDetectionName(PI_DETECTION_NAME, "ml", `${r.modelId}:${MODEL_SCHEMA_VERSION}`);
|
|
14573
|
+
}
|
|
14574
|
+
var PACKAGE_DETECTION_NAMES = {
|
|
14575
|
+
not_found: "Other:SagePackageNotFound-A [Susp]",
|
|
14576
|
+
suspicious_age: "Other:SagePackageNew-A [Susp]",
|
|
14577
|
+
malicious: "Other:SagePackageMalicious-A [Trj]",
|
|
14578
|
+
unknown: "Other:SagePackageUnknown-A [Susp]"
|
|
14579
|
+
};
|
|
14580
|
+
var AMSI_DETECTION_NAMES = {
|
|
14581
|
+
detected: "Other:SageAmsiDetected-A [Heur]",
|
|
14582
|
+
blocked_by_admin: "Other:SageAmsiBlockedByAdmin-A [Heur]",
|
|
14583
|
+
unknown: "Other:SageAmsiUnknown-A [Susp]"
|
|
14584
|
+
};
|
|
14585
|
+
var AMSI_REPORTING_RULE_NAMES = {
|
|
14586
|
+
detected: "AMSI_DETECTED",
|
|
14587
|
+
blocked_by_admin: "AMSI_BLOCKED_BY_ADMIN",
|
|
14588
|
+
unknown: "AMSI_UNKNOWN"
|
|
14589
|
+
};
|
|
14590
|
+
var SKILL_DETECTION_NAMES = {
|
|
14591
|
+
HIGH: "Other:SageSuspiciousSkill-A [Susp]",
|
|
14592
|
+
CRITICAL: "Other:SageMaliciousSkill-A [Trj]"
|
|
14593
|
+
};
|
|
14594
|
+
function isCanonicalDetectionName(name) {
|
|
14595
|
+
return /^\w+:\w+-[A-Z]+ \[[^\]\r\n]+\]$/.test(name);
|
|
14596
|
+
}
|
|
14597
|
+
function packageDetectionName(verdict) {
|
|
14598
|
+
return PACKAGE_DETECTION_NAMES[verdict];
|
|
14599
|
+
}
|
|
14600
|
+
function amsiDetectionClass(amsiResult) {
|
|
14601
|
+
if (amsiResult >= 32768)
|
|
14602
|
+
return "detected";
|
|
14603
|
+
if (amsiResult >= 16384)
|
|
14604
|
+
return "blocked_by_admin";
|
|
14605
|
+
return "unknown";
|
|
14606
|
+
}
|
|
14607
|
+
function amsiDetectionName(amsiResult) {
|
|
14608
|
+
return AMSI_DETECTION_NAMES[amsiDetectionClass(amsiResult)];
|
|
14609
|
+
}
|
|
14610
|
+
function amsiReportingData(amsiResult) {
|
|
14611
|
+
const ruleName = AMSI_REPORTING_RULE_NAMES[amsiDetectionClass(amsiResult)];
|
|
14612
|
+
return `${ruleName}:${Math.trunc(amsiResult).toString(16)}`;
|
|
14613
|
+
}
|
|
14614
|
+
function skillDetectionName(verdict) {
|
|
14615
|
+
return SKILL_DETECTION_NAMES[verdict.toUpperCase()];
|
|
14616
|
+
}
|
|
14617
|
+
function isSkillId(value) {
|
|
14618
|
+
return /^[0-9a-f]{64}$/i.test(value);
|
|
14619
|
+
}
|
|
14620
|
+
function skillReportingDetectionName(verdict, skillId) {
|
|
14621
|
+
const canonicalName = skillDetectionName(verdict);
|
|
14622
|
+
return canonicalName && isSkillId(skillId) ? formatReportingDetectionName(canonicalName, "skill", skillId.toLowerCase()) : void 0;
|
|
14623
|
+
}
|
|
14624
|
+
function buildSkillCheckSignal(skillId, analyzerVerdict, jobId) {
|
|
14625
|
+
const normalizedSkillId = skillId.toLowerCase();
|
|
14626
|
+
const normalizedVerdict = analyzerVerdict.toUpperCase();
|
|
14627
|
+
const detectionName = skillReportingDetectionName(normalizedVerdict, normalizedSkillId);
|
|
14628
|
+
if (!detectionName)
|
|
14629
|
+
return void 0;
|
|
14630
|
+
return {
|
|
14631
|
+
detection_name: detectionName,
|
|
14632
|
+
skill_id: normalizedSkillId,
|
|
14633
|
+
...jobId ? { job_id: jobId } : {},
|
|
14634
|
+
analyzer_verdict: normalizedVerdict
|
|
14635
|
+
};
|
|
14636
|
+
}
|
|
14637
|
+
|
|
14503
14638
|
// ../core/dist/audit-log.js
|
|
14504
14639
|
init_file_utils();
|
|
14505
14640
|
|
|
14506
14641
|
// ../core/dist/jsonl-log-writer.js
|
|
14507
14642
|
var import_promises2 = require("node:fs/promises");
|
|
14508
|
-
var
|
|
14643
|
+
var import_node_path5 = require("node:path");
|
|
14509
14644
|
init_config();
|
|
14510
14645
|
init_file_utils();
|
|
14511
14646
|
var writeQueues = /* @__PURE__ */ new Map();
|
|
@@ -14542,7 +14677,7 @@ async function rotateIfNeeded(filePath, maxBytes, maxFiles) {
|
|
|
14542
14677
|
});
|
|
14543
14678
|
}
|
|
14544
14679
|
async function appendJsonlEntryNow(path, config2, entry) {
|
|
14545
|
-
await (0, import_promises2.mkdir)((0,
|
|
14680
|
+
await (0, import_promises2.mkdir)((0, import_node_path5.dirname)(path), { recursive: true });
|
|
14546
14681
|
await rotateIfNeeded(path, config2.max_bytes, config2.max_files);
|
|
14547
14682
|
await (0, import_promises2.appendFile)(path, `${JSON.stringify(entry)}
|
|
14548
14683
|
`);
|
|
@@ -14590,7 +14725,7 @@ async function logVerdict(config2, input) {
|
|
|
14590
14725
|
return;
|
|
14591
14726
|
const entry = {
|
|
14592
14727
|
type: "runtime_verdict",
|
|
14593
|
-
entry_id: input.eventId ?? (0,
|
|
14728
|
+
entry_id: input.eventId ?? (0, import_node_crypto4.randomUUID)(),
|
|
14594
14729
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14595
14730
|
session_id: input.sessionId,
|
|
14596
14731
|
conversation_id: input.conversationId ?? input.sessionId,
|
|
@@ -14604,6 +14739,8 @@ async function logVerdict(config2, input) {
|
|
|
14604
14739
|
reasons: input.verdict.reasons,
|
|
14605
14740
|
source: input.verdict.source,
|
|
14606
14741
|
threat_id: input.verdict.matchedThreatId,
|
|
14742
|
+
...input.verdict.customThreatMatched ? { custom_threat_matched: true } : {},
|
|
14743
|
+
...input.nonCustomThreatMatched ? { non_custom_detection_matched: true } : {},
|
|
14607
14744
|
user_override: userOverride,
|
|
14608
14745
|
signals: input.signals,
|
|
14609
14746
|
tool_use_id: input.toolUseId
|
|
@@ -14651,6 +14788,37 @@ async function logSkillQueued(config2, skillId, pluginKey, skillFolder) {
|
|
|
14651
14788
|
} catch {
|
|
14652
14789
|
}
|
|
14653
14790
|
}
|
|
14791
|
+
async function logSkillVerdict(config2, skillId, status, verdict, summary, context = {}) {
|
|
14792
|
+
if (!config2.enabled)
|
|
14793
|
+
return false;
|
|
14794
|
+
const entry = {
|
|
14795
|
+
type: "skill_verdict",
|
|
14796
|
+
entry_id: context.entryId ?? (0, import_node_crypto4.randomUUID)(),
|
|
14797
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14798
|
+
skill_id: skillId,
|
|
14799
|
+
status
|
|
14800
|
+
};
|
|
14801
|
+
if (context.jobId !== void 0)
|
|
14802
|
+
entry.job_id = context.jobId;
|
|
14803
|
+
if (context.agentRuntime !== void 0)
|
|
14804
|
+
entry.agent_runtime = context.agentRuntime;
|
|
14805
|
+
if (verdict !== void 0)
|
|
14806
|
+
entry.verdict = verdict;
|
|
14807
|
+
if (summary !== void 0)
|
|
14808
|
+
entry.summary = summary.slice(0, MAX_SUMMARY_LEN);
|
|
14809
|
+
const skillSignal = status === "analyzed" && verdict !== void 0 ? buildSkillCheckSignal(skillId, verdict, context.jobId) : void 0;
|
|
14810
|
+
if (skillSignal) {
|
|
14811
|
+
entry.signals = {
|
|
14812
|
+
skill_checks: [skillSignal]
|
|
14813
|
+
};
|
|
14814
|
+
}
|
|
14815
|
+
try {
|
|
14816
|
+
await appendEntry(config2, entry);
|
|
14817
|
+
return true;
|
|
14818
|
+
} catch {
|
|
14819
|
+
return false;
|
|
14820
|
+
}
|
|
14821
|
+
}
|
|
14654
14822
|
|
|
14655
14823
|
// ../core/dist/cache.js
|
|
14656
14824
|
init_config();
|
|
@@ -15089,19 +15257,19 @@ var PersistentPowershellAmsiBackend = class {
|
|
|
15089
15257
|
return this.available;
|
|
15090
15258
|
}
|
|
15091
15259
|
waitForLine(timeout) {
|
|
15092
|
-
return new Promise((
|
|
15260
|
+
return new Promise((resolve11, reject) => {
|
|
15093
15261
|
const timer = setTimeout(() => {
|
|
15094
15262
|
this.pendingResponse = null;
|
|
15095
15263
|
reject(new Error("timeout"));
|
|
15096
15264
|
}, timeout);
|
|
15097
|
-
this.pendingResponse = { resolve:
|
|
15265
|
+
this.pendingResponse = { resolve: resolve11, reject, timer };
|
|
15098
15266
|
});
|
|
15099
15267
|
}
|
|
15100
15268
|
async enqueueScan(operation) {
|
|
15101
15269
|
const previous = this.scanQueue;
|
|
15102
15270
|
let release;
|
|
15103
|
-
this.scanQueue = new Promise((
|
|
15104
|
-
release =
|
|
15271
|
+
this.scanQueue = new Promise((resolve11) => {
|
|
15272
|
+
release = resolve11;
|
|
15105
15273
|
});
|
|
15106
15274
|
await previous;
|
|
15107
15275
|
try {
|
|
@@ -15149,10 +15317,10 @@ var PersistentPowershellAmsiBackend = class {
|
|
|
15149
15317
|
const line = this.stdoutBuffer.slice(0, idx).trim();
|
|
15150
15318
|
this.stdoutBuffer = this.stdoutBuffer.slice(idx + 1);
|
|
15151
15319
|
if (this.pendingResponse) {
|
|
15152
|
-
const { resolve:
|
|
15320
|
+
const { resolve: resolve11, timer } = this.pendingResponse;
|
|
15153
15321
|
this.pendingResponse = null;
|
|
15154
15322
|
clearTimeout(timer);
|
|
15155
|
-
|
|
15323
|
+
resolve11(line);
|
|
15156
15324
|
}
|
|
15157
15325
|
idx = this.stdoutBuffer.indexOf("\n");
|
|
15158
15326
|
}
|
|
@@ -15251,7 +15419,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15251
15419
|
if (!this.available)
|
|
15252
15420
|
return null;
|
|
15253
15421
|
const truncated = content.length > MAX_SCAN_LENGTH ? content.slice(0, MAX_SCAN_LENGTH) : content;
|
|
15254
|
-
return new Promise((
|
|
15422
|
+
return new Promise((resolve11) => {
|
|
15255
15423
|
try {
|
|
15256
15424
|
const ps = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", PS_ONESHOT_SCRIPT], { stdio: ["pipe", "pipe", "pipe"] });
|
|
15257
15425
|
let stdout = "";
|
|
@@ -15261,7 +15429,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15261
15429
|
ps.kill();
|
|
15262
15430
|
} catch {
|
|
15263
15431
|
}
|
|
15264
|
-
|
|
15432
|
+
resolve11(null);
|
|
15265
15433
|
}, PS_TIMEOUT);
|
|
15266
15434
|
ps.stdout?.on("data", (chunk) => {
|
|
15267
15435
|
stdout += chunk.toString();
|
|
@@ -15274,7 +15442,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15274
15442
|
ps.on("error", (err) => {
|
|
15275
15443
|
clearTimeout(timer);
|
|
15276
15444
|
this.logger.debug("AMSI: PowerShell one-shot error", { error: String(err) });
|
|
15277
|
-
|
|
15445
|
+
resolve11(null);
|
|
15278
15446
|
});
|
|
15279
15447
|
ps.on("exit", () => {
|
|
15280
15448
|
clearTimeout(timer);
|
|
@@ -15287,11 +15455,11 @@ var WslPowershellAmsiBackend = class {
|
|
|
15287
15455
|
contentName
|
|
15288
15456
|
});
|
|
15289
15457
|
}
|
|
15290
|
-
|
|
15458
|
+
resolve11(null);
|
|
15291
15459
|
return;
|
|
15292
15460
|
}
|
|
15293
15461
|
this.logger.debug("AMSI: PowerShell one-shot result", { contentName, amsiResult });
|
|
15294
|
-
|
|
15462
|
+
resolve11(interpretAmsiResult(amsiResult, content, contentName));
|
|
15295
15463
|
});
|
|
15296
15464
|
const req = JSON.stringify({ content: truncated, contentName });
|
|
15297
15465
|
ps.stdin?.write(`${req}
|
|
@@ -15299,7 +15467,7 @@ var WslPowershellAmsiBackend = class {
|
|
|
15299
15467
|
ps.stdin?.end();
|
|
15300
15468
|
} catch (e) {
|
|
15301
15469
|
this.logger.warn("AMSI: PowerShell one-shot failed", { error: String(e), contentName });
|
|
15302
|
-
|
|
15470
|
+
resolve11(null);
|
|
15303
15471
|
}
|
|
15304
15472
|
});
|
|
15305
15473
|
}
|
|
@@ -15439,15 +15607,15 @@ var ContentFetchClient = class _ContentFetchClient {
|
|
|
15439
15607
|
init_types2();
|
|
15440
15608
|
|
|
15441
15609
|
// ../core/dist/version.js
|
|
15442
|
-
var
|
|
15610
|
+
var import_node_path6 = require("node:path");
|
|
15443
15611
|
var import_node_url = require("node:url");
|
|
15444
15612
|
init_file_utils();
|
|
15445
15613
|
var import_meta = {};
|
|
15446
15614
|
function resolveVersion() {
|
|
15447
15615
|
if (true)
|
|
15448
|
-
return "0.
|
|
15616
|
+
return "0.15.0";
|
|
15449
15617
|
try {
|
|
15450
|
-
const pkgPath = (0,
|
|
15618
|
+
const pkgPath = (0, import_node_path6.join)((0, import_node_path6.dirname)((0, import_node_url.fileURLToPath)(import_meta.url)), "..", "package.json");
|
|
15451
15619
|
const pkg = JSON.parse(getFileContentSync(pkgPath));
|
|
15452
15620
|
if (typeof pkg.version === "string")
|
|
15453
15621
|
return pkg.version;
|
|
@@ -15590,43 +15758,6 @@ var FileCheckClient = class {
|
|
|
15590
15758
|
}
|
|
15591
15759
|
};
|
|
15592
15760
|
|
|
15593
|
-
// ../core/dist/model-storage.js
|
|
15594
|
-
var import_node_fs = require("node:fs");
|
|
15595
|
-
var import_node_path6 = require("node:path");
|
|
15596
|
-
init_config();
|
|
15597
|
-
var MODEL_SCHEMA_VERSION = "v2";
|
|
15598
|
-
var REQUIRED_MODELS_BY_SCHEMA = {
|
|
15599
|
-
v1: ["pi-model"],
|
|
15600
|
-
v2: ["pi-model"]
|
|
15601
|
-
};
|
|
15602
|
-
var REQUIRED_MODEL_FILES = {
|
|
15603
|
-
"pi-model": [
|
|
15604
|
-
"model_int8.onnx",
|
|
15605
|
-
"tokenizer.json",
|
|
15606
|
-
"tokenizer_config.json",
|
|
15607
|
-
"special_tokens_map.json",
|
|
15608
|
-
"vocab.txt",
|
|
15609
|
-
"config.json"
|
|
15610
|
-
]
|
|
15611
|
-
};
|
|
15612
|
-
function getModelStorageRoot(sageDir = resolvePath("~/.sage")) {
|
|
15613
|
-
return (0, import_node_path6.join)(resolvePath(sageDir), "models");
|
|
15614
|
-
}
|
|
15615
|
-
function getModelDir(modelName, schema = MODEL_SCHEMA_VERSION, sageDir) {
|
|
15616
|
-
return (0, import_node_path6.join)(getModelStorageRoot(sageDir), schema, modelName);
|
|
15617
|
-
}
|
|
15618
|
-
function isModelPresent(modelName, schema = MODEL_SCHEMA_VERSION, sageDir) {
|
|
15619
|
-
const dir = getModelDir(modelName, schema, sageDir);
|
|
15620
|
-
const files = REQUIRED_MODEL_FILES[modelName];
|
|
15621
|
-
if (!files || files.length === 0)
|
|
15622
|
-
return false;
|
|
15623
|
-
return files.every((f) => (0, import_node_fs.existsSync)((0, import_node_path6.resolve)(dir, f)));
|
|
15624
|
-
}
|
|
15625
|
-
function missingRequiredModels(schema = MODEL_SCHEMA_VERSION, sageDir) {
|
|
15626
|
-
const required = REQUIRED_MODELS_BY_SCHEMA[schema] ?? [];
|
|
15627
|
-
return required.filter((name) => !isModelPresent(name, schema, sageDir));
|
|
15628
|
-
}
|
|
15629
|
-
|
|
15630
15761
|
// ../core/dist/clients/model-downloader.js
|
|
15631
15762
|
init_types2();
|
|
15632
15763
|
var STALE_LOCK_MS = 60 * 60 * 1e3;
|
|
@@ -15967,6 +16098,8 @@ var RegistryClient = class {
|
|
|
15967
16098
|
};
|
|
15968
16099
|
|
|
15969
16100
|
// ../core/dist/clients/pi-check.js
|
|
16101
|
+
var import_node_crypto5 = require("node:crypto");
|
|
16102
|
+
var import_promises3 = require("node:fs/promises");
|
|
15970
16103
|
var import_node_path9 = require("node:path");
|
|
15971
16104
|
init_types2();
|
|
15972
16105
|
var DEFAULT_MAX_CONTENT_LENGTH2 = 16384;
|
|
@@ -15981,6 +16114,18 @@ var MODEL_METADATA_FILES = [
|
|
|
15981
16114
|
"vocab.txt",
|
|
15982
16115
|
MODEL_FILE
|
|
15983
16116
|
];
|
|
16117
|
+
async function computeModelVersion(modelPath, logger = nullLogger) {
|
|
16118
|
+
try {
|
|
16119
|
+
const hash = (0, import_node_crypto5.createHash)("sha256");
|
|
16120
|
+
for (const name of MODEL_METADATA_FILES) {
|
|
16121
|
+
hash.update(await (0, import_promises3.readFile)((0, import_node_path9.resolve)(modelPath, name)));
|
|
16122
|
+
}
|
|
16123
|
+
return hash.digest("hex").slice(0, 8);
|
|
16124
|
+
} catch (err) {
|
|
16125
|
+
logger.warn(`PI model version unavailable; hits keep the model-id detection name: ${err instanceof Error ? err.message : String(err)}`);
|
|
16126
|
+
return void 0;
|
|
16127
|
+
}
|
|
16128
|
+
}
|
|
15984
16129
|
function softmax(logits) {
|
|
15985
16130
|
const max = Math.max(...logits);
|
|
15986
16131
|
const exp = logits.map((x) => Math.exp(x - max));
|
|
@@ -16005,6 +16150,7 @@ var BundledPiProvider = class _BundledPiProvider {
|
|
|
16005
16150
|
logger;
|
|
16006
16151
|
tokenizer = null;
|
|
16007
16152
|
session = null;
|
|
16153
|
+
modelVersion;
|
|
16008
16154
|
tensorClass = null;
|
|
16009
16155
|
inputNames = /* @__PURE__ */ new Set();
|
|
16010
16156
|
initPromise = null;
|
|
@@ -16070,6 +16216,7 @@ var BundledPiProvider = class _BundledPiProvider {
|
|
|
16070
16216
|
findings,
|
|
16071
16217
|
contentName: context,
|
|
16072
16218
|
modelId,
|
|
16219
|
+
modelVersion: this.modelVersion,
|
|
16073
16220
|
contentSnippet: maxChunk || void 0
|
|
16074
16221
|
};
|
|
16075
16222
|
this.logger.info("PI model inference completed", {
|
|
@@ -16112,6 +16259,7 @@ var BundledPiProvider = class _BundledPiProvider {
|
|
|
16112
16259
|
this.session = await runtime.InferenceSession.create(modelFilePath);
|
|
16113
16260
|
const sess = this.session;
|
|
16114
16261
|
this.inputNames = new Set(sess.inputNames);
|
|
16262
|
+
this.modelVersion = await computeModelVersion(this.modelPath, this.logger);
|
|
16115
16263
|
_BundledPiProvider.modelRuntimeLoaded = true;
|
|
16116
16264
|
this.logger.info("PI model loaded", { path: this.modelPath });
|
|
16117
16265
|
return true;
|
|
@@ -16303,6 +16451,7 @@ var SkillCheckClient = class {
|
|
|
16303
16451
|
parseResult(skillId, raw) {
|
|
16304
16452
|
return {
|
|
16305
16453
|
skillId,
|
|
16454
|
+
jobId: typeof raw.job_id === "string" ? raw.job_id : void 0,
|
|
16306
16455
|
verdict: typeof raw.verdict === "string" ? raw.verdict : void 0,
|
|
16307
16456
|
summary: typeof raw.summary === "string" ? raw.summary : void 0
|
|
16308
16457
|
};
|
|
@@ -16313,7 +16462,7 @@ var SkillCheckClient = class {
|
|
|
16313
16462
|
init_config();
|
|
16314
16463
|
|
|
16315
16464
|
// ../core/dist/config-defaults.js
|
|
16316
|
-
var
|
|
16465
|
+
var import_promises4 = require("node:fs/promises");
|
|
16317
16466
|
var import_node_path10 = require("node:path");
|
|
16318
16467
|
init_file_utils();
|
|
16319
16468
|
init_types2();
|
|
@@ -16344,11 +16493,11 @@ async function deployConfigDefaults(sageDirPath, logger = nullLogger) {
|
|
|
16344
16493
|
const targetPath = (0, import_node_path10.join)(sageDirPath, CONFIG_DEFAULTS_FILENAME);
|
|
16345
16494
|
const defaults = serializeConfigDefaults();
|
|
16346
16495
|
try {
|
|
16347
|
-
await (0,
|
|
16348
|
-
await (0,
|
|
16496
|
+
await (0, import_promises4.mkdir)(sageDirPath, { recursive: true, mode: 448 });
|
|
16497
|
+
await (0, import_promises4.chmod)(sageDirPath, 448);
|
|
16349
16498
|
let existing = null;
|
|
16350
16499
|
try {
|
|
16351
|
-
existing = await (0,
|
|
16500
|
+
existing = await (0, import_promises4.readFile)(targetPath, "utf-8");
|
|
16352
16501
|
} catch {
|
|
16353
16502
|
}
|
|
16354
16503
|
if (existing) {
|
|
@@ -16365,7 +16514,7 @@ async function deployConfigDefaults(sageDirPath, logger = nullLogger) {
|
|
|
16365
16514
|
return;
|
|
16366
16515
|
}
|
|
16367
16516
|
await withFileLock(targetPath, async () => {
|
|
16368
|
-
await (0,
|
|
16517
|
+
await (0, import_promises4.writeFile)(targetPath, defaults, { encoding: "utf-8", mode: 384 });
|
|
16369
16518
|
logger.debug("Deployed config defaults", {
|
|
16370
16519
|
path: targetPath,
|
|
16371
16520
|
schemaVersion: CONFIG_DEFAULTS_SCHEMA_VERSION
|
|
@@ -16512,58 +16661,8 @@ function isScannableContent(content) {
|
|
|
16512
16661
|
return SNIFF_HTML.test(head) || SNIFF_HTML_TAGS.test(head) || SNIFF_JSON.test(head) || SNIFF_XML.test(head) || SNIFF_YAML.test(head) || SNIFF_SHEBANG_SHELL.test(head) || SNIFF_SHEBANG_PYTHON.test(head) || SNIFF_PYTHON.test(head) || SNIFF_JS_TS.test(head) || SNIFF_C_CPP.test(head) || SNIFF_KOTLIN.test(head) || SNIFF_CSV.test(head);
|
|
16513
16662
|
}
|
|
16514
16663
|
|
|
16515
|
-
// ../core/dist/detection-names.js
|
|
16516
|
-
var PI_DETECTION_NAME = "Other:SagePromptInjectionML-A [Susp]";
|
|
16517
|
-
var REPORTING_ENGINE_SHORTHANDS = {
|
|
16518
|
-
package: "sgpk",
|
|
16519
|
-
amsi: "sgam",
|
|
16520
|
-
heuristics: "sghe",
|
|
16521
|
-
ml: "sgml"
|
|
16522
|
-
};
|
|
16523
|
-
var REPORTING_DETECTION_SUFFIX = "sage";
|
|
16524
|
-
function formatReportingDetectionName(canonicalName, engine, optionalData = "") {
|
|
16525
|
-
const engineMetadata = optionalData ? `${REPORTING_ENGINE_SHORTHANDS[engine]}:${optionalData}` : REPORTING_ENGINE_SHORTHANDS[engine];
|
|
16526
|
-
return `${canonicalName}|${engineMetadata}|${REPORTING_DETECTION_SUFFIX}`;
|
|
16527
|
-
}
|
|
16528
|
-
var PACKAGE_DETECTION_NAMES = {
|
|
16529
|
-
not_found: "Other:SagePackageNotFound-A [Susp]",
|
|
16530
|
-
suspicious_age: "Other:SagePackageNew-A [Susp]",
|
|
16531
|
-
malicious: "Other:SagePackageMalicious-A [Trj]",
|
|
16532
|
-
unknown: "Other:SagePackageUnknown-A [Susp]"
|
|
16533
|
-
};
|
|
16534
|
-
var AMSI_DETECTION_NAMES = {
|
|
16535
|
-
detected: "Other:SageAmsiDetected-A [Heur]",
|
|
16536
|
-
blocked_by_admin: "Other:SageAmsiBlockedByAdmin-A [Heur]",
|
|
16537
|
-
unknown: "Other:SageAmsiUnknown-A [Susp]"
|
|
16538
|
-
};
|
|
16539
|
-
var AMSI_REPORTING_RULE_NAMES = {
|
|
16540
|
-
detected: "AMSI_DETECTED",
|
|
16541
|
-
blocked_by_admin: "AMSI_BLOCKED_BY_ADMIN",
|
|
16542
|
-
unknown: "AMSI_UNKNOWN"
|
|
16543
|
-
};
|
|
16544
|
-
function isCanonicalDetectionName(name) {
|
|
16545
|
-
return /^\w+:\w+-[A-Z]+ \[[^\]\r\n]+\]$/.test(name);
|
|
16546
|
-
}
|
|
16547
|
-
function packageDetectionName(verdict) {
|
|
16548
|
-
return PACKAGE_DETECTION_NAMES[verdict];
|
|
16549
|
-
}
|
|
16550
|
-
function amsiDetectionClass(amsiResult) {
|
|
16551
|
-
if (amsiResult >= 32768)
|
|
16552
|
-
return "detected";
|
|
16553
|
-
if (amsiResult >= 16384)
|
|
16554
|
-
return "blocked_by_admin";
|
|
16555
|
-
return "unknown";
|
|
16556
|
-
}
|
|
16557
|
-
function amsiDetectionName(amsiResult) {
|
|
16558
|
-
return AMSI_DETECTION_NAMES[amsiDetectionClass(amsiResult)];
|
|
16559
|
-
}
|
|
16560
|
-
function amsiReportingData(amsiResult) {
|
|
16561
|
-
const ruleName = AMSI_REPORTING_RULE_NAMES[amsiDetectionClass(amsiResult)];
|
|
16562
|
-
return `${ruleName}:${Math.trunc(amsiResult).toString(16)}`;
|
|
16563
|
-
}
|
|
16564
|
-
|
|
16565
16664
|
// ../core/dist/extended-info.js
|
|
16566
|
-
var
|
|
16665
|
+
var import_promises5 = require("node:fs/promises");
|
|
16567
16666
|
var import_node_os3 = require("node:os");
|
|
16568
16667
|
var import_node_path11 = require("node:path");
|
|
16569
16668
|
init_file_utils();
|
|
@@ -16643,7 +16742,7 @@ async function loadExtendedInfo(sageDirPath, logger = nullLogger) {
|
|
|
16643
16742
|
async function loadExtendedInfoUncached(filePath, logger) {
|
|
16644
16743
|
let size;
|
|
16645
16744
|
try {
|
|
16646
|
-
const info = await (0,
|
|
16745
|
+
const info = await (0, import_promises5.stat)(filePath);
|
|
16647
16746
|
if (!info.isFile()) {
|
|
16648
16747
|
logger.debug(`extended-info: not a regular file at ${filePath}`);
|
|
16649
16748
|
return null;
|
|
@@ -16730,8 +16829,8 @@ function mergeExtendedInfo(envelope, extendedInfo) {
|
|
|
16730
16829
|
}
|
|
16731
16830
|
|
|
16732
16831
|
// ../core/dist/installation-id.js
|
|
16733
|
-
var
|
|
16734
|
-
var
|
|
16832
|
+
var import_node_crypto6 = require("node:crypto");
|
|
16833
|
+
var import_promises6 = require("node:fs/promises");
|
|
16735
16834
|
var import_node_path12 = require("node:path");
|
|
16736
16835
|
init_config();
|
|
16737
16836
|
init_file_utils();
|
|
@@ -16748,9 +16847,9 @@ async function getInstallationId(sageDirPath) {
|
|
|
16748
16847
|
} catch {
|
|
16749
16848
|
}
|
|
16750
16849
|
try {
|
|
16751
|
-
const id = (0,
|
|
16752
|
-
await (0,
|
|
16753
|
-
await (0,
|
|
16850
|
+
const id = (0, import_node_crypto6.randomUUID)();
|
|
16851
|
+
await (0, import_promises6.mkdir)(sageDir, { recursive: true, mode: 448 });
|
|
16852
|
+
await (0, import_promises6.writeFile)(idPath, id, { encoding: "utf-8", mode: 384, flag: fileExists ? "w" : "wx" });
|
|
16754
16853
|
return id;
|
|
16755
16854
|
} catch (err) {
|
|
16756
16855
|
if (err.code === "EEXIST") {
|
|
@@ -16934,6 +17033,10 @@ var DecisionEngine = class {
|
|
|
16934
17033
|
this.askThreshold = policy.askThreshold;
|
|
16935
17034
|
this.logger = logger;
|
|
16936
17035
|
}
|
|
17036
|
+
/** Whether these sources contain a decision signal independent of custom rules. */
|
|
17037
|
+
hasNonCustomDetection(sources) {
|
|
17038
|
+
return this.collectSignals(sources.heuristicMatches, sources.urlCheckResults, sources.packageCheckResults, sources.amsiCheckResults, sources.piCheckResults, sources.piThresholds).some((signal) => signal.customThreat !== true);
|
|
17039
|
+
}
|
|
16937
17040
|
async decide(sources) {
|
|
16938
17041
|
const signals = this.collectSignals(sources.heuristicMatches, sources.urlCheckResults, sources.packageCheckResults, sources.amsiCheckResults, sources.piCheckResults, sources.piThresholds);
|
|
16939
17042
|
if (signals.length === 0) {
|
|
@@ -16952,12 +17055,15 @@ var DecisionEngine = class {
|
|
|
16952
17055
|
source: top.source,
|
|
16953
17056
|
artifacts: allArtifacts,
|
|
16954
17057
|
matchedThreatId: top.threatId,
|
|
17058
|
+
customThreat: top.customThreat,
|
|
16955
17059
|
reasons: allReasons
|
|
16956
17060
|
};
|
|
16957
17061
|
}
|
|
16958
17062
|
collectSignals(heuristicMatches, urlCheckResults, packageCheckResults, amsiCheckResults, piCheckResults, piThresholds) {
|
|
16959
17063
|
const signals = [];
|
|
16960
17064
|
for (const match of heuristicMatches) {
|
|
17065
|
+
if (isSubmitOnlyThreat(match.threat))
|
|
17066
|
+
continue;
|
|
16961
17067
|
const { confidence, category, severity, id, title } = match.threat;
|
|
16962
17068
|
if (applyPolicy(confidence, this.denyThreshold, this.askThreshold, this.logger) === "allow")
|
|
16963
17069
|
continue;
|
|
@@ -16967,6 +17073,7 @@ var DecisionEngine = class {
|
|
|
16967
17073
|
severity,
|
|
16968
17074
|
source: "heuristic",
|
|
16969
17075
|
threatId: id,
|
|
17076
|
+
customThreat: match.threat.customRule === true,
|
|
16970
17077
|
reason: title,
|
|
16971
17078
|
artifact: match.artifact
|
|
16972
17079
|
});
|
|
@@ -17092,43 +17199,422 @@ var DecisionEngine = class {
|
|
|
17092
17199
|
};
|
|
17093
17200
|
|
|
17094
17201
|
// ../core/dist/evaluator.js
|
|
17095
|
-
var
|
|
17096
|
-
init_config();
|
|
17097
|
-
|
|
17098
|
-
// ../core/dist/exceptions.js
|
|
17099
|
-
var import_node_crypto5 = require("node:crypto");
|
|
17100
|
-
var import_node_path15 = require("node:path");
|
|
17202
|
+
var import_node_crypto8 = require("node:crypto");
|
|
17101
17203
|
init_config();
|
|
17102
|
-
init_file_utils();
|
|
17103
17204
|
|
|
17104
|
-
// ../core/dist/
|
|
17105
|
-
var
|
|
17106
|
-
var
|
|
17107
|
-
var
|
|
17108
|
-
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
|
|
17113
|
-
|
|
17114
|
-
|
|
17115
|
-
|
|
17116
|
-
|
|
17117
|
-
|
|
17118
|
-
|
|
17119
|
-
|
|
17120
|
-
|
|
17121
|
-
|
|
17122
|
-
|
|
17123
|
-
|
|
17124
|
-
|
|
17125
|
-
|
|
17126
|
-
|
|
17127
|
-
|
|
17128
|
-
|
|
17129
|
-
|
|
17130
|
-
|
|
17131
|
-
|
|
17205
|
+
// ../core/dist/extractors.js
|
|
17206
|
+
var URL_PATTERN = /https?:\/\/[^\s"')<>[\]{}]+/g;
|
|
17207
|
+
var TRAILING_PUNCT = /[.,;:!?]+$/;
|
|
17208
|
+
var MAX_CONTENT_SIZE = 64 * 1024;
|
|
17209
|
+
var BASE64_DECODE_EXEC = /\b(echo|printf|cat)\b[^|]*\|\s*base64\s+(-d|--decode)[^|]*\|\s*(bash|sh|zsh|ksh|dash|python|perl|ruby|node)\b/;
|
|
17210
|
+
var PRINTF_ENCODE_EXEC = /\bprintf\b\s+['"](\\x[0-9a-fA-F]{2}|\\[0-7]{3})[^|]*\|\s*(bash|sh|zsh|ksh|dash)\b/;
|
|
17211
|
+
var VAR_INTERPOLATION_URL = /\b(curl|wget|fetch)\b[^|;]*(\$\{?\w+\}?\.\$\{?\w+\}?|\$\{?\w+\}?:\/\/)/;
|
|
17212
|
+
var SHELLS = "bash|sh|zsh|ksh|dash";
|
|
17213
|
+
function extractUrls(text) {
|
|
17214
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17215
|
+
const urls = [];
|
|
17216
|
+
for (const match of text.matchAll(URL_PATTERN)) {
|
|
17217
|
+
const url = match[0].replace(TRAILING_PUNCT, "");
|
|
17218
|
+
if (!seen.has(url)) {
|
|
17219
|
+
seen.add(url);
|
|
17220
|
+
urls.push(url);
|
|
17221
|
+
}
|
|
17222
|
+
}
|
|
17223
|
+
return urls;
|
|
17224
|
+
}
|
|
17225
|
+
function escapeRegExp(str) {
|
|
17226
|
+
const regExpEscape = RegExp.escape;
|
|
17227
|
+
if (regExpEscape) {
|
|
17228
|
+
return regExpEscape(str);
|
|
17229
|
+
}
|
|
17230
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17231
|
+
}
|
|
17232
|
+
function stripHeredocs(command) {
|
|
17233
|
+
return command.replace(/<<-?\s*['"]?(\w+)['"]?\n[\s\S]*?\n[ \t]*\1\b/g, "<<$1");
|
|
17234
|
+
}
|
|
17235
|
+
function extractFromBash(command) {
|
|
17236
|
+
const artifacts = [];
|
|
17237
|
+
const effective = stripHeredocs(command);
|
|
17238
|
+
artifacts.push({ type: "command", value: effective });
|
|
17239
|
+
if (BASE64_DECODE_EXEC.test(effective)) {
|
|
17240
|
+
artifacts.push({ type: "command", value: effective, context: "base64_decode_exec" });
|
|
17241
|
+
}
|
|
17242
|
+
if (PRINTF_ENCODE_EXEC.test(effective)) {
|
|
17243
|
+
artifacts.push({ type: "command", value: effective, context: "printf_encode_exec" });
|
|
17244
|
+
}
|
|
17245
|
+
if (VAR_INTERPOLATION_URL.test(effective)) {
|
|
17246
|
+
artifacts.push({ type: "command", value: effective, context: "variable_interpolation_url" });
|
|
17247
|
+
}
|
|
17248
|
+
for (const url of extractUrls(command)) {
|
|
17249
|
+
let context;
|
|
17250
|
+
const escaped = escapeRegExp(url);
|
|
17251
|
+
const directPipe = new RegExp(escaped + String.raw`[^|]*\|\s*(${SHELLS})\b`);
|
|
17252
|
+
const wrappedPipe = new RegExp(escaped + String.raw`[^|]*\|\s*\S+\s+(\S+\s+)?(${SHELLS})\b`);
|
|
17253
|
+
const subshellExec = new RegExp(`${String.raw`(\$\(|`}\`)${escaped}[^)\`]*${String.raw`\|\s*(${SHELLS})\b`}`);
|
|
17254
|
+
if (directPipe.test(command) || wrappedPipe.test(command) || subshellExec.test(command)) {
|
|
17255
|
+
context = "piped to shell";
|
|
17256
|
+
}
|
|
17257
|
+
artifacts.push({ type: "url", value: url, context });
|
|
17258
|
+
}
|
|
17259
|
+
return artifacts;
|
|
17260
|
+
}
|
|
17261
|
+
function extractFromWebFetch(toolInput) {
|
|
17262
|
+
const url = toolInput.url;
|
|
17263
|
+
if (url && typeof url === "string") {
|
|
17264
|
+
return [{ type: "url", value: url, context: "webfetch" }];
|
|
17265
|
+
}
|
|
17266
|
+
return [];
|
|
17267
|
+
}
|
|
17268
|
+
function extractFileArtifacts(toolInput, filePathKey, contentKey, contextName, urlContextName) {
|
|
17269
|
+
const artifacts = [];
|
|
17270
|
+
const filePath = toolInput[filePathKey];
|
|
17271
|
+
if (filePath && typeof filePath === "string") {
|
|
17272
|
+
artifacts.push({ type: "file_path", value: filePath, context: contextName });
|
|
17273
|
+
}
|
|
17274
|
+
const content = toolInput[contentKey];
|
|
17275
|
+
if (content && typeof content === "string" && content.trim()) {
|
|
17276
|
+
const capped = content.slice(0, MAX_CONTENT_SIZE);
|
|
17277
|
+
artifacts.push({ type: "content", value: capped, context: contextName });
|
|
17278
|
+
for (const url of extractUrls(capped)) {
|
|
17279
|
+
artifacts.push({ type: "url", value: url, context: urlContextName });
|
|
17280
|
+
}
|
|
17281
|
+
}
|
|
17282
|
+
return artifacts;
|
|
17283
|
+
}
|
|
17284
|
+
function extractFromWrite(toolInput) {
|
|
17285
|
+
return extractFileArtifacts(toolInput, "file_path", "content", "write", "from_file_content");
|
|
17286
|
+
}
|
|
17287
|
+
function extractFromEdit(toolInput) {
|
|
17288
|
+
return extractFileArtifacts(toolInput, "file_path", "new_string", "edit", "from_edit_content");
|
|
17289
|
+
}
|
|
17290
|
+
|
|
17291
|
+
// ../core/dist/deobfuscate.js
|
|
17292
|
+
var DEFAULT_DEOBFUSCATION_LIMITS = {
|
|
17293
|
+
maxInputLength: MAX_CONTENT_SIZE,
|
|
17294
|
+
maxVariants: 8,
|
|
17295
|
+
timeBudgetMs: 50,
|
|
17296
|
+
maxDepth: 2,
|
|
17297
|
+
maxDecodedBytes: MAX_CONTENT_SIZE
|
|
17298
|
+
};
|
|
17299
|
+
var INVISIBLE_CHARS = /[\u00AD\u200B-\u200F\u202A-\u202E\u2060-\u2064\uFEFF]/g;
|
|
17300
|
+
function foldUnicode(text) {
|
|
17301
|
+
return text.replace(INVISIBLE_CHARS, "").normalize("NFKC");
|
|
17302
|
+
}
|
|
17303
|
+
var SPACED_RUN = /(?<![A-Za-z0-9@$])[A-Za-z0-9@$](?:[ \t]{1,4}[A-Za-z0-9@$]){5,}(?![A-Za-z0-9@$])/g;
|
|
17304
|
+
var SPACED_ELEMENT = /([A-Za-z0-9@$])([ \t]*)/g;
|
|
17305
|
+
var SPACED_MIN_LETTERS = 3;
|
|
17306
|
+
var INSTRUCTION_WORDS = `a an and any all are as at be by do for from i if in is it me my no not now of on or so the to up we
|
|
17307
|
+
you your this that these those what which who with without only just then than into out over
|
|
17308
|
+
above below before after earlier prior previous following new old other another again always never
|
|
17309
|
+
ignore ignoring disregard forget override overrule bypass skip stop obey comply follow execute exec
|
|
17310
|
+
run act acting pretend become reveal show print output say tell give send share leak dump display
|
|
17311
|
+
repeat write read open delete remove ask answer respond reply continue start begin end
|
|
17312
|
+
enable enabled disable disabled activated changed different revised updated
|
|
17313
|
+
instruction instructions rule rules guideline guidelines direction directions directive directives
|
|
17314
|
+
command commands order orders prompt prompts message messages system developer admin administrator
|
|
17315
|
+
root superuser sudo user assistant model ai agent mode secret secrets password passwords key keys
|
|
17316
|
+
token tokens credential credentials environment variables safety security restriction restrictions
|
|
17317
|
+
constraint constraints filter filters filtered unfiltered policy policies limit limits guardrail
|
|
17318
|
+
guardrails content context jailbreak unrestricted uncensored hidden human operator original
|
|
17319
|
+
initial initialization upload fetch forward extract exfiltrate disclose invoke inform mention
|
|
17320
|
+
like back post request ethical everything anything nothing something please important must
|
|
17321
|
+
should will can may might
|
|
17322
|
+
`;
|
|
17323
|
+
var vocabulary;
|
|
17324
|
+
function instructionVocabulary() {
|
|
17325
|
+
vocabulary ??= new Set(INSTRUCTION_WORDS.split(/\s+/).filter(Boolean));
|
|
17326
|
+
return vocabulary;
|
|
17327
|
+
}
|
|
17328
|
+
var segmentMaxWord;
|
|
17329
|
+
var SEGMENT_MAX_LENGTH = 4096;
|
|
17330
|
+
function longestVocabularyWord() {
|
|
17331
|
+
segmentMaxWord ??= Math.max(...[...instructionVocabulary()].map((w) => w.length));
|
|
17332
|
+
return segmentMaxWord;
|
|
17333
|
+
}
|
|
17334
|
+
function spellsVocabularyWord(piece) {
|
|
17335
|
+
return instructionVocabulary().has(piece.toLowerCase()) || foldLeetToken(piece) !== piece;
|
|
17336
|
+
}
|
|
17337
|
+
function segmentKnownWords(joined) {
|
|
17338
|
+
const n = joined.length;
|
|
17339
|
+
if (n === 0 || n > SEGMENT_MAX_LENGTH)
|
|
17340
|
+
return null;
|
|
17341
|
+
const best = new Array(n + 1).fill(Number.POSITIVE_INFINITY);
|
|
17342
|
+
const prev = new Array(n + 1).fill(-1);
|
|
17343
|
+
best[0] = 0;
|
|
17344
|
+
for (let end = 1; end <= n; end++) {
|
|
17345
|
+
let bestHere = Number.POSITIVE_INFINITY;
|
|
17346
|
+
for (let len = 1; len <= Math.min(longestVocabularyWord(), end); len++) {
|
|
17347
|
+
const start = end - len;
|
|
17348
|
+
const words2 = best[start] ?? Number.POSITIVE_INFINITY;
|
|
17349
|
+
if (words2 + 1 >= bestHere || !spellsVocabularyWord(joined.slice(start, end)))
|
|
17350
|
+
continue;
|
|
17351
|
+
bestHere = words2 + 1;
|
|
17352
|
+
best[end] = bestHere;
|
|
17353
|
+
prev[end] = start;
|
|
17354
|
+
}
|
|
17355
|
+
}
|
|
17356
|
+
if ((best[n] ?? Number.POSITIVE_INFINITY) === Number.POSITIVE_INFINITY)
|
|
17357
|
+
return null;
|
|
17358
|
+
const words = [];
|
|
17359
|
+
for (let end = n; end > 0; ) {
|
|
17360
|
+
const start = prev[end] ?? 0;
|
|
17361
|
+
words.push(joined.slice(start, end));
|
|
17362
|
+
end = start;
|
|
17363
|
+
}
|
|
17364
|
+
return words.reverse().join(" ");
|
|
17365
|
+
}
|
|
17366
|
+
function collapseSpacedRun(run) {
|
|
17367
|
+
const elements = [];
|
|
17368
|
+
for (const m of run.matchAll(SPACED_ELEMENT)) {
|
|
17369
|
+
elements.push({ ch: m[1] ?? "", gap: (m[2] ?? "").length });
|
|
17370
|
+
}
|
|
17371
|
+
const letters = elements.reduce((n, e) => n + (/[A-Za-z]/.test(e.ch) ? 1 : 0), 0);
|
|
17372
|
+
if (letters < SPACED_MIN_LETTERS)
|
|
17373
|
+
return run;
|
|
17374
|
+
const gaps = elements.slice(0, -1).map((e) => e.gap);
|
|
17375
|
+
const minGap = Math.min(...gaps);
|
|
17376
|
+
const uniform = gaps.every((g) => g === minGap);
|
|
17377
|
+
let out = "";
|
|
17378
|
+
elements.forEach((e, i) => {
|
|
17379
|
+
out += e.ch;
|
|
17380
|
+
if (i < elements.length - 1 && e.gap > minGap)
|
|
17381
|
+
out += " ";
|
|
17382
|
+
});
|
|
17383
|
+
return uniform ? segmentKnownWords(out) ?? out : out;
|
|
17384
|
+
}
|
|
17385
|
+
function collapseSpaced(text) {
|
|
17386
|
+
return text.replace(SPACED_RUN, collapseSpacedRun);
|
|
17387
|
+
}
|
|
17388
|
+
var LEET_MAP = {
|
|
17389
|
+
"0": "o",
|
|
17390
|
+
"3": "e",
|
|
17391
|
+
"4": "a",
|
|
17392
|
+
"5": "s",
|
|
17393
|
+
"7": "t",
|
|
17394
|
+
"@": "a",
|
|
17395
|
+
$: "s"
|
|
17396
|
+
};
|
|
17397
|
+
var WORD_TOKEN = /[A-Za-z0-9@$]+/g;
|
|
17398
|
+
var LETTER = /[A-Za-z]/;
|
|
17399
|
+
var LEET_CHARS = /[0-9@$]/;
|
|
17400
|
+
var LEET_MIN_TOKEN = 3;
|
|
17401
|
+
var NUMBER_WITH_SUFFIX = /^[0-9]+[A-Za-z]$/;
|
|
17402
|
+
function foldLeetToken(token) {
|
|
17403
|
+
if (token.length < LEET_MIN_TOKEN || !LETTER.test(token) || !LEET_CHARS.test(token)) {
|
|
17404
|
+
return token;
|
|
17405
|
+
}
|
|
17406
|
+
if (NUMBER_WITH_SUFFIX.test(token))
|
|
17407
|
+
return token;
|
|
17408
|
+
for (const one of ["i", "l"]) {
|
|
17409
|
+
let folded = "";
|
|
17410
|
+
for (const ch of token)
|
|
17411
|
+
folded += ch === "1" ? one : LEET_MAP[ch] ?? ch;
|
|
17412
|
+
if (instructionVocabulary().has(folded.toLowerCase()))
|
|
17413
|
+
return folded;
|
|
17414
|
+
if (!token.includes("1"))
|
|
17415
|
+
break;
|
|
17416
|
+
}
|
|
17417
|
+
return token;
|
|
17418
|
+
}
|
|
17419
|
+
function foldLeet(text) {
|
|
17420
|
+
return text.replace(WORD_TOKEN, foldLeetToken);
|
|
17421
|
+
}
|
|
17422
|
+
var BASE64_CANDIDATE = /(?<![A-Za-z0-9+/_-])[A-Za-z0-9+/_-]{16,}={0,2}(?![A-Za-z0-9+/=_-])/g;
|
|
17423
|
+
var BASE64_MIN_ALNUM_RATIO = 0.85;
|
|
17424
|
+
var HEX_CONTIGUOUS = /(?<![0-9A-Za-z])(?:[0-9A-Fa-f]{2}){8,}[0-9A-Fa-f]?(?![0-9A-Za-z])/g;
|
|
17425
|
+
var HEX_ESCAPES = /(?:\\x[0-9A-Fa-f]{2}){6,}/g;
|
|
17426
|
+
var HEX_SEPARATED = /(?<![0-9A-Za-z])[0-9A-Fa-f]{2}(?:[ :,-][0-9A-Fa-f]{2}){7,}(?![0-9A-Za-z])/g;
|
|
17427
|
+
var MIN_DECODED_BYTES = 8;
|
|
17428
|
+
var MIN_PRINTABLE_RATIO = 0.95;
|
|
17429
|
+
var MIN_WORDISH_RATIO = 0.6;
|
|
17430
|
+
var WORDISH = /[\p{L}\s]/u;
|
|
17431
|
+
var REPLACEMENT_CHAR = String.fromCharCode(65533);
|
|
17432
|
+
function decodeIfText(bytes) {
|
|
17433
|
+
if (bytes.length < MIN_DECODED_BYTES)
|
|
17434
|
+
return null;
|
|
17435
|
+
const text = bytes.toString("utf8");
|
|
17436
|
+
if (text.includes(REPLACEMENT_CHAR))
|
|
17437
|
+
return null;
|
|
17438
|
+
let total = 0;
|
|
17439
|
+
let printable = 0;
|
|
17440
|
+
let wordish = 0;
|
|
17441
|
+
for (const ch of text) {
|
|
17442
|
+
total++;
|
|
17443
|
+
const c = ch.codePointAt(0) ?? 0;
|
|
17444
|
+
if (c >= 32 && c < 127 || c === 9 || c === 10 || c === 13 || c >= 160) {
|
|
17445
|
+
printable++;
|
|
17446
|
+
}
|
|
17447
|
+
if (WORDISH.test(ch))
|
|
17448
|
+
wordish++;
|
|
17449
|
+
}
|
|
17450
|
+
if (total === 0)
|
|
17451
|
+
return null;
|
|
17452
|
+
if (printable / total < MIN_PRINTABLE_RATIO)
|
|
17453
|
+
return null;
|
|
17454
|
+
if (wordish / total < MIN_WORDISH_RATIO && !isHexDump(text))
|
|
17455
|
+
return null;
|
|
17456
|
+
return text;
|
|
17457
|
+
}
|
|
17458
|
+
function isHexDump(text) {
|
|
17459
|
+
const digits = text.replace(/\\x|[\s:,-]/g, "");
|
|
17460
|
+
return digits.length >= 16 && digits.length % 2 === 0 && /^[0-9A-Fa-f]+$/.test(digits);
|
|
17461
|
+
}
|
|
17462
|
+
function alnumRatio(s) {
|
|
17463
|
+
let n = 0;
|
|
17464
|
+
for (let i = 0; i < s.length; i++) {
|
|
17465
|
+
const c = s.charCodeAt(i);
|
|
17466
|
+
if (c >= 48 && c <= 57 || c >= 65 && c <= 90 || c >= 97 && c <= 122)
|
|
17467
|
+
n++;
|
|
17468
|
+
}
|
|
17469
|
+
return n / s.length;
|
|
17470
|
+
}
|
|
17471
|
+
function decodeCandidates(candidates, toBytes, budget) {
|
|
17472
|
+
const decoded = [];
|
|
17473
|
+
for (const candidate of candidates) {
|
|
17474
|
+
if (budget.bytesLeft < MIN_DECODED_BYTES)
|
|
17475
|
+
break;
|
|
17476
|
+
const bytes = toBytes(candidate);
|
|
17477
|
+
if (bytes === null || bytes.length > budget.bytesLeft)
|
|
17478
|
+
continue;
|
|
17479
|
+
const text = decodeIfText(bytes);
|
|
17480
|
+
if (text === null)
|
|
17481
|
+
continue;
|
|
17482
|
+
budget.bytesLeft -= bytes.length;
|
|
17483
|
+
decoded.push(text);
|
|
17484
|
+
}
|
|
17485
|
+
return decoded.length > 0 ? decoded.join("\n") : null;
|
|
17486
|
+
}
|
|
17487
|
+
function base64Bytes(candidate) {
|
|
17488
|
+
const token = candidate.replace(/=+$/, "");
|
|
17489
|
+
if (token.length % 4 === 1 || alnumRatio(token) < BASE64_MIN_ALNUM_RATIO)
|
|
17490
|
+
return null;
|
|
17491
|
+
if (!LETTER.test(token))
|
|
17492
|
+
return null;
|
|
17493
|
+
return Buffer.from(token.replace(/-/g, "+").replace(/_/g, "/"), "base64");
|
|
17494
|
+
}
|
|
17495
|
+
function hexBytes(candidate) {
|
|
17496
|
+
const digits = candidate.replace(/\\x|[^0-9A-Fa-f]/g, "");
|
|
17497
|
+
return Buffer.from(digits.length % 2 === 1 ? digits.slice(0, -1) : digits, "hex");
|
|
17498
|
+
}
|
|
17499
|
+
function decodeBase64Snippets(text, budget) {
|
|
17500
|
+
return decodeCandidates(text.matchAll(BASE64_CANDIDATE), (m) => base64Bytes(m[0]), budget);
|
|
17501
|
+
}
|
|
17502
|
+
function decodeHexSnippets(text, budget) {
|
|
17503
|
+
const matches = [HEX_CONTIGUOUS, HEX_ESCAPES, HEX_SEPARATED].flatMap((p) => [
|
|
17504
|
+
...text.matchAll(p)
|
|
17505
|
+
]);
|
|
17506
|
+
return decodeCandidates(matches, (m) => hexBytes(m[0]), budget);
|
|
17507
|
+
}
|
|
17508
|
+
function exhausted(run) {
|
|
17509
|
+
return Date.now() > run.deadline || run.variants.length >= run.limits.maxVariants;
|
|
17510
|
+
}
|
|
17511
|
+
function addVariant(run, text, techniques) {
|
|
17512
|
+
const { maxInputLength, maxVariants } = run.limits;
|
|
17513
|
+
const clamped = text.length > maxInputLength ? text.slice(0, maxInputLength) : text;
|
|
17514
|
+
if (run.seen.has(clamped) || run.variants.length >= maxVariants)
|
|
17515
|
+
return false;
|
|
17516
|
+
run.seen.add(clamped);
|
|
17517
|
+
run.variants.push({ text: clamped, techniques });
|
|
17518
|
+
return true;
|
|
17519
|
+
}
|
|
17520
|
+
function expand(run, text, depth, prefix) {
|
|
17521
|
+
let current = text;
|
|
17522
|
+
const applied = [...prefix];
|
|
17523
|
+
const folded = foldUnicode(current);
|
|
17524
|
+
if (folded !== current) {
|
|
17525
|
+
current = folded;
|
|
17526
|
+
applied.push("unicode");
|
|
17527
|
+
addVariant(run, current, [...applied]);
|
|
17528
|
+
}
|
|
17529
|
+
const decodeSource = current;
|
|
17530
|
+
if (!exhausted(run)) {
|
|
17531
|
+
const collapsed = collapseSpaced(current);
|
|
17532
|
+
if (collapsed !== current) {
|
|
17533
|
+
current = collapsed;
|
|
17534
|
+
applied.push("spaced");
|
|
17535
|
+
addVariant(run, current, [...applied]);
|
|
17536
|
+
}
|
|
17537
|
+
}
|
|
17538
|
+
if (!exhausted(run)) {
|
|
17539
|
+
const leetFolded = foldLeet(current);
|
|
17540
|
+
if (leetFolded !== current)
|
|
17541
|
+
addVariant(run, leetFolded, [...applied, "leet"]);
|
|
17542
|
+
}
|
|
17543
|
+
if (depth >= run.limits.maxDepth)
|
|
17544
|
+
return;
|
|
17545
|
+
const decoders = [
|
|
17546
|
+
["base64", decodeBase64Snippets],
|
|
17547
|
+
["hex", decodeHexSnippets]
|
|
17548
|
+
];
|
|
17549
|
+
for (const [technique, decode] of decoders) {
|
|
17550
|
+
if (exhausted(run))
|
|
17551
|
+
return;
|
|
17552
|
+
const decoded = decode(decodeSource, run.budget);
|
|
17553
|
+
if (decoded === null)
|
|
17554
|
+
continue;
|
|
17555
|
+
const techniques = [...prefix, technique];
|
|
17556
|
+
if (addVariant(run, decoded, techniques)) {
|
|
17557
|
+
expand(run, decoded, depth + 1, techniques);
|
|
17558
|
+
}
|
|
17559
|
+
}
|
|
17560
|
+
}
|
|
17561
|
+
function deobfuscate(input, limits = {}) {
|
|
17562
|
+
const resolved = { ...DEFAULT_DEOBFUSCATION_LIMITS, ...limits };
|
|
17563
|
+
const original = input.length > resolved.maxInputLength ? input.slice(0, resolved.maxInputLength) : input;
|
|
17564
|
+
const run = {
|
|
17565
|
+
limits: resolved,
|
|
17566
|
+
deadline: Date.now() + resolved.timeBudgetMs,
|
|
17567
|
+
seen: /* @__PURE__ */ new Set([original]),
|
|
17568
|
+
variants: [],
|
|
17569
|
+
budget: { bytesLeft: resolved.maxDecodedBytes }
|
|
17570
|
+
};
|
|
17571
|
+
if (!original)
|
|
17572
|
+
return run.variants;
|
|
17573
|
+
try {
|
|
17574
|
+
expand(run, original, 0, []);
|
|
17575
|
+
} catch {
|
|
17576
|
+
}
|
|
17577
|
+
return run.variants;
|
|
17578
|
+
}
|
|
17579
|
+
var DEOBFUSCATED_CONTEXT_PREFIX = "deobfuscated:";
|
|
17580
|
+
function deobfuscationContext(techniques) {
|
|
17581
|
+
return `${DEOBFUSCATED_CONTEXT_PREFIX}${techniques.join("+")}`;
|
|
17582
|
+
}
|
|
17583
|
+
|
|
17584
|
+
// ../core/dist/exceptions.js
|
|
17585
|
+
var import_node_crypto7 = require("node:crypto");
|
|
17586
|
+
var import_node_path15 = require("node:path");
|
|
17587
|
+
init_config();
|
|
17588
|
+
init_file_utils();
|
|
17589
|
+
|
|
17590
|
+
// ../core/dist/trusted-domains.js
|
|
17591
|
+
var import_promises7 = require("node:fs/promises");
|
|
17592
|
+
var import_node_path14 = require("node:path");
|
|
17593
|
+
var import_yaml = __toESM(require_dist(), 1);
|
|
17594
|
+
init_file_utils();
|
|
17595
|
+
init_types2();
|
|
17596
|
+
async function loadTrustedDomains(trustedDomainsDir, logger = nullLogger) {
|
|
17597
|
+
let files;
|
|
17598
|
+
try {
|
|
17599
|
+
files = (await (0, import_promises7.readdir)(trustedDomainsDir)).filter((f) => f.endsWith(".yaml")).sort();
|
|
17600
|
+
} catch {
|
|
17601
|
+
logger.debug("Trusted domains directory does not exist", { path: trustedDomainsDir });
|
|
17602
|
+
return [];
|
|
17603
|
+
}
|
|
17604
|
+
const domains = [];
|
|
17605
|
+
for (const filename of files) {
|
|
17606
|
+
const filePath = (0, import_node_path14.join)(trustedDomainsDir, filename);
|
|
17607
|
+
let content;
|
|
17608
|
+
try {
|
|
17609
|
+
content = await getFileContent(filePath);
|
|
17610
|
+
} catch (e) {
|
|
17611
|
+
logger.warn(`Failed to read ${filename}`, { error: String(e) });
|
|
17612
|
+
continue;
|
|
17613
|
+
}
|
|
17614
|
+
let data;
|
|
17615
|
+
try {
|
|
17616
|
+
data = (0, import_yaml.parse)(content);
|
|
17617
|
+
} catch (e) {
|
|
17132
17618
|
logger.warn(`Failed to parse ${filename}`, { error: String(e) });
|
|
17133
17619
|
continue;
|
|
17134
17620
|
}
|
|
@@ -17182,7 +17668,7 @@ init_types2();
|
|
|
17182
17668
|
var MAX_RULES_WARNING = 100;
|
|
17183
17669
|
var REGEX_TIMEOUT_MS = 50;
|
|
17184
17670
|
function computeRuleId(decision, match, pattern) {
|
|
17185
|
-
const hash = (0,
|
|
17671
|
+
const hash = (0, import_node_crypto7.createHash)("sha256").update(`${decision}:${match}:${pattern}`).digest("hex");
|
|
17186
17672
|
return hash.slice(0, 8);
|
|
17187
17673
|
}
|
|
17188
17674
|
async function loadExceptions(config2, logger = nullLogger) {
|
|
@@ -17548,93 +18034,8 @@ function matchesArtifact(rule, artifact) {
|
|
|
17548
18034
|
}
|
|
17549
18035
|
}
|
|
17550
18036
|
|
|
17551
|
-
// ../core/dist/extractors.js
|
|
17552
|
-
var URL_PATTERN = /https?:\/\/[^\s"')<>[\]{}]+/g;
|
|
17553
|
-
var TRAILING_PUNCT = /[.,;:!?]+$/;
|
|
17554
|
-
var MAX_CONTENT_SIZE = 64 * 1024;
|
|
17555
|
-
var BASE64_DECODE_EXEC = /\b(echo|printf|cat)\b[^|]*\|\s*base64\s+(-d|--decode)[^|]*\|\s*(bash|sh|zsh|ksh|dash|python|perl|ruby|node)\b/;
|
|
17556
|
-
var PRINTF_ENCODE_EXEC = /\bprintf\b\s+['"](\\x[0-9a-fA-F]{2}|\\[0-7]{3})[^|]*\|\s*(bash|sh|zsh|ksh|dash)\b/;
|
|
17557
|
-
var VAR_INTERPOLATION_URL = /\b(curl|wget|fetch)\b[^|;]*(\$\{?\w+\}?\.\$\{?\w+\}?|\$\{?\w+\}?:\/\/)/;
|
|
17558
|
-
var SHELLS = "bash|sh|zsh|ksh|dash";
|
|
17559
|
-
function extractUrls(text) {
|
|
17560
|
-
const seen = /* @__PURE__ */ new Set();
|
|
17561
|
-
const urls = [];
|
|
17562
|
-
for (const match of text.matchAll(URL_PATTERN)) {
|
|
17563
|
-
const url = match[0].replace(TRAILING_PUNCT, "");
|
|
17564
|
-
if (!seen.has(url)) {
|
|
17565
|
-
seen.add(url);
|
|
17566
|
-
urls.push(url);
|
|
17567
|
-
}
|
|
17568
|
-
}
|
|
17569
|
-
return urls;
|
|
17570
|
-
}
|
|
17571
|
-
function escapeRegExp(str) {
|
|
17572
|
-
const regExpEscape = RegExp.escape;
|
|
17573
|
-
if (regExpEscape) {
|
|
17574
|
-
return regExpEscape(str);
|
|
17575
|
-
}
|
|
17576
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17577
|
-
}
|
|
17578
|
-
function stripHeredocs(command) {
|
|
17579
|
-
return command.replace(/<<-?\s*['"]?(\w+)['"]?\n[\s\S]*?\n[ \t]*\1\b/g, "<<$1");
|
|
17580
|
-
}
|
|
17581
|
-
function extractFromBash(command) {
|
|
17582
|
-
const artifacts = [];
|
|
17583
|
-
const effective = stripHeredocs(command);
|
|
17584
|
-
artifacts.push({ type: "command", value: effective });
|
|
17585
|
-
if (BASE64_DECODE_EXEC.test(effective)) {
|
|
17586
|
-
artifacts.push({ type: "command", value: effective, context: "base64_decode_exec" });
|
|
17587
|
-
}
|
|
17588
|
-
if (PRINTF_ENCODE_EXEC.test(effective)) {
|
|
17589
|
-
artifacts.push({ type: "command", value: effective, context: "printf_encode_exec" });
|
|
17590
|
-
}
|
|
17591
|
-
if (VAR_INTERPOLATION_URL.test(effective)) {
|
|
17592
|
-
artifacts.push({ type: "command", value: effective, context: "variable_interpolation_url" });
|
|
17593
|
-
}
|
|
17594
|
-
for (const url of extractUrls(command)) {
|
|
17595
|
-
let context;
|
|
17596
|
-
const escaped = escapeRegExp(url);
|
|
17597
|
-
const directPipe = new RegExp(escaped + String.raw`[^|]*\|\s*(${SHELLS})\b`);
|
|
17598
|
-
const wrappedPipe = new RegExp(escaped + String.raw`[^|]*\|\s*\S+\s+(\S+\s+)?(${SHELLS})\b`);
|
|
17599
|
-
const subshellExec = new RegExp(`${String.raw`(\$\(|`}\`)${escaped}[^)\`]*${String.raw`\|\s*(${SHELLS})\b`}`);
|
|
17600
|
-
if (directPipe.test(command) || wrappedPipe.test(command) || subshellExec.test(command)) {
|
|
17601
|
-
context = "piped to shell";
|
|
17602
|
-
}
|
|
17603
|
-
artifacts.push({ type: "url", value: url, context });
|
|
17604
|
-
}
|
|
17605
|
-
return artifacts;
|
|
17606
|
-
}
|
|
17607
|
-
function extractFromWebFetch(toolInput) {
|
|
17608
|
-
const url = toolInput.url;
|
|
17609
|
-
if (url && typeof url === "string") {
|
|
17610
|
-
return [{ type: "url", value: url, context: "webfetch" }];
|
|
17611
|
-
}
|
|
17612
|
-
return [];
|
|
17613
|
-
}
|
|
17614
|
-
function extractFileArtifacts(toolInput, filePathKey, contentKey, contextName, urlContextName) {
|
|
17615
|
-
const artifacts = [];
|
|
17616
|
-
const filePath = toolInput[filePathKey];
|
|
17617
|
-
if (filePath && typeof filePath === "string") {
|
|
17618
|
-
artifacts.push({ type: "file_path", value: filePath, context: contextName });
|
|
17619
|
-
}
|
|
17620
|
-
const content = toolInput[contentKey];
|
|
17621
|
-
if (content && typeof content === "string" && content.trim()) {
|
|
17622
|
-
const capped = content.slice(0, MAX_CONTENT_SIZE);
|
|
17623
|
-
artifacts.push({ type: "content", value: capped, context: contextName });
|
|
17624
|
-
for (const url of extractUrls(capped)) {
|
|
17625
|
-
artifacts.push({ type: "url", value: url, context: urlContextName });
|
|
17626
|
-
}
|
|
17627
|
-
}
|
|
17628
|
-
return artifacts;
|
|
17629
|
-
}
|
|
17630
|
-
function extractFromWrite(toolInput) {
|
|
17631
|
-
return extractFileArtifacts(toolInput, "file_path", "content", "write", "from_file_content");
|
|
17632
|
-
}
|
|
17633
|
-
function extractFromEdit(toolInput) {
|
|
17634
|
-
return extractFileArtifacts(toolInput, "file_path", "new_string", "edit", "from_edit_content");
|
|
17635
|
-
}
|
|
17636
|
-
|
|
17637
18037
|
// ../core/dist/heuristics.js
|
|
18038
|
+
var MAX_VARIANTS_PER_CALL = 16;
|
|
17638
18039
|
var TRUSTED_DOMAIN_SUPPRESSIBLE = /* @__PURE__ */ new Set([
|
|
17639
18040
|
"CLT-CMD-001",
|
|
17640
18041
|
"CLT-CMD-002",
|
|
@@ -17695,6 +18096,51 @@ var HeuristicsEngine = class {
|
|
|
17695
18096
|
}
|
|
17696
18097
|
return matches;
|
|
17697
18098
|
}
|
|
18099
|
+
/**
|
|
18100
|
+
* Match deobfuscated variants of the `content` artifacts against this
|
|
18101
|
+
* engine's prompt-injection rules only. Command, credential, URL and other
|
|
18102
|
+
* rules never see decoded or folded text, so deobfuscation adds coverage for
|
|
18103
|
+
* hidden instructions without widening any other rule's false-positive surface.
|
|
18104
|
+
*
|
|
18105
|
+
* A variant match is attributed to the *original* artifact (`artifact`), and
|
|
18106
|
+
* only the short matched snippet (`matchValue`) comes from decoded text; the
|
|
18107
|
+
* decoded variant itself is never stored or passed on.
|
|
18108
|
+
*
|
|
18109
|
+
* Each rule is reported at most once, and not at all when its ID is in
|
|
18110
|
+
* `alreadyMatched` (the raw text already tripped it), so audit and telemetry
|
|
18111
|
+
* signals are not duplicated.
|
|
18112
|
+
*/
|
|
18113
|
+
matchDeobfuscated(artifacts, alreadyMatched = /* @__PURE__ */ new Set()) {
|
|
18114
|
+
const piThreats = (this.threatMap.get("content") ?? []).filter((t) => t.category === "prompt_injection");
|
|
18115
|
+
if (piThreats.length === 0)
|
|
18116
|
+
return [];
|
|
18117
|
+
const matches = [];
|
|
18118
|
+
const reported = new Set(alreadyMatched);
|
|
18119
|
+
let variantsLeft = MAX_VARIANTS_PER_CALL;
|
|
18120
|
+
for (const artifact of artifacts) {
|
|
18121
|
+
if (artifact.type !== "content")
|
|
18122
|
+
continue;
|
|
18123
|
+
for (const variant of deobfuscate(artifact.value)) {
|
|
18124
|
+
if (variantsLeft-- <= 0)
|
|
18125
|
+
return matches;
|
|
18126
|
+
for (const threat of piThreats) {
|
|
18127
|
+
if (reported.has(threat.id))
|
|
18128
|
+
continue;
|
|
18129
|
+
const m = threat.compiledPattern.exec(variant.text);
|
|
18130
|
+
if (!m)
|
|
18131
|
+
continue;
|
|
18132
|
+
reported.add(threat.id);
|
|
18133
|
+
matches.push({
|
|
18134
|
+
threat,
|
|
18135
|
+
artifact: artifact.value,
|
|
18136
|
+
matchValue: m[0],
|
|
18137
|
+
context: deobfuscationContext(variant.techniques)
|
|
18138
|
+
});
|
|
18139
|
+
}
|
|
18140
|
+
}
|
|
18141
|
+
}
|
|
18142
|
+
return matches;
|
|
18143
|
+
}
|
|
17698
18144
|
};
|
|
17699
18145
|
|
|
17700
18146
|
// ../core/dist/package-checker.js
|
|
@@ -18137,7 +18583,7 @@ function extractFromPackageJson(content) {
|
|
|
18137
18583
|
continue;
|
|
18138
18584
|
if (version === "" || version === "*")
|
|
18139
18585
|
continue;
|
|
18140
|
-
if (version.startsWith("file:") || version.startsWith("link:") || version.startsWith("git+") || version.startsWith("git:") || version.startsWith("http:") || version.startsWith("https:") || version.startsWith("workspace:") || version.startsWith("github:") || version.startsWith("bitbucket:") || version.startsWith("gitlab:") || version.startsWith("portal:") || version.startsWith("patch:"))
|
|
18586
|
+
if (version.startsWith("catalog:") || version.startsWith("file:") || version.startsWith("link:") || version.startsWith("git+") || version.startsWith("git:") || version.startsWith("http:") || version.startsWith("https:") || version.startsWith("workspace:") || version.startsWith("github:") || version.startsWith("bitbucket:") || version.startsWith("gitlab:") || version.startsWith("portal:") || version.startsWith("patch:"))
|
|
18141
18587
|
continue;
|
|
18142
18588
|
if (version.startsWith("npm:")) {
|
|
18143
18589
|
const aliasTarget = version.slice(4);
|
|
@@ -18234,12 +18680,98 @@ async function updateSessionStatus(sessionId, verdict) {
|
|
|
18234
18680
|
await atomicWriteJson(statusFilePath(sessionId), status);
|
|
18235
18681
|
}
|
|
18236
18682
|
|
|
18237
|
-
// ../core/dist/
|
|
18238
|
-
var
|
|
18683
|
+
// ../core/dist/submit-limits.js
|
|
18684
|
+
var import_promises8 = require("node:fs/promises");
|
|
18239
18685
|
var import_node_path17 = require("node:path");
|
|
18686
|
+
init_config();
|
|
18687
|
+
init_file_utils();
|
|
18688
|
+
init_types2();
|
|
18689
|
+
var SUBMIT_LIMITS_SCHEMA_VERSION = 1;
|
|
18690
|
+
var SUBMIT_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
18691
|
+
function submitLimitsPath(sageDirPath) {
|
|
18692
|
+
return (0, import_node_path17.join)(sageDirPath ?? resolvePath(SAGE_DIR), "submit-limits.json");
|
|
18693
|
+
}
|
|
18694
|
+
function isEligible(limits, ruleId, now) {
|
|
18695
|
+
const last = limits[ruleId];
|
|
18696
|
+
if (last === void 0)
|
|
18697
|
+
return true;
|
|
18698
|
+
return now - last >= SUBMIT_WINDOW_MS || last > now;
|
|
18699
|
+
}
|
|
18700
|
+
async function loadSubmitLimits(opts = {}) {
|
|
18701
|
+
const logger = opts.logger ?? nullLogger;
|
|
18702
|
+
let raw;
|
|
18703
|
+
try {
|
|
18704
|
+
raw = await getFileContent(submitLimitsPath(opts.sageDirPath));
|
|
18705
|
+
} catch {
|
|
18706
|
+
return {};
|
|
18707
|
+
}
|
|
18708
|
+
try {
|
|
18709
|
+
const data = JSON.parse(raw);
|
|
18710
|
+
const rules = data.rules ?? {};
|
|
18711
|
+
const limits = {};
|
|
18712
|
+
for (const [id, rec] of Object.entries(rules)) {
|
|
18713
|
+
const last = rec?.last_submitted_ms;
|
|
18714
|
+
if (typeof last === "number" && Number.isFinite(last))
|
|
18715
|
+
limits[id] = last;
|
|
18716
|
+
}
|
|
18717
|
+
return limits;
|
|
18718
|
+
} catch (e) {
|
|
18719
|
+
logger.warn("Failed to parse submit limits; treating as empty", { error: String(e) });
|
|
18720
|
+
return {};
|
|
18721
|
+
}
|
|
18722
|
+
}
|
|
18723
|
+
async function saveSubmitLimits(limits, sageDirPath) {
|
|
18724
|
+
await atomicWriteJson(submitLimitsPath(sageDirPath), {
|
|
18725
|
+
schema_version: SUBMIT_LIMITS_SCHEMA_VERSION,
|
|
18726
|
+
rules: Object.fromEntries(Object.entries(limits).map(([id, last]) => [id, { last_submitted_ms: last }]))
|
|
18727
|
+
});
|
|
18728
|
+
}
|
|
18729
|
+
async function claimRuleSubmissions(ruleIds, opts = {}) {
|
|
18730
|
+
const logger = opts.logger ?? nullLogger;
|
|
18731
|
+
const unique = [...new Set(ruleIds)];
|
|
18732
|
+
if (unique.length === 0)
|
|
18733
|
+
return [];
|
|
18734
|
+
const now = opts.now ?? Date.now();
|
|
18735
|
+
const path = submitLimitsPath(opts.sageDirPath);
|
|
18736
|
+
let claimed = [];
|
|
18737
|
+
try {
|
|
18738
|
+
await (0, import_promises8.mkdir)((0, import_node_path17.dirname)(path), { recursive: true });
|
|
18739
|
+
await withFileLock(path, async () => {
|
|
18740
|
+
const limits = await loadSubmitLimits(opts);
|
|
18741
|
+
const eligible = opts.force ? unique : unique.filter((id) => isEligible(limits, id, now));
|
|
18742
|
+
if (eligible.length === 0)
|
|
18743
|
+
return;
|
|
18744
|
+
const next = {};
|
|
18745
|
+
for (const [id, last] of Object.entries(limits)) {
|
|
18746
|
+
if (isEligible(limits, id, now))
|
|
18747
|
+
continue;
|
|
18748
|
+
next[id] = last;
|
|
18749
|
+
}
|
|
18750
|
+
for (const id of eligible)
|
|
18751
|
+
next[id] = now;
|
|
18752
|
+
await saveSubmitLimits(next, opts.sageDirPath);
|
|
18753
|
+
claimed = eligible;
|
|
18754
|
+
});
|
|
18755
|
+
} catch (e) {
|
|
18756
|
+
logger.warn("Failed to persist submit limits; suppressing submission", { error: String(e) });
|
|
18757
|
+
return [];
|
|
18758
|
+
}
|
|
18759
|
+
if (claimed.length === 0)
|
|
18760
|
+
logger.debug("Rule submissions capped", { ruleIds: unique });
|
|
18761
|
+
return claimed;
|
|
18762
|
+
}
|
|
18763
|
+
|
|
18764
|
+
// ../core/dist/threat-loader.js
|
|
18765
|
+
var import_promises9 = require("node:fs/promises");
|
|
18766
|
+
var import_node_path18 = require("node:path");
|
|
18240
18767
|
var import_yaml2 = __toESM(require_dist(), 1);
|
|
18768
|
+
init_config();
|
|
18241
18769
|
init_file_utils();
|
|
18242
18770
|
init_types2();
|
|
18771
|
+
var MAX_THREAT_RULES = 1e4;
|
|
18772
|
+
function getUserThreatsDir(sageDirPath) {
|
|
18773
|
+
return (0, import_node_path18.join)(resolvePath(sageDirPath ?? SAGE_DIR), "threats");
|
|
18774
|
+
}
|
|
18243
18775
|
var REQUIRED_FIELDS = /* @__PURE__ */ new Set([
|
|
18244
18776
|
"id",
|
|
18245
18777
|
"version",
|
|
@@ -18298,13 +18830,21 @@ function isExpired(entry) {
|
|
|
18298
18830
|
return false;
|
|
18299
18831
|
return Date.now() > expiresAt.getTime();
|
|
18300
18832
|
}
|
|
18301
|
-
async function
|
|
18833
|
+
async function loadThreatDirectory(threatDir, logger, { custom: custom2 = false, customThreatIds = /* @__PURE__ */ new Set(), bundledThreatIds = /* @__PURE__ */ new Set(), maxRules = MAX_THREAT_RULES, logCustomRuleIds = false, optional = false } = {}) {
|
|
18302
18834
|
const threats = [];
|
|
18303
18835
|
let files;
|
|
18304
18836
|
try {
|
|
18305
|
-
files = (await (0,
|
|
18306
|
-
} catch {
|
|
18307
|
-
|
|
18837
|
+
files = (await (0, import_promises9.readdir)(threatDir)).filter((f) => f.endsWith(".yaml")).sort();
|
|
18838
|
+
} catch (error) {
|
|
18839
|
+
const code = error.code;
|
|
18840
|
+
if (optional && code === "ENOENT") {
|
|
18841
|
+
logger.debug("Custom threat directory does not exist", { path: threatDir });
|
|
18842
|
+
return threats;
|
|
18843
|
+
}
|
|
18844
|
+
logger.warn("Threat directory does not exist or is unreadable", {
|
|
18845
|
+
path: threatDir,
|
|
18846
|
+
error: String(error)
|
|
18847
|
+
});
|
|
18308
18848
|
return threats;
|
|
18309
18849
|
}
|
|
18310
18850
|
const ruleFiles = [];
|
|
@@ -18312,7 +18852,7 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18312
18852
|
for (const filename of files) {
|
|
18313
18853
|
let content;
|
|
18314
18854
|
try {
|
|
18315
|
-
content = await getFileContent((0,
|
|
18855
|
+
content = await getFileContent((0, import_node_path18.join)(threatDir, filename));
|
|
18316
18856
|
} catch (e) {
|
|
18317
18857
|
logger.warn(`Failed to read ${filename}`, { error: String(e) });
|
|
18318
18858
|
continue;
|
|
@@ -18338,8 +18878,15 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18338
18878
|
}
|
|
18339
18879
|
ruleFiles.push({ filename, data });
|
|
18340
18880
|
}
|
|
18341
|
-
for (const { filename, data } of ruleFiles) {
|
|
18881
|
+
ruleFilesLoop: for (const { filename, data } of ruleFiles) {
|
|
18342
18882
|
for (const entry of data) {
|
|
18883
|
+
if (threats.length >= maxRules) {
|
|
18884
|
+
logger.warn("Threat rule limit reached; ignoring remaining rules", {
|
|
18885
|
+
path: threatDir,
|
|
18886
|
+
maxRules: MAX_THREAT_RULES
|
|
18887
|
+
});
|
|
18888
|
+
break ruleFilesLoop;
|
|
18889
|
+
}
|
|
18343
18890
|
if (typeof entry !== "object" || entry === null) {
|
|
18344
18891
|
logger.warn(`Skipping non-object entry in ${filename}`);
|
|
18345
18892
|
continue;
|
|
@@ -18351,6 +18898,14 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18351
18898
|
logger.warn(`Skipping threat in ${filename}: missing fields ${missing.join(", ")}`);
|
|
18352
18899
|
continue;
|
|
18353
18900
|
}
|
|
18901
|
+
if (typeof record.id !== "string" || record.id.length === 0) {
|
|
18902
|
+
logger.warn(`Skipping threat in ${filename}: invalid id`, { id: record.id });
|
|
18903
|
+
continue;
|
|
18904
|
+
}
|
|
18905
|
+
if (custom2 && customThreatIds.has(record.id)) {
|
|
18906
|
+
logger.warn(bundledThreatIds.has(record.id) ? `Skipping custom threat ${record.id}: id conflicts with a bundled rule` : `Skipping custom threat ${record.id}: duplicate custom rule id`, { id: record.id, path: (0, import_node_path18.join)(threatDir, filename) });
|
|
18907
|
+
continue;
|
|
18908
|
+
}
|
|
18354
18909
|
if (record.revoked === true)
|
|
18355
18910
|
continue;
|
|
18356
18911
|
if (isExpired(record))
|
|
@@ -18392,7 +18947,7 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18392
18947
|
});
|
|
18393
18948
|
continue;
|
|
18394
18949
|
}
|
|
18395
|
-
|
|
18950
|
+
const threat = {
|
|
18396
18951
|
id: record.id,
|
|
18397
18952
|
version,
|
|
18398
18953
|
detectionName,
|
|
@@ -18405,13 +18960,41 @@ async function loadThreats(threatDir, logger = nullLogger) {
|
|
|
18405
18960
|
title: record.title,
|
|
18406
18961
|
expiresAt: parseExpiresAt(record.expires_at),
|
|
18407
18962
|
revoked: false,
|
|
18408
|
-
flags
|
|
18409
|
-
|
|
18963
|
+
flags,
|
|
18964
|
+
customRule: custom2
|
|
18965
|
+
};
|
|
18966
|
+
threats.push(threat);
|
|
18967
|
+
if (custom2) {
|
|
18968
|
+
customThreatIds.add(threat.id);
|
|
18969
|
+
if (logCustomRuleIds) {
|
|
18970
|
+
logger.info("Loaded custom threat rule", {
|
|
18971
|
+
id: threat.id,
|
|
18972
|
+
path: (0, import_node_path18.join)(threatDir, filename)
|
|
18973
|
+
});
|
|
18974
|
+
}
|
|
18975
|
+
}
|
|
18410
18976
|
}
|
|
18411
18977
|
}
|
|
18412
18978
|
logger.debug(`Loaded ${threats.length} threats from ${threatDir}`);
|
|
18413
18979
|
return threats;
|
|
18414
18980
|
}
|
|
18981
|
+
async function loadThreats(threatDir, logger = nullLogger, options = {}) {
|
|
18982
|
+
const bundledThreats = await loadThreatDirectory(threatDir, logger);
|
|
18983
|
+
if (!options.customThreatsDir || (0, import_node_path18.resolve)(options.customThreatsDir) === (0, import_node_path18.resolve)(threatDir)) {
|
|
18984
|
+
return bundledThreats;
|
|
18985
|
+
}
|
|
18986
|
+
const bundledThreatIds = new Set(bundledThreats.map((threat) => threat.id));
|
|
18987
|
+
const customThreatIds = new Set(bundledThreatIds);
|
|
18988
|
+
const customThreats = await loadThreatDirectory(options.customThreatsDir, logger, {
|
|
18989
|
+
custom: true,
|
|
18990
|
+
customThreatIds,
|
|
18991
|
+
bundledThreatIds,
|
|
18992
|
+
maxRules: Math.max(0, MAX_THREAT_RULES - bundledThreats.length),
|
|
18993
|
+
logCustomRuleIds: options.logCustomRuleIds,
|
|
18994
|
+
optional: true
|
|
18995
|
+
});
|
|
18996
|
+
return [...bundledThreats, ...customThreats];
|
|
18997
|
+
}
|
|
18415
18998
|
|
|
18416
18999
|
// ../core/dist/evaluator.js
|
|
18417
19000
|
init_types2();
|
|
@@ -18419,6 +19002,8 @@ var AMSI_CONTENT_SNIPPET_MAX = 200;
|
|
|
18419
19002
|
var AMSI_CONTENT_NAME_MAX = 256;
|
|
18420
19003
|
var HEURISTIC_PI_CONTENT_SNIPPET_MAX = 200;
|
|
18421
19004
|
function buildHeuristicSignal(match) {
|
|
19005
|
+
if (match.threat.customRule)
|
|
19006
|
+
return void 0;
|
|
18422
19007
|
return {
|
|
18423
19008
|
detection_name: formatReportingDetectionName(match.threat.detectionName, "heuristics", `${match.threat.id}:${match.threat.version}`),
|
|
18424
19009
|
rule_id: match.threat.id,
|
|
@@ -18434,7 +19019,7 @@ function resolvePiContentSnippet(piResults, heuristicMatches) {
|
|
|
18434
19019
|
}
|
|
18435
19020
|
if (highestRiskResult?.contentSnippet)
|
|
18436
19021
|
return scrubHomePath(highestRiskResult.contentSnippet);
|
|
18437
|
-
const heuristicMatch = heuristicMatches.find((match) => match.threat.category === "prompt_injection");
|
|
19022
|
+
const heuristicMatch = heuristicMatches.find((match) => !match.threat.customRule && match.threat.category === "prompt_injection");
|
|
18438
19023
|
if (!heuristicMatch)
|
|
18439
19024
|
return void 0;
|
|
18440
19025
|
const snippet = safeTruncate(scrubHomePath(heuristicMatch.matchValue), HEURISTIC_PI_CONTENT_SNIPPET_MAX);
|
|
@@ -18485,7 +19070,7 @@ function shouldSkipPromptInjectionForLocalMarkdown(request) {
|
|
|
18485
19070
|
async function evaluateToolCall(request, context) {
|
|
18486
19071
|
const logger = context.logger ?? nullLogger;
|
|
18487
19072
|
const config2 = await resolveEvaluationConfig(context, logger);
|
|
18488
|
-
const eventId = (0,
|
|
19073
|
+
const eventId = (0, import_node_crypto8.randomUUID)();
|
|
18489
19074
|
logger.debug("Tool call evaluation started", {
|
|
18490
19075
|
eventId,
|
|
18491
19076
|
toolUseId: request.toolUseId,
|
|
@@ -18582,7 +19167,9 @@ async function evaluateToolCall(request, context) {
|
|
|
18582
19167
|
const { cachedUrlVerdicts, uncachedUrls } = partitionUrlsByCache(urls, cache2);
|
|
18583
19168
|
let heuristicMatches = [];
|
|
18584
19169
|
if (config2.heuristics_enabled) {
|
|
18585
|
-
let threats = await loadThreats(context.threatsDir, logger
|
|
19170
|
+
let threats = await loadThreats(context.threatsDir, logger, {
|
|
19171
|
+
customThreatsDir: getUserThreatsDir(context.sageDirPath)
|
|
19172
|
+
});
|
|
18586
19173
|
if (config2.disabled_threats.length > 0) {
|
|
18587
19174
|
const disabledSet = new Set(config2.disabled_threats);
|
|
18588
19175
|
threats = threats.filter((t) => !disabledSet.has(t.id));
|
|
@@ -18593,6 +19180,8 @@ async function evaluateToolCall(request, context) {
|
|
|
18593
19180
|
const trustedDomains = await loadTrustedDomains(context.trustedDomainsDir, logger);
|
|
18594
19181
|
const heuristics = new HeuristicsEngine(threats, trustedDomains);
|
|
18595
19182
|
heuristicMatches = heuristics.match(request.artifacts);
|
|
19183
|
+
const rawMatchedIds = new Set(heuristicMatches.map((m) => m.threat.id));
|
|
19184
|
+
heuristicMatches.push(...heuristics.matchDeobfuscated(request.artifacts, rawMatchedIds));
|
|
18596
19185
|
}
|
|
18597
19186
|
let urlCheckResults = [];
|
|
18598
19187
|
const urlsToCheck = cache2 ? uncachedUrls : urls;
|
|
@@ -18669,12 +19258,13 @@ async function evaluateToolCall(request, context) {
|
|
|
18669
19258
|
}
|
|
18670
19259
|
}
|
|
18671
19260
|
const engine = new DecisionEngine(config2.sensitivity, logger);
|
|
18672
|
-
const
|
|
19261
|
+
const preliminarySources = {
|
|
18673
19262
|
heuristicMatches,
|
|
18674
19263
|
urlCheckResults,
|
|
18675
19264
|
packageCheckResults: packageCheckResults.length > 0 ? packageCheckResults : void 0,
|
|
18676
19265
|
amsiCheckResults: amsiCheckResults.length > 0 ? amsiCheckResults : void 0
|
|
18677
|
-
}
|
|
19266
|
+
};
|
|
19267
|
+
const preliminaryVerdict = await engine.decide(preliminarySources);
|
|
18678
19268
|
const allPiResults = [];
|
|
18679
19269
|
const alreadyDenied = preliminaryVerdict.decision === "deny" || cachedUrlVerdicts.size > 0 && [...cachedUrlVerdicts.values()].some((v) => v.verdict === "deny");
|
|
18680
19270
|
if (config2.pi_check.enabled && !alreadyDenied && request.toolName === "WebFetch") {
|
|
@@ -18708,18 +19298,17 @@ async function evaluateToolCall(request, context) {
|
|
|
18708
19298
|
}
|
|
18709
19299
|
}
|
|
18710
19300
|
let verdict;
|
|
19301
|
+
let verdictSources = preliminarySources;
|
|
18711
19302
|
if (allPiResults.length > 0) {
|
|
18712
|
-
|
|
18713
|
-
|
|
18714
|
-
urlCheckResults,
|
|
18715
|
-
packageCheckResults: packageCheckResults.length > 0 ? packageCheckResults : void 0,
|
|
18716
|
-
amsiCheckResults: amsiCheckResults.length > 0 ? amsiCheckResults : void 0,
|
|
19303
|
+
verdictSources = {
|
|
19304
|
+
...preliminarySources,
|
|
18717
19305
|
piCheckResults: allPiResults,
|
|
18718
19306
|
piThresholds: {
|
|
18719
19307
|
highRisk: DEFAULT_PI_HIGH_RISK_THRESHOLD,
|
|
18720
19308
|
mediumRisk: DEFAULT_PI_MEDIUM_RISK_THRESHOLD
|
|
18721
19309
|
}
|
|
18722
|
-
}
|
|
19310
|
+
};
|
|
19311
|
+
verdict = await engine.decide(verdictSources);
|
|
18723
19312
|
} else {
|
|
18724
19313
|
verdict = preliminaryVerdict;
|
|
18725
19314
|
}
|
|
@@ -18744,10 +19333,19 @@ async function evaluateToolCall(request, context) {
|
|
|
18744
19333
|
}
|
|
18745
19334
|
}
|
|
18746
19335
|
}
|
|
19336
|
+
const customThreatMatched = heuristicMatches.some((match) => match.threat.customRule === true);
|
|
19337
|
+
const hasCachedUrlDetection = [...cachedUrlVerdicts.values()].some((cachedVerdict) => cachedVerdict.verdict !== "allow");
|
|
19338
|
+
const nonCustomThreatMatched = customThreatMatched && (engine.hasNonCustomDetection(verdictSources) || hasCachedUrlDetection);
|
|
19339
|
+
verdict.customThreatMatched = customThreatMatched;
|
|
18747
19340
|
await cacheUrlResults(urlCheckResults, cache2);
|
|
18748
19341
|
const auditSignals = {};
|
|
18749
19342
|
if (heuristicMatches.length > 0) {
|
|
18750
|
-
|
|
19343
|
+
const heuristicSignals = heuristicMatches.flatMap((match) => {
|
|
19344
|
+
const signal = buildHeuristicSignal(match);
|
|
19345
|
+
return signal ? [signal] : [];
|
|
19346
|
+
});
|
|
19347
|
+
if (heuristicSignals.length > 0)
|
|
19348
|
+
auditSignals.heuristics = heuristicSignals;
|
|
18751
19349
|
}
|
|
18752
19350
|
if (urlCheckResults.length > 0) {
|
|
18753
19351
|
const relevant = urlCheckResults.filter((r) => r.isMalicious);
|
|
@@ -18795,7 +19393,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18795
19393
|
if (allPiResults.length > 0) {
|
|
18796
19394
|
const piSnippetFloor = DEFAULT_PI_MEDIUM_RISK_THRESHOLD;
|
|
18797
19395
|
auditSignals.pi_checks = allPiResults.map((r) => ({
|
|
18798
|
-
detection_name:
|
|
19396
|
+
detection_name: piReportingDetectionName(r),
|
|
18799
19397
|
risk: r.risk,
|
|
18800
19398
|
model_id: r.modelId,
|
|
18801
19399
|
content_name: r.contentName,
|
|
@@ -18818,6 +19416,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18818
19416
|
toolName: request.toolName,
|
|
18819
19417
|
toolInput: request.toolInput,
|
|
18820
19418
|
verdict,
|
|
19419
|
+
nonCustomThreatMatched,
|
|
18821
19420
|
conversationId: request.conversationId,
|
|
18822
19421
|
agentRuntime: request.agentRuntime,
|
|
18823
19422
|
hookType: request.hookType,
|
|
@@ -18830,7 +19429,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18830
19429
|
} catch (error) {
|
|
18831
19430
|
logger.debug("Audit verdict logging failed open", { error: String(error) });
|
|
18832
19431
|
}
|
|
18833
|
-
const submitTelemetry = async (blocking) => {
|
|
19432
|
+
const submitTelemetry = async (blocking, signals = resolvedSignals) => {
|
|
18834
19433
|
try {
|
|
18835
19434
|
await sendCommunityIqTelemetry({
|
|
18836
19435
|
eventId,
|
|
@@ -18840,7 +19439,7 @@ async function evaluateToolCall(request, context) {
|
|
|
18840
19439
|
toolName: request.toolName,
|
|
18841
19440
|
content: resolvedContent,
|
|
18842
19441
|
contentSnippet,
|
|
18843
|
-
signals
|
|
19442
|
+
signals,
|
|
18844
19443
|
communityIqEnabled: config2.community_iq,
|
|
18845
19444
|
config: config2,
|
|
18846
19445
|
blocking,
|
|
@@ -18851,9 +19450,18 @@ async function evaluateToolCall(request, context) {
|
|
|
18851
19450
|
}
|
|
18852
19451
|
};
|
|
18853
19452
|
if (verdict.decision === "deny") {
|
|
18854
|
-
|
|
18855
|
-
|
|
18856
|
-
|
|
19453
|
+
if (customThreatMatched && !nonCustomThreatMatched) {
|
|
19454
|
+
logger.debug("Custom threat matched without non-custom detections; skipping Community IQ telemetry", { eventId });
|
|
19455
|
+
} else {
|
|
19456
|
+
await submitTelemetry(true);
|
|
19457
|
+
}
|
|
19458
|
+
} else {
|
|
19459
|
+
const piSuspicious = allPiResults.some((r) => r.risk >= DEFAULT_PI_TELEMETRY_THRESHOLD && r.risk < DEFAULT_PI_HIGH_RISK_THRESHOLD);
|
|
19460
|
+
const submitOnlyIds = new Set(heuristicMatches.filter((m) => !m.threat.customRule && isSubmitOnlyThreat(m.threat)).map((m) => m.threat.id));
|
|
19461
|
+
const claimedRuleIds = await claimSubmitOnlySubmissions(submitOnlyIds, config2, logger);
|
|
19462
|
+
if (piSuspicious || claimedRuleIds.size > 0) {
|
|
19463
|
+
await submitTelemetry(false, withoutCappedSubmitOnly(resolvedSignals, submitOnlyIds, claimedRuleIds));
|
|
19464
|
+
}
|
|
18857
19465
|
}
|
|
18858
19466
|
if (verdict.decision !== "allow") {
|
|
18859
19467
|
try {
|
|
@@ -18868,6 +19476,26 @@ async function evaluateToolCall(request, context) {
|
|
|
18868
19476
|
logEvaluationCompleted(verdict);
|
|
18869
19477
|
return verdict;
|
|
18870
19478
|
}
|
|
19479
|
+
async function claimSubmitOnlySubmissions(submitOnlyIds, config2, logger) {
|
|
19480
|
+
if (!config2.community_iq || submitOnlyIds.size === 0)
|
|
19481
|
+
return /* @__PURE__ */ new Set();
|
|
19482
|
+
try {
|
|
19483
|
+
return new Set(await claimRuleSubmissions([...submitOnlyIds], { logger }));
|
|
19484
|
+
} catch (error) {
|
|
19485
|
+
logger.debug("Submit-limit claim failed open", { error: String(error) });
|
|
19486
|
+
return /* @__PURE__ */ new Set();
|
|
19487
|
+
}
|
|
19488
|
+
}
|
|
19489
|
+
function withoutCappedSubmitOnly(signals, submitOnlyIds, claimedRuleIds) {
|
|
19490
|
+
if (!signals?.heuristics)
|
|
19491
|
+
return signals;
|
|
19492
|
+
const heuristics = signals.heuristics.filter((h) => !submitOnlyIds.has(h.rule_id) || claimedRuleIds.has(h.rule_id));
|
|
19493
|
+
if (heuristics.length === signals.heuristics.length)
|
|
19494
|
+
return signals;
|
|
19495
|
+
const { heuristics: _dropped, ...rest } = signals;
|
|
19496
|
+
const next = heuristics.length > 0 ? { ...rest, heuristics } : rest;
|
|
19497
|
+
return Object.keys(next).length > 0 ? next : void 0;
|
|
19498
|
+
}
|
|
18871
19499
|
function partitionUrlsByCache(urls, cache2) {
|
|
18872
19500
|
const cachedUrlVerdicts = /* @__PURE__ */ new Map();
|
|
18873
19501
|
let uncachedUrls = [];
|
|
@@ -19035,14 +19663,19 @@ function ruleLabel(verdict) {
|
|
|
19035
19663
|
return null;
|
|
19036
19664
|
return verdict.source === "exception" ? `${verdict.matchedThreatId} (your exception rule)` : verdict.matchedThreatId;
|
|
19037
19665
|
}
|
|
19666
|
+
var FP_REPORT_TOOL_NAME = "sage_report_false_positive";
|
|
19038
19667
|
function remediationHint(verdict) {
|
|
19039
19668
|
if (verdict.source === "exception") {
|
|
19040
19669
|
const id = verdict.matchedThreatId ? ` (id ${verdict.matchedThreatId})` : "";
|
|
19041
19670
|
return `This block comes from your own rule in ~/.sage/exceptions.json${id} \u2014 edit or remove it there.`;
|
|
19042
19671
|
}
|
|
19672
|
+
if (verdict.customThreat) {
|
|
19673
|
+
return "This match comes from your custom threat rules in ~/.sage/threats/ \u2014 edit or remove the matching rule there.";
|
|
19674
|
+
}
|
|
19043
19675
|
const rule = verdict.matchedThreatId ? ` (rule ${verdict.matchedThreatId})` : "";
|
|
19044
|
-
return `If this is a false positive,
|
|
19676
|
+
return `If this is a false positive, ask your agent to report it with the ${FP_REPORT_TOOL_NAME} MCP tool${rule}.`;
|
|
19045
19677
|
}
|
|
19678
|
+
var SESSION_START_FP_HINT = `If this is a false positive, ask your agent to report it with the ${FP_REPORT_TOOL_NAME} MCP tool.`;
|
|
19046
19679
|
function formatUpdateNotice(result) {
|
|
19047
19680
|
return `\u2B06\uFE0F Update available: v${result.currentVersion} \u2192 v${result.latestVersion} (https://github.com/gendigitalinc/sage)`;
|
|
19048
19681
|
}
|
|
@@ -19108,6 +19741,10 @@ function formatThreatBannerVerbose(version, results, versionCheck, branding2) {
|
|
|
19108
19741
|
lines.push(` ... and ${overflow} more findings`);
|
|
19109
19742
|
}
|
|
19110
19743
|
}
|
|
19744
|
+
if (!first) {
|
|
19745
|
+
lines.push("");
|
|
19746
|
+
lines.push(SESSION_START_FP_HINT);
|
|
19747
|
+
}
|
|
19111
19748
|
if (versionCheck?.updateAvailable) {
|
|
19112
19749
|
lines.push("");
|
|
19113
19750
|
lines.push(formatUpdateNotice(versionCheck));
|
|
@@ -19130,6 +19767,7 @@ function formatThreatBannerCompact(version, results, versionCheck, branding2) {
|
|
|
19130
19767
|
if (remaining > 0) {
|
|
19131
19768
|
lines.push(`\u2026and ${remaining} more finding${remaining === 1 ? "" : "s"}`);
|
|
19132
19769
|
}
|
|
19770
|
+
lines.push(SESSION_START_FP_HINT);
|
|
19133
19771
|
if (versionCheck?.updateAvailable) {
|
|
19134
19772
|
lines.push(formatUpdateNotice(versionCheck));
|
|
19135
19773
|
}
|
|
@@ -19151,8 +19789,8 @@ function formatSessionStartMessage(version, result, branding2 = defaultBranding,
|
|
|
19151
19789
|
// ../core/dist/guard.js
|
|
19152
19790
|
init_config();
|
|
19153
19791
|
init_types2();
|
|
19154
|
-
async function guardToolCall(request, context, approvalStore) {
|
|
19155
|
-
const actionId = ApprovalStore.actionId(request.toolName, request.toolInput, request.sessionId);
|
|
19792
|
+
async function guardToolCall(request, context, approvalStore, actionIdOverride) {
|
|
19793
|
+
const actionId = actionIdOverride ?? ApprovalStore.actionId(request.toolName, request.toolInput, request.sessionId);
|
|
19156
19794
|
if (approvalStore.isApproved(actionId)) {
|
|
19157
19795
|
return { verdict: allowVerdict("approved") };
|
|
19158
19796
|
}
|
|
@@ -19201,7 +19839,7 @@ function formatDenyMessage(verdict, branding2 = defaultBranding) {
|
|
|
19201
19839
|
}
|
|
19202
19840
|
|
|
19203
19841
|
// ../core/dist/install-state.js
|
|
19204
|
-
var
|
|
19842
|
+
var import_node_path19 = require("node:path");
|
|
19205
19843
|
init_config();
|
|
19206
19844
|
init_file_utils();
|
|
19207
19845
|
|
|
@@ -19239,7 +19877,7 @@ init_types2();
|
|
|
19239
19877
|
var INSTALL_STATE_SCHEMA_VERSION = 1;
|
|
19240
19878
|
var SKILL_UPLOAD_NOTICE_ID = SKILL_UPLOAD_NOTICE.id;
|
|
19241
19879
|
function defaultStatePath(sageDirPath) {
|
|
19242
|
-
return (0,
|
|
19880
|
+
return (0, import_node_path19.join)(sageDirPath ?? resolvePath(SAGE_DIR), "install-state.json");
|
|
19243
19881
|
}
|
|
19244
19882
|
function emptyState() {
|
|
19245
19883
|
return { schemaVersion: INSTALL_STATE_SCHEMA_VERSION, notices: {} };
|
|
@@ -19479,8 +20117,8 @@ function createOperationalLogger(config2, runtime) {
|
|
|
19479
20117
|
try {
|
|
19480
20118
|
const result = await Promise.race([
|
|
19481
20119
|
Promise.allSettled([...pendingWrites]).then(() => "drained"),
|
|
19482
|
-
new Promise((
|
|
19483
|
-
timeout = setTimeout(() =>
|
|
20120
|
+
new Promise((resolve11) => {
|
|
20121
|
+
timeout = setTimeout(() => resolve11("timeout"), remainingMs);
|
|
19484
20122
|
timeout.unref?.();
|
|
19485
20123
|
})
|
|
19486
20124
|
]);
|
|
@@ -19528,32 +20166,32 @@ function createOperationalLogger(config2, runtime) {
|
|
|
19528
20166
|
}
|
|
19529
20167
|
|
|
19530
20168
|
// ../core/dist/plugin-scan-cache.js
|
|
19531
|
-
var
|
|
19532
|
-
var
|
|
19533
|
-
var
|
|
20169
|
+
var import_node_crypto9 = require("node:crypto");
|
|
20170
|
+
var import_promises10 = require("node:fs/promises");
|
|
20171
|
+
var import_node_path20 = require("node:path");
|
|
19534
20172
|
init_config();
|
|
19535
20173
|
init_file_utils();
|
|
19536
20174
|
init_types2();
|
|
19537
|
-
var DEFAULT_CACHE_PATH = (0,
|
|
20175
|
+
var DEFAULT_CACHE_PATH = (0, import_node_path20.join)(getHomeDir(), ".sage", "plugin_scan_cache.json");
|
|
19538
20176
|
var DEFAULT_CACHE_TTL_MS = MS_PER_DAY;
|
|
19539
20177
|
var SCHEMA_VERSION = 5;
|
|
19540
20178
|
function cacheKey(pluginKey, version, lastUpdated) {
|
|
19541
20179
|
return `${pluginKey}:${version}:${lastUpdated}`;
|
|
19542
20180
|
}
|
|
19543
20181
|
async function computeConfigHash(sageVersion, ...dirs) {
|
|
19544
|
-
const h = (0,
|
|
20182
|
+
const h = (0, import_node_crypto9.createHash)("sha256");
|
|
19545
20183
|
if (sageVersion)
|
|
19546
20184
|
h.update(sageVersion);
|
|
19547
20185
|
for (const dir of dirs) {
|
|
19548
20186
|
let files;
|
|
19549
20187
|
try {
|
|
19550
|
-
files = (await (0,
|
|
20188
|
+
files = (await (0, import_promises10.readdir)(dir)).filter((f) => f.endsWith(".yaml")).sort();
|
|
19551
20189
|
} catch {
|
|
19552
20190
|
continue;
|
|
19553
20191
|
}
|
|
19554
20192
|
for (const file of files) {
|
|
19555
20193
|
try {
|
|
19556
|
-
const content = await getFileContent((0,
|
|
20194
|
+
const content = await getFileContent((0, import_node_path20.join)(dir, file));
|
|
19557
20195
|
h.update(content);
|
|
19558
20196
|
} catch {
|
|
19559
20197
|
}
|
|
@@ -19652,22 +20290,22 @@ function storeResult(cache2, pluginKey, version, lastUpdated, findings) {
|
|
|
19652
20290
|
}
|
|
19653
20291
|
|
|
19654
20292
|
// ../core/dist/plugin-scanner.js
|
|
19655
|
-
var
|
|
19656
|
-
var
|
|
19657
|
-
var
|
|
20293
|
+
var import_node_crypto12 = require("node:crypto");
|
|
20294
|
+
var import_promises13 = require("node:fs/promises");
|
|
20295
|
+
var import_node_path24 = require("node:path");
|
|
19658
20296
|
init_config();
|
|
19659
20297
|
init_file_utils();
|
|
19660
20298
|
|
|
19661
20299
|
// ../core/dist/skill-id.js
|
|
19662
|
-
var
|
|
19663
|
-
var
|
|
19664
|
-
var
|
|
20300
|
+
var import_node_crypto10 = require("node:crypto");
|
|
20301
|
+
var import_promises11 = require("node:fs/promises");
|
|
20302
|
+
var import_node_path21 = require("node:path");
|
|
19665
20303
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "__pycache__"]);
|
|
19666
20304
|
var MAX_SKILL_BYTES = 50 * 1024 * 1024;
|
|
19667
20305
|
var SkillTooLargeError = class extends Error {
|
|
19668
20306
|
};
|
|
19669
20307
|
function isContained(childReal, rootReal) {
|
|
19670
|
-
return childReal === rootReal || childReal.startsWith(`${rootReal}${
|
|
20308
|
+
return childReal === rootReal || childReal.startsWith(`${rootReal}${import_node_path21.sep}`);
|
|
19671
20309
|
}
|
|
19672
20310
|
function normalizePath(p) {
|
|
19673
20311
|
let out = p.replace(/\\/g, "/").normalize("NFC");
|
|
@@ -19678,24 +20316,24 @@ function normalizePath(p) {
|
|
|
19678
20316
|
return out;
|
|
19679
20317
|
}
|
|
19680
20318
|
async function entriesFromDirectory(dirPath, maxBytes = Number.POSITIVE_INFINITY) {
|
|
19681
|
-
const absDir = (0,
|
|
19682
|
-
const rootReal = await (0,
|
|
20319
|
+
const absDir = (0, import_node_path21.resolve)(dirPath);
|
|
20320
|
+
const rootReal = await (0, import_promises11.realpath)(absDir);
|
|
19683
20321
|
const entries = [];
|
|
19684
20322
|
const visited = /* @__PURE__ */ new Set();
|
|
19685
20323
|
let total = 0;
|
|
19686
20324
|
async function walk(currentPath) {
|
|
19687
|
-
const real = await (0,
|
|
20325
|
+
const real = await (0, import_promises11.realpath)(currentPath);
|
|
19688
20326
|
if (visited.has(real))
|
|
19689
20327
|
return;
|
|
19690
20328
|
if (!isContained(real, rootReal))
|
|
19691
20329
|
return;
|
|
19692
20330
|
visited.add(real);
|
|
19693
|
-
const items = await (0,
|
|
20331
|
+
const items = await (0, import_promises11.readdir)(currentPath);
|
|
19694
20332
|
const dirs = [];
|
|
19695
20333
|
const files = [];
|
|
19696
20334
|
for (const item of items) {
|
|
19697
|
-
const fullPath = (0,
|
|
19698
|
-
const st = await (0,
|
|
20335
|
+
const fullPath = (0, import_node_path21.join)(currentPath, item);
|
|
20336
|
+
const st = await (0, import_promises11.stat)(fullPath);
|
|
19699
20337
|
if (st.isDirectory())
|
|
19700
20338
|
dirs.push(item);
|
|
19701
20339
|
else if (st.isFile())
|
|
@@ -19704,24 +20342,24 @@ async function entriesFromDirectory(dirPath, maxBytes = Number.POSITIVE_INFINITY
|
|
|
19704
20342
|
dirs.sort();
|
|
19705
20343
|
files.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
19706
20344
|
for (const d of dirs) {
|
|
19707
|
-
const fullPath = (0,
|
|
19708
|
-
const relPath = (0,
|
|
20345
|
+
const fullPath = (0, import_node_path21.join)(currentPath, d);
|
|
20346
|
+
const relPath = (0, import_node_path21.relative)(absDir, fullPath).replace(/\\/g, "/");
|
|
19709
20347
|
entries.push({ entryPath: relPath, isDir: true, content: Buffer.alloc(0) });
|
|
19710
20348
|
await walk(fullPath);
|
|
19711
20349
|
}
|
|
19712
20350
|
for (const f of files) {
|
|
19713
|
-
const fullPath = (0,
|
|
19714
|
-
const fileReal = await (0,
|
|
20351
|
+
const fullPath = (0, import_node_path21.join)(currentPath, f.name);
|
|
20352
|
+
const fileReal = await (0, import_promises11.realpath)(fullPath);
|
|
19715
20353
|
if (!isContained(fileReal, rootReal))
|
|
19716
20354
|
continue;
|
|
19717
20355
|
total += f.size;
|
|
19718
20356
|
if (total > maxBytes)
|
|
19719
20357
|
throw new SkillTooLargeError();
|
|
19720
|
-
const relPath = (0,
|
|
20358
|
+
const relPath = (0, import_node_path21.relative)(absDir, fullPath).replace(/\\/g, "/");
|
|
19721
20359
|
entries.push({
|
|
19722
20360
|
entryPath: relPath,
|
|
19723
20361
|
isDir: false,
|
|
19724
|
-
content: await (0,
|
|
20362
|
+
content: await (0, import_promises11.readFile)(fullPath)
|
|
19725
20363
|
});
|
|
19726
20364
|
}
|
|
19727
20365
|
}
|
|
@@ -19744,14 +20382,14 @@ function computeSkillId(entries) {
|
|
|
19744
20382
|
const fileHashes = {};
|
|
19745
20383
|
const treeEntries = [];
|
|
19746
20384
|
for (const e of normalized) {
|
|
19747
|
-
const contentHash = (0,
|
|
20385
|
+
const contentHash = (0, import_node_crypto10.createHash)("sha256").update(e.isDir ? Buffer.alloc(0) : e.content).digest("hex");
|
|
19748
20386
|
const entryType = e.isDir ? "dir" : "file";
|
|
19749
20387
|
treeEntries.push({ type: entryType, entryPath: e.entryPath, hash: contentHash });
|
|
19750
20388
|
if (!e.isDir)
|
|
19751
20389
|
fileHashes[e.entryPath] = contentHash;
|
|
19752
20390
|
}
|
|
19753
20391
|
treeEntries.sort((a, b) => a.entryPath < b.entryPath ? -1 : a.entryPath > b.entryPath ? 1 : 0);
|
|
19754
|
-
const skillHasher = (0,
|
|
20392
|
+
const skillHasher = (0, import_node_crypto10.createHash)("sha256");
|
|
19755
20393
|
for (const e of treeEntries) {
|
|
19756
20394
|
skillHasher.update(Buffer.from(`${e.type}\0${e.entryPath}\0${e.hash}
|
|
19757
20395
|
`, "utf8"));
|
|
@@ -19762,7 +20400,7 @@ async function findSkillPackagesWithMtime(rootDir) {
|
|
|
19762
20400
|
const found = [];
|
|
19763
20401
|
let rootReal;
|
|
19764
20402
|
try {
|
|
19765
|
-
rootReal = await (0,
|
|
20403
|
+
rootReal = await (0, import_promises11.realpath)((0, import_node_path21.resolve)(rootDir));
|
|
19766
20404
|
} catch {
|
|
19767
20405
|
return found;
|
|
19768
20406
|
}
|
|
@@ -19770,7 +20408,7 @@ async function findSkillPackagesWithMtime(rootDir) {
|
|
|
19770
20408
|
async function walk(currentPath) {
|
|
19771
20409
|
let real;
|
|
19772
20410
|
try {
|
|
19773
|
-
real = await (0,
|
|
20411
|
+
real = await (0, import_promises11.realpath)(currentPath);
|
|
19774
20412
|
} catch {
|
|
19775
20413
|
return 0;
|
|
19776
20414
|
}
|
|
@@ -19781,7 +20419,7 @@ async function findSkillPackagesWithMtime(rootDir) {
|
|
|
19781
20419
|
visited.add(real);
|
|
19782
20420
|
let st;
|
|
19783
20421
|
try {
|
|
19784
|
-
st = await (0,
|
|
20422
|
+
st = await (0, import_promises11.stat)(currentPath);
|
|
19785
20423
|
} catch {
|
|
19786
20424
|
return 0;
|
|
19787
20425
|
}
|
|
@@ -19790,7 +20428,7 @@ async function findSkillPackagesWithMtime(rootDir) {
|
|
|
19790
20428
|
return subtreeMax;
|
|
19791
20429
|
let items;
|
|
19792
20430
|
try {
|
|
19793
|
-
items = await (0,
|
|
20431
|
+
items = await (0, import_promises11.readdir)(currentPath);
|
|
19794
20432
|
} catch {
|
|
19795
20433
|
return subtreeMax;
|
|
19796
20434
|
}
|
|
@@ -19798,7 +20436,7 @@ async function findSkillPackagesWithMtime(rootDir) {
|
|
|
19798
20436
|
let entry;
|
|
19799
20437
|
if (items.includes("SKILL.md")) {
|
|
19800
20438
|
try {
|
|
19801
|
-
if ((await (0,
|
|
20439
|
+
if ((await (0, import_promises11.stat)((0, import_node_path21.join)(currentPath, "SKILL.md"))).isFile()) {
|
|
19802
20440
|
entry = { folder: currentPath, newestMtimeMs: 0 };
|
|
19803
20441
|
found.push(entry);
|
|
19804
20442
|
}
|
|
@@ -19808,7 +20446,7 @@ async function findSkillPackagesWithMtime(rootDir) {
|
|
|
19808
20446
|
for (const item of items) {
|
|
19809
20447
|
if (SKIP_DIRS.has(item))
|
|
19810
20448
|
continue;
|
|
19811
|
-
const childMax = await walk((0,
|
|
20449
|
+
const childMax = await walk((0, import_node_path21.join)(currentPath, item));
|
|
19812
20450
|
if (childMax > subtreeMax)
|
|
19813
20451
|
subtreeMax = childMax;
|
|
19814
20452
|
}
|
|
@@ -19816,16 +20454,16 @@ async function findSkillPackagesWithMtime(rootDir) {
|
|
|
19816
20454
|
entry.newestMtimeMs = subtreeMax;
|
|
19817
20455
|
return subtreeMax;
|
|
19818
20456
|
}
|
|
19819
|
-
await walk((0,
|
|
20457
|
+
await walk((0, import_node_path21.resolve)(rootDir));
|
|
19820
20458
|
return found;
|
|
19821
20459
|
}
|
|
19822
20460
|
function looseSkillKey(skillsRoot, folder, tag, scope) {
|
|
19823
|
-
const rel = (0,
|
|
20461
|
+
const rel = (0, import_node_path21.relative)((0, import_node_path21.resolve)(skillsRoot), (0, import_node_path21.resolve)(folder)).split(import_node_path21.sep).join("/") || (0, import_node_path21.basename)(folder);
|
|
19824
20462
|
return `skill:${tag}/${rel}@${scope}`;
|
|
19825
20463
|
}
|
|
19826
20464
|
async function discoverLooseSkills(skillsDir, tag, scope) {
|
|
19827
|
-
const resolvedRoot = (0,
|
|
19828
|
-
const packages = (await findSkillPackagesWithMtime(skillsDir)).filter((p) => (0,
|
|
20465
|
+
const resolvedRoot = (0, import_node_path21.resolve)(skillsDir);
|
|
20466
|
+
const packages = (await findSkillPackagesWithMtime(skillsDir)).filter((p) => (0, import_node_path21.resolve)(p.folder) !== resolvedRoot);
|
|
19829
20467
|
return packages.map(({ folder, newestMtimeMs }) => ({
|
|
19830
20468
|
key: looseSkillKey(resolvedRoot, folder, tag, scope),
|
|
19831
20469
|
installPath: folder,
|
|
@@ -19839,7 +20477,7 @@ async function dedupeSkillsByResolvedPath(skills) {
|
|
|
19839
20477
|
for (const skill of skills) {
|
|
19840
20478
|
let canonical = skill.installPath;
|
|
19841
20479
|
try {
|
|
19842
|
-
canonical = await (0,
|
|
20480
|
+
canonical = await (0, import_promises11.realpath)(skill.installPath);
|
|
19843
20481
|
} catch {
|
|
19844
20482
|
}
|
|
19845
20483
|
if (seen.has(canonical))
|
|
@@ -19881,15 +20519,15 @@ async function computeSkillIdsForRoot(rootDir, logger) {
|
|
|
19881
20519
|
}
|
|
19882
20520
|
|
|
19883
20521
|
// ../core/dist/skill-pending.js
|
|
19884
|
-
var
|
|
19885
|
-
var
|
|
20522
|
+
var import_promises12 = require("node:fs/promises");
|
|
20523
|
+
var import_node_path22 = require("node:path");
|
|
19886
20524
|
init_config();
|
|
19887
20525
|
init_file_utils();
|
|
19888
20526
|
init_types2();
|
|
19889
20527
|
var SCHEMA_VERSION2 = 1;
|
|
19890
20528
|
var PENDING_TTL_MS = 60 * 60 * 1e3;
|
|
19891
20529
|
function defaultPendingPath() {
|
|
19892
|
-
return (0,
|
|
20530
|
+
return (0, import_node_path22.join)(resolvePath(SAGE_DIR), "skill_pending.json");
|
|
19893
20531
|
}
|
|
19894
20532
|
async function loadPendingMarker(pendingPath = defaultPendingPath(), logger = nullLogger) {
|
|
19895
20533
|
let raw;
|
|
@@ -19927,7 +20565,7 @@ async function loadPendingMarker(pendingPath = defaultPendingPath(), logger = nu
|
|
|
19927
20565
|
async function savePendingMarker(marker, pendingPath = defaultPendingPath(), logger = nullLogger) {
|
|
19928
20566
|
if (Object.keys(marker.entries).length === 0) {
|
|
19929
20567
|
try {
|
|
19930
|
-
await (0,
|
|
20568
|
+
await (0, import_promises12.unlink)(pendingPath);
|
|
19931
20569
|
} catch (e) {
|
|
19932
20570
|
if (!e || typeof e !== "object" || e.code !== "ENOENT") {
|
|
19933
20571
|
logger.warn(`Failed to delete empty pending marker at ${pendingPath}`, {
|
|
@@ -19979,7 +20617,7 @@ function listPending(marker) {
|
|
|
19979
20617
|
}
|
|
19980
20618
|
|
|
19981
20619
|
// ../core/dist/skill-verdict-cache.js
|
|
19982
|
-
var
|
|
20620
|
+
var import_node_path23 = require("node:path");
|
|
19983
20621
|
init_config();
|
|
19984
20622
|
init_file_utils();
|
|
19985
20623
|
init_types2();
|
|
@@ -20015,9 +20653,9 @@ function mergeSources(existing, incoming) {
|
|
|
20015
20653
|
return merged.length > 0 ? merged : void 0;
|
|
20016
20654
|
}
|
|
20017
20655
|
function defaultCachePath2() {
|
|
20018
|
-
return (0,
|
|
20656
|
+
return (0, import_node_path23.join)(resolvePath(SAGE_DIR), "skill_verdict_cache.json");
|
|
20019
20657
|
}
|
|
20020
|
-
async function
|
|
20658
|
+
async function loadSkillVerdictCacheInternal(cachePath, ttlMs, logger, pruneOnLoad) {
|
|
20021
20659
|
let raw;
|
|
20022
20660
|
try {
|
|
20023
20661
|
raw = await getFileContent(cachePath);
|
|
@@ -20040,6 +20678,8 @@ async function loadSkillVerdictCache(cachePath = defaultCachePath2(), ttlMs = DE
|
|
|
20040
20678
|
continue;
|
|
20041
20679
|
}
|
|
20042
20680
|
entries[skillId] = {
|
|
20681
|
+
entryId: typeof entry.entry_id === "string" ? entry.entry_id : void 0,
|
|
20682
|
+
jobId: typeof entry.job_id === "string" ? entry.job_id : void 0,
|
|
20043
20683
|
verdict: typeof entry.verdict === "string" ? entry.verdict : void 0,
|
|
20044
20684
|
summary: typeof entry.summary === "string" ? entry.summary : void 0,
|
|
20045
20685
|
skillName: typeof entry.skill_name === "string" ? entry.skill_name : void 0,
|
|
@@ -20048,7 +20688,7 @@ async function loadSkillVerdictCache(cachePath = defaultCachePath2(), ttlMs = DE
|
|
|
20048
20688
|
};
|
|
20049
20689
|
}
|
|
20050
20690
|
const cache2 = { entries };
|
|
20051
|
-
if (pruned > 0) {
|
|
20691
|
+
if (pruneOnLoad && pruned > 0) {
|
|
20052
20692
|
logger.debug(`Pruned ${pruned} stale verdict(s) from cache`, { cachePath });
|
|
20053
20693
|
await saveSkillVerdictCache(cache2, cachePath, logger);
|
|
20054
20694
|
}
|
|
@@ -20058,6 +20698,9 @@ async function loadSkillVerdictCache(cachePath = defaultCachePath2(), ttlMs = DE
|
|
|
20058
20698
|
return { entries: {} };
|
|
20059
20699
|
}
|
|
20060
20700
|
}
|
|
20701
|
+
async function loadSkillVerdictCache(cachePath = defaultCachePath2(), ttlMs = DEFAULT_VERDICT_TTL_MS, logger = nullLogger) {
|
|
20702
|
+
return loadSkillVerdictCacheInternal(cachePath, ttlMs, logger, true);
|
|
20703
|
+
}
|
|
20061
20704
|
async function saveSkillVerdictCache(cache2, cachePath = defaultCachePath2(), logger = nullLogger) {
|
|
20062
20705
|
try {
|
|
20063
20706
|
const data = {
|
|
@@ -20065,6 +20708,8 @@ async function saveSkillVerdictCache(cache2, cachePath = defaultCachePath2(), lo
|
|
|
20065
20708
|
entries: Object.fromEntries(Object.entries(cache2.entries).map(([skillId, v]) => [
|
|
20066
20709
|
skillId,
|
|
20067
20710
|
{
|
|
20711
|
+
entry_id: v.entryId,
|
|
20712
|
+
job_id: v.jobId,
|
|
20068
20713
|
verdict: v.verdict,
|
|
20069
20714
|
summary: v.summary,
|
|
20070
20715
|
skill_name: v.skillName,
|
|
@@ -20077,8 +20722,10 @@ async function saveSkillVerdictCache(cache2, cachePath = defaultCachePath2(), lo
|
|
|
20077
20722
|
]))
|
|
20078
20723
|
};
|
|
20079
20724
|
await atomicWriteJson(cachePath, data);
|
|
20725
|
+
return true;
|
|
20080
20726
|
} catch (e) {
|
|
20081
20727
|
logger.warn(`Failed to save verdict cache to ${cachePath}`, { error: String(e) });
|
|
20728
|
+
return false;
|
|
20082
20729
|
}
|
|
20083
20730
|
}
|
|
20084
20731
|
function getVerdict(cache2, skillId, ttlMs = DEFAULT_VERDICT_TTL_MS) {
|
|
@@ -20099,6 +20746,34 @@ function putVerdict(cache2, skillId, verdict) {
|
|
|
20099
20746
|
};
|
|
20100
20747
|
}
|
|
20101
20748
|
|
|
20749
|
+
// ../core/dist/skill-verdict-record.js
|
|
20750
|
+
var import_node_crypto11 = require("node:crypto");
|
|
20751
|
+
function recordAnalyzedSkillVerdict(cache2, skillId, result, origin) {
|
|
20752
|
+
const entryId = (0, import_node_crypto11.randomUUID)();
|
|
20753
|
+
putVerdict(cache2, skillId, {
|
|
20754
|
+
entryId,
|
|
20755
|
+
jobId: result.jobId,
|
|
20756
|
+
verdict: result.verdict,
|
|
20757
|
+
summary: result.summary,
|
|
20758
|
+
skillName: origin.skillName,
|
|
20759
|
+
sources: [{ agentRuntime: origin.agentRuntime, containerKey: origin.containerKey }]
|
|
20760
|
+
});
|
|
20761
|
+
const { loggingConfig } = origin;
|
|
20762
|
+
return {
|
|
20763
|
+
entryId,
|
|
20764
|
+
writeAuditEntry: async () => {
|
|
20765
|
+
if (!loggingConfig)
|
|
20766
|
+
return;
|
|
20767
|
+
await logSkillVerdict(loggingConfig, skillId, "analyzed", result.verdict, result.summary, {
|
|
20768
|
+
entryId,
|
|
20769
|
+
jobId: result.jobId,
|
|
20770
|
+
agentRuntime: origin.agentRuntime
|
|
20771
|
+
});
|
|
20772
|
+
}
|
|
20773
|
+
};
|
|
20774
|
+
}
|
|
20775
|
+
var SKILL_ENTRY_ID_NAMESPACE = Buffer.from("75bba79a376142a7bacbf95d791126d3", "hex");
|
|
20776
|
+
|
|
20102
20777
|
// ../core/dist/plugin-scanner.js
|
|
20103
20778
|
init_types2();
|
|
20104
20779
|
var SCANNABLE_EXTENSIONS2 = /* @__PURE__ */ new Set([
|
|
@@ -20126,7 +20801,7 @@ async function walkPluginFiles(installPath, logger) {
|
|
|
20126
20801
|
const visited = /* @__PURE__ */ new Set();
|
|
20127
20802
|
let rootReal;
|
|
20128
20803
|
try {
|
|
20129
|
-
rootReal = await (0,
|
|
20804
|
+
rootReal = await (0, import_promises13.realpath)((0, import_node_path24.resolve)(installPath));
|
|
20130
20805
|
} catch (e) {
|
|
20131
20806
|
logger.warn(`Error walking plugin path ${installPath}`, { error: String(e) });
|
|
20132
20807
|
return files;
|
|
@@ -20134,7 +20809,7 @@ async function walkPluginFiles(installPath, logger) {
|
|
|
20134
20809
|
async function walk(dirOrFile) {
|
|
20135
20810
|
let real;
|
|
20136
20811
|
try {
|
|
20137
|
-
real = await (0,
|
|
20812
|
+
real = await (0, import_promises13.realpath)(dirOrFile);
|
|
20138
20813
|
} catch {
|
|
20139
20814
|
return;
|
|
20140
20815
|
}
|
|
@@ -20145,12 +20820,12 @@ async function walkPluginFiles(installPath, logger) {
|
|
|
20145
20820
|
visited.add(real);
|
|
20146
20821
|
let stats;
|
|
20147
20822
|
try {
|
|
20148
|
-
stats = await (0,
|
|
20823
|
+
stats = await (0, import_promises13.stat)(dirOrFile);
|
|
20149
20824
|
} catch {
|
|
20150
20825
|
return;
|
|
20151
20826
|
}
|
|
20152
20827
|
if (stats.isFile()) {
|
|
20153
|
-
if (SCANNABLE_EXTENSIONS2.has((0,
|
|
20828
|
+
if (SCANNABLE_EXTENSIONS2.has((0, import_node_path24.extname)(dirOrFile).toLowerCase()) && stats.size <= MAX_FILE_SIZE) {
|
|
20154
20829
|
files.push(dirOrFile);
|
|
20155
20830
|
}
|
|
20156
20831
|
return;
|
|
@@ -20158,20 +20833,20 @@ async function walkPluginFiles(installPath, logger) {
|
|
|
20158
20833
|
if (stats.isDirectory()) {
|
|
20159
20834
|
let entries;
|
|
20160
20835
|
try {
|
|
20161
|
-
entries = await (0,
|
|
20836
|
+
entries = await (0, import_promises13.readdir)(dirOrFile);
|
|
20162
20837
|
} catch {
|
|
20163
20838
|
return;
|
|
20164
20839
|
}
|
|
20165
20840
|
for (const entry of entries) {
|
|
20166
20841
|
if (SKIP_DIRS.has(entry))
|
|
20167
20842
|
continue;
|
|
20168
|
-
const fullPath = (0,
|
|
20843
|
+
const fullPath = (0, import_node_path24.join)(dirOrFile, entry);
|
|
20169
20844
|
await walk(fullPath);
|
|
20170
20845
|
}
|
|
20171
20846
|
}
|
|
20172
20847
|
}
|
|
20173
20848
|
try {
|
|
20174
|
-
await walk((0,
|
|
20849
|
+
await walk((0, import_node_path24.resolve)(installPath));
|
|
20175
20850
|
} catch (e) {
|
|
20176
20851
|
logger.warn(`Error walking plugin path ${installPath}`, { error: String(e) });
|
|
20177
20852
|
}
|
|
@@ -20208,7 +20883,7 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
20208
20883
|
}
|
|
20209
20884
|
if (amsiClient) {
|
|
20210
20885
|
try {
|
|
20211
|
-
const scanName = `${plugin.key}/${(0,
|
|
20886
|
+
const scanName = `${plugin.key}/${(0, import_node_path24.relative)(plugin.installPath, filePath)}`;
|
|
20212
20887
|
const amsiResult = await amsiClient.scanString("Plugin", scanName, content);
|
|
20213
20888
|
if (amsiResult && (amsiResult.isDetected || amsiResult.isBlockedByAdmin)) {
|
|
20214
20889
|
result.findings.push({
|
|
@@ -20216,7 +20891,7 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
20216
20891
|
title: `AMSI detection (result=${amsiResult.amsiResult})`,
|
|
20217
20892
|
severity: "critical",
|
|
20218
20893
|
artifact: content.slice(0, 200),
|
|
20219
|
-
sourceFile: (0,
|
|
20894
|
+
sourceFile: (0, import_node_path24.relative)(plugin.installPath, filePath)
|
|
20220
20895
|
});
|
|
20221
20896
|
}
|
|
20222
20897
|
} catch {
|
|
@@ -20226,7 +20901,7 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
20226
20901
|
allUrls.push(...extractUrls(content));
|
|
20227
20902
|
}
|
|
20228
20903
|
if (checkFileHashes) {
|
|
20229
|
-
const sha256 = (0,
|
|
20904
|
+
const sha256 = (0, import_node_crypto12.createHash)("sha256").update(rawBytes).digest("hex");
|
|
20230
20905
|
const existing = hashToFiles.get(sha256);
|
|
20231
20906
|
if (existing) {
|
|
20232
20907
|
existing.push(filePath);
|
|
@@ -20269,7 +20944,7 @@ async function scanPlugin(plugin, options = {}) {
|
|
|
20269
20944
|
title: `Malicious file (${fr.detectionNames.join(", ") || "unknown"})`,
|
|
20270
20945
|
severity: "critical",
|
|
20271
20946
|
artifact: fr.sha256,
|
|
20272
|
-
sourceFile: (0,
|
|
20947
|
+
sourceFile: (0, import_node_path24.relative)(plugin.installPath, filePath)
|
|
20273
20948
|
});
|
|
20274
20949
|
}
|
|
20275
20950
|
}
|
|
@@ -20296,7 +20971,7 @@ function pushSkillFindingIfRisky(findings, plugin, skillId, folder, verdict) {
|
|
|
20296
20971
|
title: verdict.summary?.trim() || (risk === "CRITICAL" ? "Malicious skill detected" : "Suspicious skill detected"),
|
|
20297
20972
|
severity,
|
|
20298
20973
|
artifact: skillId.slice(0, 16),
|
|
20299
|
-
sourceFile: (0,
|
|
20974
|
+
sourceFile: (0, import_node_path24.relative)(plugin.installPath, folder) || "."
|
|
20300
20975
|
});
|
|
20301
20976
|
}
|
|
20302
20977
|
async function runSkillCheck(plugin, findings, logger, paths = {}) {
|
|
@@ -20323,6 +20998,8 @@ async function runSkillCheck(plugin, findings, logger, paths = {}) {
|
|
|
20323
20998
|
let markerModified = false;
|
|
20324
20999
|
let lookupFailed = false;
|
|
20325
21000
|
let graceDeferred = false;
|
|
21001
|
+
const resolvedPendingSkillIds = /* @__PURE__ */ new Set();
|
|
21002
|
+
const recordedVerdicts = /* @__PURE__ */ new Map();
|
|
20326
21003
|
for (const { folder, skillId } of uncached) {
|
|
20327
21004
|
if (!verdicts.has(skillId)) {
|
|
20328
21005
|
lookupFailed = true;
|
|
@@ -20330,17 +21007,18 @@ async function runSkillCheck(plugin, findings, logger, paths = {}) {
|
|
|
20330
21007
|
}
|
|
20331
21008
|
const verdict = verdicts.get(skillId) ?? null;
|
|
20332
21009
|
if (verdict) {
|
|
20333
|
-
|
|
20334
|
-
|
|
20335
|
-
|
|
20336
|
-
|
|
20337
|
-
|
|
20338
|
-
|
|
20339
|
-
|
|
21010
|
+
if (!recordedVerdicts.has(skillId)) {
|
|
21011
|
+
recordedVerdicts.set(skillId, recordAnalyzedSkillVerdict(verdictCache, skillId, verdict, {
|
|
21012
|
+
skillName: (0, import_node_path24.basename)(folder),
|
|
21013
|
+
agentRuntime: paths.agentRuntime,
|
|
21014
|
+
containerKey: plugin.key,
|
|
21015
|
+
loggingConfig: paths.loggingConfig
|
|
21016
|
+
}));
|
|
21017
|
+
cacheModified = true;
|
|
21018
|
+
}
|
|
20340
21019
|
pushSkillFindingIfRisky(findings, plugin, skillId, folder, verdict);
|
|
20341
21020
|
if (isPending(marker, skillId)) {
|
|
20342
|
-
|
|
20343
|
-
markerModified = true;
|
|
21021
|
+
resolvedPendingSkillIds.add(skillId);
|
|
20344
21022
|
}
|
|
20345
21023
|
} else if (!isPending(marker, skillId)) {
|
|
20346
21024
|
if (paths.uploadEnabled === false) {
|
|
@@ -20359,8 +21037,17 @@ async function runSkillCheck(plugin, findings, logger, paths = {}) {
|
|
|
20359
21037
|
}
|
|
20360
21038
|
}
|
|
20361
21039
|
}
|
|
20362
|
-
|
|
20363
|
-
|
|
21040
|
+
const cacheSaved = cacheModified ? await saveSkillVerdictCache(verdictCache, paths.verdictCachePath, logger) : true;
|
|
21041
|
+
if (cacheSaved) {
|
|
21042
|
+
await Promise.all([...recordedVerdicts.values()].map((entry) => entry.writeAuditEntry().catch(() => {
|
|
21043
|
+
})));
|
|
21044
|
+
for (const skillId of resolvedPendingSkillIds) {
|
|
21045
|
+
removePending(marker, skillId);
|
|
21046
|
+
markerModified = true;
|
|
21047
|
+
}
|
|
21048
|
+
} else {
|
|
21049
|
+
lookupFailed = true;
|
|
21050
|
+
}
|
|
20364
21051
|
if (markerModified)
|
|
20365
21052
|
await savePendingMarker(marker, paths.pendingPath, logger);
|
|
20366
21053
|
const anyStillPending = uncached.some(({ skillId }) => isPending(marker, skillId));
|
|
@@ -20374,12 +21061,12 @@ async function runSkillCheck(plugin, findings, logger, paths = {}) {
|
|
|
20374
21061
|
// ../core/dist/session-start.js
|
|
20375
21062
|
var import_node_child_process2 = require("node:child_process");
|
|
20376
21063
|
var import_node_fs4 = require("node:fs");
|
|
20377
|
-
var
|
|
21064
|
+
var import_node_path26 = require("node:path");
|
|
20378
21065
|
init_config();
|
|
20379
21066
|
init_file_utils();
|
|
20380
21067
|
|
|
20381
21068
|
// ../core/dist/session-start-scan.js
|
|
20382
|
-
var
|
|
21069
|
+
var import_node_path25 = require("node:path");
|
|
20383
21070
|
init_config();
|
|
20384
21071
|
init_types2();
|
|
20385
21072
|
function fromCachedFinding(finding) {
|
|
@@ -20462,8 +21149,8 @@ async function scanAllPlugins(context, config2, plugins, exceptions, amsiClient,
|
|
|
20462
21149
|
deniedByException: 0,
|
|
20463
21150
|
findingsCount: 0
|
|
20464
21151
|
};
|
|
20465
|
-
const skillPendingPath = context.sageDirPath ? (0,
|
|
20466
|
-
const skillVerdictCachePath = context.sageDirPath ? (0,
|
|
21152
|
+
const skillPendingPath = context.sageDirPath ? (0, import_node_path25.join)(context.sageDirPath, "skill_pending.json") : void 0;
|
|
21153
|
+
const skillVerdictCachePath = context.sageDirPath ? (0, import_node_path25.join)(context.sageDirPath, "skill_verdict_cache.json") : void 0;
|
|
20467
21154
|
const cacheTtlMs = skillCacheTtlMs(config2);
|
|
20468
21155
|
for (const plugin of plugins) {
|
|
20469
21156
|
const denyMatch = findPluginDenyException(exceptions, plugin.key);
|
|
@@ -20629,6 +21316,16 @@ async function checkForUpdate(currentVersion, logger = nullLogger, timeoutMs = D
|
|
|
20629
21316
|
async function runSessionStart(ctx) {
|
|
20630
21317
|
const logger = ctx.logger ?? nullLogger;
|
|
20631
21318
|
const sageDirPath = resolvePath(ctx.sageDirPath ?? "~/.sage");
|
|
21319
|
+
try {
|
|
21320
|
+
await loadThreats(ctx.threatsDir, logger, {
|
|
21321
|
+
customThreatsDir: getUserThreatsDir(sageDirPath),
|
|
21322
|
+
logCustomRuleIds: true
|
|
21323
|
+
});
|
|
21324
|
+
} catch (error) {
|
|
21325
|
+
logger.warn("Threat rules failed to load at session start (fail-open)", {
|
|
21326
|
+
error: String(error)
|
|
21327
|
+
});
|
|
21328
|
+
}
|
|
20632
21329
|
pruneOrphanedTmpFiles(sageDirPath).catch(() => {
|
|
20633
21330
|
});
|
|
20634
21331
|
deployConfigDefaults(sageDirPath, logger).catch(() => {
|
|
@@ -20811,7 +21508,7 @@ async function maybeSpawnSkillUploadWorker(args) {
|
|
|
20811
21508
|
});
|
|
20812
21509
|
return;
|
|
20813
21510
|
}
|
|
20814
|
-
const marker = await loadPendingMarker((0,
|
|
21511
|
+
const marker = await loadPendingMarker((0, import_node_path26.join)(args.sageDirPath, "skill_pending.json"), args.logger);
|
|
20815
21512
|
const pendingCount = listPending(marker).length;
|
|
20816
21513
|
if (pendingCount === 0) {
|
|
20817
21514
|
args.logger.debug("Skill upload worker skipped", {
|
|
@@ -20950,19 +21647,19 @@ function canonicalizeToolName(map, rawName) {
|
|
|
20950
21647
|
init_types2();
|
|
20951
21648
|
|
|
20952
21649
|
// src/bundled-dirs.ts
|
|
20953
|
-
var
|
|
20954
|
-
var packageRoot = (0,
|
|
21650
|
+
var import_node_path27 = require("node:path");
|
|
21651
|
+
var packageRoot = (0, import_node_path27.resolve)(__dirname, "..");
|
|
20955
21652
|
function getBundledDataDirs() {
|
|
20956
21653
|
return {
|
|
20957
|
-
threatsDir: (0,
|
|
20958
|
-
trustedDomainsDir: (0,
|
|
21654
|
+
threatsDir: (0, import_node_path27.join)(packageRoot, "resources", "threats"),
|
|
21655
|
+
trustedDomainsDir: (0, import_node_path27.join)(packageRoot, "resources", "trusted-domains")
|
|
20959
21656
|
};
|
|
20960
21657
|
}
|
|
20961
21658
|
var cachedVersion;
|
|
20962
21659
|
function getSageVersion() {
|
|
20963
21660
|
if (cachedVersion) return cachedVersion;
|
|
20964
21661
|
try {
|
|
20965
|
-
const pkg = JSON.parse(getFileContentSync((0,
|
|
21662
|
+
const pkg = JSON.parse(getFileContentSync((0, import_node_path27.join)(packageRoot, "package.json"), "utf-8"));
|
|
20966
21663
|
cachedVersion = pkg.version ?? "0.0.0";
|
|
20967
21664
|
} catch {
|
|
20968
21665
|
cachedVersion = "0.0.0";
|
|
@@ -20971,16 +21668,16 @@ function getSageVersion() {
|
|
|
20971
21668
|
}
|
|
20972
21669
|
|
|
20973
21670
|
// src/startup-scan.ts
|
|
20974
|
-
var
|
|
21671
|
+
var import_node_path29 = require("node:path");
|
|
20975
21672
|
|
|
20976
21673
|
// src/plugin-discovery.ts
|
|
20977
|
-
var
|
|
21674
|
+
var import_promises14 = require("node:fs/promises");
|
|
20978
21675
|
var import_node_os4 = require("node:os");
|
|
20979
|
-
var
|
|
20980
|
-
var DEFAULT_EXTENSIONS_DIR = (0,
|
|
21676
|
+
var import_node_path28 = require("node:path");
|
|
21677
|
+
var DEFAULT_EXTENSIONS_DIR = (0, import_node_path28.join)((0, import_node_os4.homedir)(), ".openclaw", "extensions");
|
|
20981
21678
|
var SKILL_ROOTS = [
|
|
20982
|
-
{ dir: (0,
|
|
20983
|
-
{ dir: (0,
|
|
21679
|
+
{ dir: (0, import_node_path28.join)(getHomeDir(), ".agents", "skills"), tag: "agents", scope: "personal" },
|
|
21680
|
+
{ dir: (0, import_node_path28.join)(getHomeDir(), ".openclaw", "skills"), tag: "openclaw", scope: "personal" }
|
|
20984
21681
|
];
|
|
20985
21682
|
async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSIONS_DIR, branding2 = defaultBranding, skillRoots = SKILL_ROOTS) {
|
|
20986
21683
|
logger.debug(`${branding2.name} plugin discovery: scanning extensions directory`, {
|
|
@@ -20989,23 +21686,23 @@ async function discoverOpenClawPlugins(logger, extensionsDir = DEFAULT_EXTENSION
|
|
|
20989
21686
|
const plugins = [];
|
|
20990
21687
|
let entries;
|
|
20991
21688
|
try {
|
|
20992
|
-
entries = await (0,
|
|
21689
|
+
entries = await (0, import_promises14.readdir)(extensionsDir);
|
|
20993
21690
|
} catch {
|
|
20994
21691
|
logger.debug("OpenClaw extensions directory not found", { path: extensionsDir });
|
|
20995
21692
|
entries = [];
|
|
20996
21693
|
}
|
|
20997
21694
|
for (const entry of entries) {
|
|
20998
|
-
const extDir = (0,
|
|
21695
|
+
const extDir = (0, import_node_path28.join)(extensionsDir, entry);
|
|
20999
21696
|
let stats;
|
|
21000
21697
|
try {
|
|
21001
|
-
stats = await (0,
|
|
21698
|
+
stats = await (0, import_promises14.stat)(extDir);
|
|
21002
21699
|
if (!stats.isDirectory()) continue;
|
|
21003
21700
|
} catch {
|
|
21004
21701
|
continue;
|
|
21005
21702
|
}
|
|
21006
21703
|
let pkg;
|
|
21007
21704
|
try {
|
|
21008
|
-
const pkgPath = (0,
|
|
21705
|
+
const pkgPath = (0, import_node_path28.join)(extDir, "package.json");
|
|
21009
21706
|
const raw = await getFileContent(pkgPath);
|
|
21010
21707
|
pkg = JSON.parse(raw);
|
|
21011
21708
|
} catch {
|
|
@@ -21052,8 +21749,8 @@ function createOpenClawScanHandler(logger, context, branding2 = defaultBranding,
|
|
|
21052
21749
|
// No onNotices: OpenClaw delivers the one-time notice from
|
|
21053
21750
|
// `before_prompt_build` (a different process/turn than this scan) via
|
|
21054
21751
|
// takePendingNotices, so the scan-time callback would never fire.
|
|
21055
|
-
modelDownloadWorkerPath: (0,
|
|
21056
|
-
skillUploadWorkerPath: (0,
|
|
21752
|
+
modelDownloadWorkerPath: (0, import_node_path29.resolve)(__dirname, "model-download-worker.cjs"),
|
|
21753
|
+
skillUploadWorkerPath: (0, import_node_path29.resolve)(__dirname, "skill-upload-worker.cjs"),
|
|
21057
21754
|
// Don't commit the "shown" flag here — commit happens at delivery.
|
|
21058
21755
|
commitUploadNotice: false,
|
|
21059
21756
|
announceCleanScans
|