agentseal 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agentseal.js +3096 -6858
- package/dist/cache-MVU2E2LB.js +8 -0
- package/dist/canaries-K2JQLX7Z.js +314 -0
- package/dist/chunk-4EOVMNW5.js +100 -0
- package/dist/chunk-BXOPZ7UC.js +223 -0
- package/dist/chunk-EGCYEYIX.js +580 -0
- package/dist/chunk-I6HSMNTE.js +1906 -0
- package/dist/chunk-IGSX7F4B.js +69 -0
- package/dist/{chunk-23GC7G5P.js → chunk-IO5DO7DS.js} +1 -2
- package/dist/chunk-PG5LEDUE.js +530 -0
- package/dist/chunk-RJ56XHCI.js +115 -0
- package/dist/chunk-XQGUICLL.js +45 -0
- package/dist/chunk-ZNNQ2HKJ.js +267 -0
- package/dist/collectors-Y5Z2R2UT.js +39 -0
- package/dist/deep-reasoning-GHCZ3SO6.js +17 -0
- package/dist/fix-NOFNO7VW.js +204 -0
- package/dist/http-AIVCASYV.js +8 -0
- package/dist/index.cjs +6726 -3446
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +165 -38
- package/dist/index.d.ts +165 -38
- package/dist/index.js +5967 -2703
- package/dist/index.js.map +1 -1
- package/dist/llm-client-4F2EACT5.js +156 -0
- package/dist/profiles-UHSPR44T.js +108 -0
- package/dist/project-3P2OW3W6.js +10 -0
- package/dist/scan-mcp-YOM2YJJG.js +380 -0
- package/dist/shield-HCIU3CSU.js +1962 -0
- package/dist/skill-llm-R3L7TL42.js +225 -0
- package/package.json +2 -2
- package/dist/llm-judge-T6LDAZRQ.js +0 -241
- package/dist/machine-discovery-XIJE7CFD.js +0 -22
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/guard-models.ts
|
|
4
|
+
var GuardVerdict = {
|
|
5
|
+
SAFE: "safe",
|
|
6
|
+
WARNING: "warning",
|
|
7
|
+
DANGER: "danger",
|
|
8
|
+
ERROR: "error"
|
|
9
|
+
};
|
|
10
|
+
var SEVERITY_ORDER = {
|
|
11
|
+
critical: 0,
|
|
12
|
+
high: 1,
|
|
13
|
+
medium: 2,
|
|
14
|
+
low: 3
|
|
15
|
+
};
|
|
16
|
+
function customFindingFromDict(d) {
|
|
17
|
+
return {
|
|
18
|
+
code: d.code ?? "",
|
|
19
|
+
title: d.title ?? "",
|
|
20
|
+
severity: d.severity ?? "medium",
|
|
21
|
+
verdict: d.verdict ?? "warning",
|
|
22
|
+
remediation: d.remediation ?? "",
|
|
23
|
+
rule_file: d.rule_file ?? "",
|
|
24
|
+
entity_type: d.entity_type ?? "",
|
|
25
|
+
entity_name: d.entity_name ?? ""
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function countVerdict(skills, mcp, runtime, verdict) {
|
|
29
|
+
return skills.filter((s) => s.verdict === verdict).length + mcp.filter((m) => m.verdict === verdict).length + runtime.filter((r) => r.verdict === verdict).length;
|
|
30
|
+
}
|
|
31
|
+
function totalDangers(report) {
|
|
32
|
+
return countVerdict(report.skill_results, report.mcp_results, report.mcp_runtime_results, GuardVerdict.DANGER);
|
|
33
|
+
}
|
|
34
|
+
function totalWarnings(report) {
|
|
35
|
+
return countVerdict(report.skill_results, report.mcp_results, report.mcp_runtime_results, GuardVerdict.WARNING);
|
|
36
|
+
}
|
|
37
|
+
function totalSafe(report) {
|
|
38
|
+
return countVerdict(report.skill_results, report.mcp_results, report.mcp_runtime_results, GuardVerdict.SAFE);
|
|
39
|
+
}
|
|
40
|
+
function guardReportFromDict(d) {
|
|
41
|
+
return {
|
|
42
|
+
timestamp: d.timestamp ?? "",
|
|
43
|
+
duration_seconds: d.duration_seconds ?? 0,
|
|
44
|
+
agents_found: d.agents_found ?? [],
|
|
45
|
+
skill_results: d.skill_results ?? [],
|
|
46
|
+
mcp_results: (d.mcp_results ?? []).map((m) => ({
|
|
47
|
+
...m,
|
|
48
|
+
registry_score: m.registry?.score ?? m.registry_score,
|
|
49
|
+
registry_level: m.registry?.level ?? m.registry_level,
|
|
50
|
+
registry_findings_count: m.registry?.findings_count ?? m.registry_findings_count
|
|
51
|
+
})),
|
|
52
|
+
mcp_runtime_results: d.mcp_runtime_results ?? [],
|
|
53
|
+
toxic_flows: d.toxic_flows ?? [],
|
|
54
|
+
baseline_changes: d.baseline_changes ?? [],
|
|
55
|
+
llm_tokens_used: d.llm_tokens_used ?? 0,
|
|
56
|
+
unlisted_findings: d.unlisted_findings ?? [],
|
|
57
|
+
custom_findings: (d.custom_findings ?? []).map(customFindingFromDict),
|
|
58
|
+
config_path: d.config_path ?? ""
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
GuardVerdict,
|
|
64
|
+
SEVERITY_ORDER,
|
|
65
|
+
totalDangers,
|
|
66
|
+
totalWarnings,
|
|
67
|
+
totalSafe,
|
|
68
|
+
guardReportFromDict
|
|
69
|
+
};
|
|
@@ -564,6 +564,7 @@ function _scanProjectDir(dir, mcpServers, skillPaths, seenSkillPaths) {
|
|
|
564
564
|
var MAX_SKILL_SIZE, PROJECT_MCP_CONFIGS, PROJECT_SKILL_FILES, PROJECT_SKILL_DIRS, SKILL_DIRS, SKILL_FILES;
|
|
565
565
|
var init_machine_discovery = __esm({
|
|
566
566
|
"src/machine-discovery.ts"() {
|
|
567
|
+
"use strict";
|
|
567
568
|
MAX_SKILL_SIZE = 10 * 1024 * 1024;
|
|
568
569
|
PROJECT_MCP_CONFIGS = [
|
|
569
570
|
[".mcp.json", "mcpServers", null],
|
|
@@ -628,8 +629,6 @@ export {
|
|
|
628
629
|
PROJECT_SKILL_DIRS,
|
|
629
630
|
getWellKnownConfigs,
|
|
630
631
|
stripJsonComments,
|
|
631
|
-
scanMachine,
|
|
632
|
-
scanDirectory,
|
|
633
632
|
machine_discovery_exports,
|
|
634
633
|
init_machine_discovery
|
|
635
634
|
};
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
init_machine_discovery,
|
|
4
|
+
machine_discovery_exports
|
|
5
|
+
} from "./chunk-IO5DO7DS.js";
|
|
6
|
+
import {
|
|
7
|
+
__toCommonJS
|
|
8
|
+
} from "./chunk-ZLRN7Q7C.js";
|
|
9
|
+
|
|
10
|
+
// src/project-config.ts
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, statSync } from "fs";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import { dirname, join, resolve } from "path";
|
|
14
|
+
import { parse } from "yaml";
|
|
15
|
+
var CONFIG_FILENAME = ".agentseal.yaml";
|
|
16
|
+
function generateConfigYaml(agents, mcpServers) {
|
|
17
|
+
const activeAgents = agents.filter(
|
|
18
|
+
(a) => a.status === "found" || a.status === "installed_no_config"
|
|
19
|
+
);
|
|
20
|
+
const agentTypes = activeAgents.map((a) => a.agent_type);
|
|
21
|
+
const serverNameSet = /* @__PURE__ */ new Set();
|
|
22
|
+
for (const s of mcpServers) {
|
|
23
|
+
serverNameSet.add(s.name);
|
|
24
|
+
}
|
|
25
|
+
const serverNames = Array.from(serverNameSet);
|
|
26
|
+
const agentLines = agentTypes.length > 0 ? agentTypes.map((t) => ` - ${t}`).join("\n") : " # - cursor\n # - claude-desktop";
|
|
27
|
+
const serverLines = serverNames.length > 0 ? serverNames.map((n) => ` - ${n}`).join("\n") : " # - filesystem\n # - sqlite";
|
|
28
|
+
return `# AgentSeal project configuration
|
|
29
|
+
# https://agentseal.org/docs/config
|
|
30
|
+
|
|
31
|
+
# Exit code behavior: "danger" (default), "warning", or "safe"
|
|
32
|
+
fail_on: danger
|
|
33
|
+
|
|
34
|
+
# Agents expected on this machine (unlisted agents trigger GUARD-001)
|
|
35
|
+
allowed_agents:
|
|
36
|
+
${agentLines}
|
|
37
|
+
|
|
38
|
+
# MCP servers expected (unlisted servers trigger GUARD-002)
|
|
39
|
+
# Use "name" or "name@agent_type" for agent-specific allowlisting
|
|
40
|
+
allowed_mcp_servers:
|
|
41
|
+
${serverLines}
|
|
42
|
+
|
|
43
|
+
# Paths to ignore during skill scanning (matched by path segment)
|
|
44
|
+
ignore_paths:
|
|
45
|
+
- node_modules
|
|
46
|
+
- .git
|
|
47
|
+
- __pycache__
|
|
48
|
+
|
|
49
|
+
# Findings to ignore (by code, or code:path for file-specific ignores)
|
|
50
|
+
ignore_findings: []
|
|
51
|
+
# - id: "SKILL-001"
|
|
52
|
+
# reason: "Known safe pattern"
|
|
53
|
+
# - id: "MCP-002:./configs/server.json"
|
|
54
|
+
# reason: "Accepted risk for this file"
|
|
55
|
+
|
|
56
|
+
# Additional rule directories
|
|
57
|
+
rules_paths: []
|
|
58
|
+
# - ./rules
|
|
59
|
+
# - ./custom-rules
|
|
60
|
+
`;
|
|
61
|
+
}
|
|
62
|
+
function runGuardInit(opts) {
|
|
63
|
+
const { targetDir, force = false, interactive = true } = opts ?? {};
|
|
64
|
+
const dir = targetDir ?? process.cwd();
|
|
65
|
+
const configFile = join(dir, CONFIG_FILENAME);
|
|
66
|
+
if (existsSync(configFile) && !force) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
let agents = [];
|
|
70
|
+
let allMcpServers = [];
|
|
71
|
+
try {
|
|
72
|
+
const { scanMachine, scanDirectory } = (init_machine_discovery(), __toCommonJS(machine_discovery_exports));
|
|
73
|
+
const machineResult = scanMachine();
|
|
74
|
+
agents = machineResult.agents;
|
|
75
|
+
allMcpServers = [...machineResult.mcpServers];
|
|
76
|
+
const dirResult = scanDirectory(dir);
|
|
77
|
+
const seen = new Set(allMcpServers.map((s) => `${s.name}::${s.agent_type}`));
|
|
78
|
+
for (const srv of dirResult.mcpServers) {
|
|
79
|
+
const key = `${srv.name}::${srv.agent_type}`;
|
|
80
|
+
if (!seen.has(key)) {
|
|
81
|
+
seen.add(key);
|
|
82
|
+
allMcpServers.push(srv);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
}
|
|
87
|
+
const yaml = generateConfigYaml(agents, allMcpServers);
|
|
88
|
+
writeFileSync(configFile, yaml, "utf-8");
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// src/rules.ts
|
|
93
|
+
import { readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
|
|
94
|
+
import { join as join2 } from "path";
|
|
95
|
+
import { parse as parse2 } from "yaml";
|
|
96
|
+
function fnmatchCase(value, pattern) {
|
|
97
|
+
let re = "";
|
|
98
|
+
let i = 0;
|
|
99
|
+
while (i < pattern.length) {
|
|
100
|
+
const ch = pattern[i];
|
|
101
|
+
if (ch === "*") {
|
|
102
|
+
re += ".*";
|
|
103
|
+
} else if (ch === "?") {
|
|
104
|
+
re += ".";
|
|
105
|
+
} else if (ch === "[") {
|
|
106
|
+
let j = i + 1;
|
|
107
|
+
if (j < pattern.length && pattern[j] === "!") {
|
|
108
|
+
re += "[^";
|
|
109
|
+
j++;
|
|
110
|
+
} else {
|
|
111
|
+
re += "[";
|
|
112
|
+
}
|
|
113
|
+
while (j < pattern.length && pattern[j] !== "]") {
|
|
114
|
+
re += pattern[j];
|
|
115
|
+
j++;
|
|
116
|
+
}
|
|
117
|
+
if (j < pattern.length) {
|
|
118
|
+
re += "]";
|
|
119
|
+
i = j;
|
|
120
|
+
} else {
|
|
121
|
+
re += "\\[";
|
|
122
|
+
}
|
|
123
|
+
} else if (".$^+{}()|\\".includes(ch)) {
|
|
124
|
+
re += "\\" + ch;
|
|
125
|
+
} else {
|
|
126
|
+
re += ch;
|
|
127
|
+
}
|
|
128
|
+
i++;
|
|
129
|
+
}
|
|
130
|
+
return new RegExp(`^${re}$`, "i").test(value);
|
|
131
|
+
}
|
|
132
|
+
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "high", "medium", "low"]);
|
|
133
|
+
var VALID_VERDICTS = /* @__PURE__ */ new Set(["danger", "warning"]);
|
|
134
|
+
var VALID_MATCH_TYPES = /* @__PURE__ */ new Set(["mcp", "skill", "agent"]);
|
|
135
|
+
var REQUIRED_FIELDS = ["id", "title", "severity", "verdict", "match"];
|
|
136
|
+
var RuleEngine = class _RuleEngine {
|
|
137
|
+
rules;
|
|
138
|
+
constructor(rules) {
|
|
139
|
+
this.rules = rules;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Load rules from file paths and/or directory paths.
|
|
143
|
+
*
|
|
144
|
+
* - Files are loaded directly.
|
|
145
|
+
* - Directories are globbed for *.yaml and *.yml files.
|
|
146
|
+
* - Files without a top-level "rules" key are silently skipped.
|
|
147
|
+
* - Validates required fields, severity, verdict, match.type.
|
|
148
|
+
* - Throws on duplicate IDs across files.
|
|
149
|
+
*/
|
|
150
|
+
static fromPaths(paths) {
|
|
151
|
+
const resolvedFiles = [];
|
|
152
|
+
for (const p of paths) {
|
|
153
|
+
const stat = statSync2(p);
|
|
154
|
+
if (stat.isDirectory()) {
|
|
155
|
+
const entries = readdirSync(p);
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
if (entry.endsWith(".yaml") || entry.endsWith(".yml")) {
|
|
158
|
+
resolvedFiles.push(join2(p, entry));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
resolvedFiles.push(p);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const allRules = [];
|
|
166
|
+
const seenIds = /* @__PURE__ */ new Map();
|
|
167
|
+
for (const filePath of resolvedFiles) {
|
|
168
|
+
const raw = readFileSync2(filePath, "utf-8");
|
|
169
|
+
const doc = parse2(raw);
|
|
170
|
+
if (!doc || !("rules" in doc)) {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const rulesList = doc.rules;
|
|
174
|
+
if (!Array.isArray(rulesList)) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
for (const r of rulesList) {
|
|
178
|
+
for (const field of REQUIRED_FIELDS) {
|
|
179
|
+
if (r[field] == null || r[field] === "") {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Rule in ${filePath} is missing required field: ${field}`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const sev = String(r.severity).toLowerCase();
|
|
186
|
+
if (!VALID_SEVERITIES.has(sev)) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Rule "${r.id}" in ${filePath} has invalid severity: "${r.severity}" (must be one of: ${[...VALID_SEVERITIES].join(", ")})`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
const verd = String(r.verdict).toLowerCase();
|
|
192
|
+
if (!VALID_VERDICTS.has(verd)) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Rule "${r.id}" in ${filePath} has invalid verdict: "${r.verdict}" (must be one of: ${[...VALID_VERDICTS].join(", ")})`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const matchType = r.match?.type;
|
|
198
|
+
if (!matchType || !VALID_MATCH_TYPES.has(String(matchType).toLowerCase())) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Rule "${r.id}" in ${filePath} has invalid match.type: "${matchType}" (must be one of: ${[...VALID_MATCH_TYPES].join(", ")})`
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
const id = String(r.id);
|
|
204
|
+
const existingFile = seenIds.get(id);
|
|
205
|
+
if (existingFile) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Duplicate rule ID "${id}" found in ${filePath} (already defined in ${existingFile})`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
seenIds.set(id, filePath);
|
|
211
|
+
const rule = {
|
|
212
|
+
id,
|
|
213
|
+
title: String(r.title),
|
|
214
|
+
description: r.description ? String(r.description) : "",
|
|
215
|
+
severity: sev,
|
|
216
|
+
verdict: verd,
|
|
217
|
+
remediation: r.remediation ? String(r.remediation) : "",
|
|
218
|
+
match: r.match,
|
|
219
|
+
tests: Array.isArray(r.tests) ? r.tests.map((t) => ({
|
|
220
|
+
name: String(t.name ?? ""),
|
|
221
|
+
input: t.input ?? {},
|
|
222
|
+
expect: String(t.expect ?? "no_match")
|
|
223
|
+
})) : [],
|
|
224
|
+
source_file: filePath
|
|
225
|
+
};
|
|
226
|
+
allRules.push(rule);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return new _RuleEngine(allRules);
|
|
230
|
+
}
|
|
231
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
232
|
+
// Internal matching
|
|
233
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
234
|
+
/**
|
|
235
|
+
* Check if a rule matches an entity's data.
|
|
236
|
+
*
|
|
237
|
+
* - AND logic across fields (all fields must match).
|
|
238
|
+
* - OR logic within a field (any pattern in the array matches).
|
|
239
|
+
* - The "type" field in match is skipped (used for routing only).
|
|
240
|
+
*/
|
|
241
|
+
_matchEntity(rule, entityData) {
|
|
242
|
+
for (const [field, patterns] of Object.entries(rule.match)) {
|
|
243
|
+
if (field === "type") continue;
|
|
244
|
+
const patternList = typeof patterns === "string" ? [patterns] : patterns;
|
|
245
|
+
const entityValue = entityData[field] ?? "";
|
|
246
|
+
let fieldMatched = false;
|
|
247
|
+
for (const pattern of patternList) {
|
|
248
|
+
if (fnmatchCase(entityValue, String(pattern))) {
|
|
249
|
+
fieldMatched = true;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (!fieldMatched) return false;
|
|
254
|
+
}
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
258
|
+
// Evaluate methods
|
|
259
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
260
|
+
/**
|
|
261
|
+
* Evaluate MCP rules against a server result.
|
|
262
|
+
*/
|
|
263
|
+
evaluateMcp(server, rawConfig) {
|
|
264
|
+
const mcpRules = this.rules.filter(
|
|
265
|
+
(r) => String(r.match.type).toLowerCase() === "mcp"
|
|
266
|
+
);
|
|
267
|
+
const args = rawConfig.args;
|
|
268
|
+
const argsStr = Array.isArray(args) ? args.join(" ") : String(args ?? "");
|
|
269
|
+
const env = rawConfig.env;
|
|
270
|
+
const envKeys = env ? Object.keys(env).join(" ") : "";
|
|
271
|
+
const envValues = env ? Object.values(env).join(" ") : "";
|
|
272
|
+
const entityData = {
|
|
273
|
+
name: String(server.name ?? ""),
|
|
274
|
+
command: String(server.command ?? ""),
|
|
275
|
+
args: argsStr,
|
|
276
|
+
env_keys: envKeys,
|
|
277
|
+
env_values: envValues,
|
|
278
|
+
source_file: String(server.source_file ?? "")
|
|
279
|
+
};
|
|
280
|
+
const findings = [];
|
|
281
|
+
for (const rule of mcpRules) {
|
|
282
|
+
if (this._matchEntity(rule, entityData)) {
|
|
283
|
+
findings.push({
|
|
284
|
+
code: rule.id,
|
|
285
|
+
title: rule.title,
|
|
286
|
+
severity: rule.severity,
|
|
287
|
+
verdict: rule.verdict,
|
|
288
|
+
remediation: rule.remediation,
|
|
289
|
+
rule_file: rule.source_file,
|
|
290
|
+
entity_type: "mcp",
|
|
291
|
+
entity_name: entityData.name ?? ""
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return findings;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Evaluate skill rules against a skill result.
|
|
299
|
+
*/
|
|
300
|
+
evaluateSkill(skill, content) {
|
|
301
|
+
const skillRules = this.rules.filter(
|
|
302
|
+
(r) => String(r.match.type).toLowerCase() === "skill"
|
|
303
|
+
);
|
|
304
|
+
const entityData = {
|
|
305
|
+
name: String(skill.name ?? ""),
|
|
306
|
+
path: String(skill.path ?? ""),
|
|
307
|
+
content: content.slice(0, 10240)
|
|
308
|
+
};
|
|
309
|
+
const findings = [];
|
|
310
|
+
for (const rule of skillRules) {
|
|
311
|
+
if (this._matchEntity(rule, entityData)) {
|
|
312
|
+
findings.push({
|
|
313
|
+
code: rule.id,
|
|
314
|
+
title: rule.title,
|
|
315
|
+
severity: rule.severity,
|
|
316
|
+
verdict: rule.verdict,
|
|
317
|
+
remediation: rule.remediation,
|
|
318
|
+
rule_file: rule.source_file,
|
|
319
|
+
entity_type: "skill",
|
|
320
|
+
entity_name: entityData.name ?? ""
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return findings;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Evaluate agent rules against an agent config result.
|
|
328
|
+
*/
|
|
329
|
+
evaluateAgent(agent) {
|
|
330
|
+
const agentRules = this.rules.filter(
|
|
331
|
+
(r) => String(r.match.type).toLowerCase() === "agent"
|
|
332
|
+
);
|
|
333
|
+
const entityData = {
|
|
334
|
+
agent_type: String(agent.agent_type ?? ""),
|
|
335
|
+
name: String(agent.name ?? ""),
|
|
336
|
+
config_path: String(agent.config_path ?? "")
|
|
337
|
+
};
|
|
338
|
+
const findings = [];
|
|
339
|
+
for (const rule of agentRules) {
|
|
340
|
+
if (this._matchEntity(rule, entityData)) {
|
|
341
|
+
findings.push({
|
|
342
|
+
code: rule.id,
|
|
343
|
+
title: rule.title,
|
|
344
|
+
severity: rule.severity,
|
|
345
|
+
verdict: rule.verdict,
|
|
346
|
+
remediation: rule.remediation,
|
|
347
|
+
rule_file: rule.source_file,
|
|
348
|
+
entity_type: "agent",
|
|
349
|
+
entity_name: entityData.name ?? ""
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return findings;
|
|
354
|
+
}
|
|
355
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
356
|
+
// Self-test
|
|
357
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
358
|
+
/**
|
|
359
|
+
* Run embedded tests for all rules.
|
|
360
|
+
*/
|
|
361
|
+
runTests() {
|
|
362
|
+
const results = [];
|
|
363
|
+
for (const rule of this.rules) {
|
|
364
|
+
for (const test of rule.tests) {
|
|
365
|
+
const matched = this._matchEntity(rule, test.input);
|
|
366
|
+
const actual = matched ? "match" : "no_match";
|
|
367
|
+
results.push({
|
|
368
|
+
rule_id: rule.id,
|
|
369
|
+
test_name: test.name,
|
|
370
|
+
passed: actual === test.expect,
|
|
371
|
+
expected: test.expect,
|
|
372
|
+
actual
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return results;
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// src/baselines.ts
|
|
381
|
+
import { createHash } from "crypto";
|
|
382
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, readdirSync as readdirSync2, unlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
383
|
+
import { homedir as homedir2 } from "os";
|
|
384
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
385
|
+
function configFingerprint(server) {
|
|
386
|
+
const rawCmd = server.command ?? "";
|
|
387
|
+
const cmdStr = Array.isArray(rawCmd) ? rawCmd.join(" ") : String(rawCmd);
|
|
388
|
+
const parts = [
|
|
389
|
+
cmdStr,
|
|
390
|
+
JSON.stringify([...server.args ?? []].map(String).sort()),
|
|
391
|
+
JSON.stringify(Object.keys(server.env ?? {}).map(String).sort()),
|
|
392
|
+
server.url ?? "",
|
|
393
|
+
JSON.stringify(Object.keys(server.headers ?? {}).map(String).sort())
|
|
394
|
+
];
|
|
395
|
+
return createHash("sha256").update(parts.join("|")).digest("hex");
|
|
396
|
+
}
|
|
397
|
+
function sanitizeName(name) {
|
|
398
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
399
|
+
}
|
|
400
|
+
function rglob(dir, ext) {
|
|
401
|
+
const results = [];
|
|
402
|
+
const walk = (d) => {
|
|
403
|
+
try {
|
|
404
|
+
for (const entry of readdirSync2(d, { withFileTypes: true })) {
|
|
405
|
+
const full = join3(d, entry.name);
|
|
406
|
+
if (entry.isDirectory()) walk(full);
|
|
407
|
+
else if (entry.isFile() && entry.name.endsWith(ext)) results.push(full);
|
|
408
|
+
}
|
|
409
|
+
} catch {
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
walk(dir);
|
|
413
|
+
return results;
|
|
414
|
+
}
|
|
415
|
+
var BaselineStore = class {
|
|
416
|
+
_dir;
|
|
417
|
+
constructor(baselinesDir) {
|
|
418
|
+
this._dir = baselinesDir ?? join3(homedir2(), ".agentseal", "baselines");
|
|
419
|
+
}
|
|
420
|
+
_entryPath(agentType, serverName) {
|
|
421
|
+
return join3(this._dir, sanitizeName(agentType), `${sanitizeName(serverName)}.json`);
|
|
422
|
+
}
|
|
423
|
+
/** Load a stored baseline entry. Returns null if not found. */
|
|
424
|
+
load(agentType, serverName) {
|
|
425
|
+
const path = this._entryPath(agentType, serverName);
|
|
426
|
+
if (!existsSync2(path)) return null;
|
|
427
|
+
try {
|
|
428
|
+
const data = JSON.parse(readFileSync3(path, "utf-8"));
|
|
429
|
+
return data;
|
|
430
|
+
} catch {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/** Save a baseline entry to disk. */
|
|
435
|
+
save(entry) {
|
|
436
|
+
const path = this._entryPath(entry.agent_type, entry.server_name);
|
|
437
|
+
mkdirSync(dirname2(path), { recursive: true });
|
|
438
|
+
writeFileSync2(path, JSON.stringify(entry, null, 2), "utf-8");
|
|
439
|
+
}
|
|
440
|
+
/** Check a single MCP server against its stored baseline. */
|
|
441
|
+
checkServer(server) {
|
|
442
|
+
const name = server.name ?? "unknown";
|
|
443
|
+
const agentType = server.agent_type ?? "unknown";
|
|
444
|
+
const rawCmd = server.command ?? "";
|
|
445
|
+
const command = Array.isArray(rawCmd) ? rawCmd.join(" ") : String(rawCmd);
|
|
446
|
+
const args = (server.args ?? []).filter((a) => typeof a === "string");
|
|
447
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
448
|
+
const configHash = configFingerprint(server);
|
|
449
|
+
const existing = this.load(agentType, name);
|
|
450
|
+
if (existing === null) {
|
|
451
|
+
this.save({
|
|
452
|
+
server_name: name,
|
|
453
|
+
agent_type: agentType,
|
|
454
|
+
config_hash: configHash,
|
|
455
|
+
binary_hash: null,
|
|
456
|
+
binary_path: null,
|
|
457
|
+
command,
|
|
458
|
+
args,
|
|
459
|
+
first_seen: now,
|
|
460
|
+
last_verified: now
|
|
461
|
+
});
|
|
462
|
+
return {
|
|
463
|
+
server_name: name,
|
|
464
|
+
agent_type: agentType,
|
|
465
|
+
change_type: "new_server",
|
|
466
|
+
detail: `New MCP server '${name}' baselined.`
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
if (existing.config_hash !== configHash) {
|
|
470
|
+
const change = {
|
|
471
|
+
server_name: name,
|
|
472
|
+
agent_type: agentType,
|
|
473
|
+
change_type: "config_changed",
|
|
474
|
+
old_value: existing.config_hash.slice(0, 12),
|
|
475
|
+
new_value: configHash.slice(0, 12),
|
|
476
|
+
detail: `Config for '${name}' changed (command/args/env modified).`
|
|
477
|
+
};
|
|
478
|
+
existing.config_hash = configHash;
|
|
479
|
+
existing.command = command;
|
|
480
|
+
existing.args = args;
|
|
481
|
+
existing.last_verified = now;
|
|
482
|
+
this.save(existing);
|
|
483
|
+
return change;
|
|
484
|
+
}
|
|
485
|
+
existing.last_verified = now;
|
|
486
|
+
this.save(existing);
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
/** Check all servers. Returns list of changes (empty = no changes). */
|
|
490
|
+
checkAll(servers, includeNew = false) {
|
|
491
|
+
const changes = [];
|
|
492
|
+
for (const srv of servers) {
|
|
493
|
+
const change = this.checkServer(srv);
|
|
494
|
+
if (change === null) continue;
|
|
495
|
+
if (change.change_type === "new_server" && !includeNew) continue;
|
|
496
|
+
changes.push(change);
|
|
497
|
+
}
|
|
498
|
+
return changes;
|
|
499
|
+
}
|
|
500
|
+
/** Remove all baselines. Returns count of entries removed. */
|
|
501
|
+
reset() {
|
|
502
|
+
let count = 0;
|
|
503
|
+
for (const f of rglob(this._dir, ".json")) {
|
|
504
|
+
try {
|
|
505
|
+
unlinkSync(f);
|
|
506
|
+
count++;
|
|
507
|
+
} catch {
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
return count;
|
|
511
|
+
}
|
|
512
|
+
/** List all stored baseline entries. */
|
|
513
|
+
listEntries() {
|
|
514
|
+
const entries = [];
|
|
515
|
+
for (const f of rglob(this._dir, ".json")) {
|
|
516
|
+
try {
|
|
517
|
+
const data = JSON.parse(readFileSync3(f, "utf-8"));
|
|
518
|
+
entries.push(data);
|
|
519
|
+
} catch {
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return entries;
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
export {
|
|
527
|
+
runGuardInit,
|
|
528
|
+
RuleEngine,
|
|
529
|
+
BaselineStore
|
|
530
|
+
};
|