mythos-sentinel 0.1.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/LICENSE +21 -0
- package/README.md +362 -0
- package/action.yml +43 -0
- package/assets/banner.png +0 -0
- package/bin/mythos-sentinel-mcp.js +7 -0
- package/bin/mythos-sentinel.js +8 -0
- package/docs/ARCHITECTURE.md +55 -0
- package/docs/BASE_X402.md +33 -0
- package/docs/BAZAAR_ADAPTER.md +41 -0
- package/docs/DASHBOARD.md +22 -0
- package/docs/FALLBACK_ROUTING.md +37 -0
- package/docs/MCP.md +70 -0
- package/docs/PASSIVE_SCORING.md +33 -0
- package/docs/ROUTESCORE.md +101 -0
- package/docs/RUNTIME_MCP_PROXY.md +90 -0
- package/docs/SPEND_FIREWALL.md +50 -0
- package/docs/TELEMETRY.md +74 -0
- package/docs/THREAT_MODEL.md +28 -0
- package/docs/X402_RECEIPTS.md +54 -0
- package/examples/base/mythos.policy.json +142 -0
- package/examples/claude_desktop/mcp.json +8 -0
- package/examples/codex/AGENTS.md +31 -0
- package/examples/cursor/mcp.json +8 -0
- package/examples/github/verify.yml +29 -0
- package/examples/routescore/services.yml +19 -0
- package/examples/skill/mythos.skill.json +20 -0
- package/package.json +79 -0
- package/schemas/agent-receipt.schema.json +17 -0
- package/schemas/policy.schema.json +322 -0
- package/schemas/sentinel-report.schema.json +14 -0
- package/schemas/skill.manifest.schema.json +42 -0
- package/src/cli.js +570 -0
- package/src/core/fs.js +88 -0
- package/src/core/path-utils.js +54 -0
- package/src/core/policy.js +326 -0
- package/src/core/receipt.js +52 -0
- package/src/core/routescore.js +576 -0
- package/src/core/snapshot.js +35 -0
- package/src/core/telemetry.js +214 -0
- package/src/core/x402-receipts.js +303 -0
- package/src/index.js +19 -0
- package/src/mcp/proxy.js +493 -0
- package/src/mcp/server.js +226 -0
- package/src/report/format.js +53 -0
- package/src/report/sarif.js +50 -0
- package/src/scanner/rules.js +185 -0
- package/src/scanner/scan.js +118 -0
- package/src/ui/server.js +346 -0
- package/src/ui/static/app.js +210 -0
- package/src/ui/static/index.html +342 -0
- package/src/ui/static/styles.css +904 -0
- package/src/version.js +2 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import { scanPath } from '../scanner/scan.js';
|
|
3
|
+
import { loadPolicy, checkPayment, checkCommand, checkFilesystemAccess, checkNetwork } from '../core/policy.js';
|
|
4
|
+
import { createSnapshot } from '../core/snapshot.js';
|
|
5
|
+
import { loadRouteScoreServices, recommendService, routeService, serviceForDomain, scoreService, listServiceCategories } from '../core/routescore.js';
|
|
6
|
+
import { VERSION } from '../version.js';
|
|
7
|
+
import { normalizeX402Receipt } from '../core/x402-receipts.js';
|
|
8
|
+
|
|
9
|
+
const tools = [
|
|
10
|
+
{
|
|
11
|
+
name: 'sentinel_scan_path',
|
|
12
|
+
description: 'Scan an agent skill, MCP server, repository, or folder for risky commands, prompt injection, secrets, wallet access, and policy violations.',
|
|
13
|
+
inputSchema: {
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
path: { type: 'string', description: 'Path to scan. Defaults to current directory.' },
|
|
17
|
+
policyPath: { type: 'string', description: 'Path to mythos.policy.json.' }
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'sentinel_check_x402_payment',
|
|
23
|
+
description: 'Check whether an x402/Base payment is allowed by the local Sentinel policy.',
|
|
24
|
+
inputSchema: {
|
|
25
|
+
type: 'object',
|
|
26
|
+
required: ['domain', 'amountUSDC'],
|
|
27
|
+
properties: {
|
|
28
|
+
domain: { type: 'string' },
|
|
29
|
+
amountUSDC: { type: 'number' },
|
|
30
|
+
dailySpentUSDC: { type: 'number' },
|
|
31
|
+
policyPath: { type: 'string' }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'sentinel_check_command',
|
|
37
|
+
description: 'Check whether a shell command is allowed, blocked, or requires approval by the local Sentinel policy.',
|
|
38
|
+
inputSchema: {
|
|
39
|
+
type: 'object',
|
|
40
|
+
required: ['command'],
|
|
41
|
+
properties: {
|
|
42
|
+
command: { type: 'string' },
|
|
43
|
+
policyPath: { type: 'string' }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'sentinel_check_file',
|
|
49
|
+
description: 'Check whether an agent can read or write a file path under the local Sentinel policy.',
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
required: ['path'],
|
|
53
|
+
properties: {
|
|
54
|
+
path: { type: 'string' },
|
|
55
|
+
operation: { type: 'string', enum: ['read', 'write'] },
|
|
56
|
+
policyPath: { type: 'string' }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'sentinel_check_network',
|
|
62
|
+
description: 'Check whether network access to a domain is allowed by the local Sentinel policy.',
|
|
63
|
+
inputSchema: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
required: ['domain'],
|
|
66
|
+
properties: {
|
|
67
|
+
domain: { type: 'string' },
|
|
68
|
+
policyPath: { type: 'string' }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'sentinel_recommend_x402_service',
|
|
74
|
+
description: 'Recommend a seed x402/Base paid API by category, price, and RouteScore before an agent spends money.',
|
|
75
|
+
inputSchema: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
properties: {
|
|
78
|
+
category: { type: 'string', description: 'Example: web_search, content_extraction, inference, web3_data, wallet_intel.' },
|
|
79
|
+
maxPriceUSDC: { type: 'number', description: 'Maximum acceptable price per call.' }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: 'sentinel_route_x402_service',
|
|
85
|
+
description: 'Select the best known x402 service plus fallback services by category, price, query, and local RouteScore telemetry.',
|
|
86
|
+
inputSchema: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
properties: {
|
|
89
|
+
category: { type: 'string', description: 'Example: web_search, content_extraction, inference, web3_data, wallet_intel.' },
|
|
90
|
+
maxPriceUSDC: { type: 'number', description: 'Maximum acceptable price per call.' },
|
|
91
|
+
query: { type: 'string', description: 'Optional semantic keyword filter over local/imported services.' },
|
|
92
|
+
minScore: { type: 'number', description: 'Optional minimum RouteScore required for selected/fallback services.' },
|
|
93
|
+
policyPath: { type: 'string' }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
name: 'sentinel_list_service_categories',
|
|
99
|
+
description: 'List supported RouteScore service categories and aliases for routing paid agent APIs.',
|
|
100
|
+
inputSchema: { type: 'object', properties: {} }
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'sentinel_parse_x402_receipt',
|
|
104
|
+
description: 'Normalize an x402 payment receipt or payment-response payload without storing prompts, responses, secrets, or wallet balances.',
|
|
105
|
+
inputSchema: {
|
|
106
|
+
type: 'object',
|
|
107
|
+
properties: {
|
|
108
|
+
receipt: { type: 'object', description: 'x402 receipt/payment response JSON.' },
|
|
109
|
+
raw: { type: 'string', description: 'Optional raw/base64 payment response blob.' }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'sentinel_score_x402_domain',
|
|
115
|
+
description: 'Return the seed catalog score for a payment domain if Sentinel knows it.',
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: 'object',
|
|
118
|
+
required: ['domain'],
|
|
119
|
+
properties: {
|
|
120
|
+
domain: { type: 'string' }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'sentinel_snapshot',
|
|
126
|
+
description: 'Create a hash snapshot of a workspace before an agent changes files.',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
type: 'object',
|
|
129
|
+
properties: {
|
|
130
|
+
path: { type: 'string', description: 'Path to snapshot. Defaults to current directory.' }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
export async function runMcpServer({ input = process.stdin, output = process.stdout } = {}) {
|
|
137
|
+
const rl = readline.createInterface({ input, crlfDelay: Infinity });
|
|
138
|
+
for await (const line of rl) {
|
|
139
|
+
const trimmed = line.trim();
|
|
140
|
+
if (!trimmed) continue;
|
|
141
|
+
let message;
|
|
142
|
+
try {
|
|
143
|
+
message = JSON.parse(trimmed);
|
|
144
|
+
const response = await handleMessage(message);
|
|
145
|
+
if (response) output.write(`${JSON.stringify(response)}\n`);
|
|
146
|
+
} catch (error) {
|
|
147
|
+
const id = message?.id ?? null;
|
|
148
|
+
output.write(`${JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32000, message: error.message } })}\n`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function handleMessage(message) {
|
|
154
|
+
const { id, method, params = {} } = message;
|
|
155
|
+
if (!method || id === undefined) return null;
|
|
156
|
+
if (method === 'initialize') {
|
|
157
|
+
return result(id, {
|
|
158
|
+
protocolVersion: params.protocolVersion || '2025-06-18',
|
|
159
|
+
serverInfo: { name: 'mythos-sentinel', version: VERSION },
|
|
160
|
+
capabilities: { tools: {} }
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (method === 'tools/list') return result(id, { tools });
|
|
164
|
+
if (method === 'tools/call') return result(id, await callTool(params.name, params.arguments || {}));
|
|
165
|
+
return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function callTool(name, args) {
|
|
169
|
+
const policy = await loadPolicy(args.policyPath || 'mythos.policy.json');
|
|
170
|
+
if (name === 'sentinel_scan_path') {
|
|
171
|
+
const report = await scanPath(args.path || '.', { policy });
|
|
172
|
+
return content(report);
|
|
173
|
+
}
|
|
174
|
+
if (name === 'sentinel_check_x402_payment') {
|
|
175
|
+
const services = await loadRouteScoreServices({ rootDir: process.cwd() });
|
|
176
|
+
const matched = serviceForDomain(args.domain, services);
|
|
177
|
+
const score = matched ? scoreService(matched).score : args.routeScore;
|
|
178
|
+
const decision = checkPayment({ domain: args.domain, amountUSDC: args.amountUSDC, dailySpentUSDC: args.dailySpentUSDC || 0, unknownDailySpentUSDC: args.unknownDailySpentUSDC || 0, routeScore: score, category: args.category, knownService: Boolean(matched) }, policy);
|
|
179
|
+
return content(decision);
|
|
180
|
+
}
|
|
181
|
+
if (name === 'sentinel_check_command') {
|
|
182
|
+
return content(checkCommand({ command: args.command }, policy));
|
|
183
|
+
}
|
|
184
|
+
if (name === 'sentinel_check_file') {
|
|
185
|
+
return content(checkFilesystemAccess({ filePath: args.path, operation: args.operation || 'read' }, policy));
|
|
186
|
+
}
|
|
187
|
+
if (name === 'sentinel_check_network') {
|
|
188
|
+
return content(checkNetwork({ domain: args.domain }, policy));
|
|
189
|
+
}
|
|
190
|
+
if (name === 'sentinel_recommend_x402_service') {
|
|
191
|
+
const services = await loadRouteScoreServices({ rootDir: process.cwd() });
|
|
192
|
+
return content(recommendService({ category: args.category, maxPriceUSDC: args.maxPriceUSDC, query: args.query, services }));
|
|
193
|
+
}
|
|
194
|
+
if (name === 'sentinel_route_x402_service') {
|
|
195
|
+
const services = await loadRouteScoreServices({ rootDir: process.cwd() });
|
|
196
|
+
return content(routeService({ category: args.category, maxPriceUSDC: args.maxPriceUSDC, query: args.query, minScore: args.minScore || 0, services }));
|
|
197
|
+
}
|
|
198
|
+
if (name === 'sentinel_list_service_categories') {
|
|
199
|
+
return content({ ok: true, categories: listServiceCategories() });
|
|
200
|
+
}
|
|
201
|
+
if (name === 'sentinel_parse_x402_receipt') {
|
|
202
|
+
return content(normalizeX402Receipt(args.receipt || args.raw || args, { source: 'mcp' }));
|
|
203
|
+
}
|
|
204
|
+
if (name === 'sentinel_score_x402_domain') {
|
|
205
|
+
const services = await loadRouteScoreServices({ rootDir: process.cwd() });
|
|
206
|
+
const matched = serviceForDomain(args.domain, services);
|
|
207
|
+
return content(matched ? scoreService(matched) : { ok: false, decision: 'unknown', domain: args.domain, reasons: ['domain is not in the local RouteScore catalog'] });
|
|
208
|
+
}
|
|
209
|
+
if (name === 'sentinel_snapshot') {
|
|
210
|
+
const snapshot = await createSnapshot(args.path || '.');
|
|
211
|
+
return content(snapshot);
|
|
212
|
+
}
|
|
213
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function content(data) {
|
|
217
|
+
return {
|
|
218
|
+
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
219
|
+
structuredContent: data,
|
|
220
|
+
isError: data.ok === false || data.summary?.ok === false
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function result(id, value) {
|
|
225
|
+
return { jsonrpc: '2.0', id, result: value };
|
|
226
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { SEVERITY_ORDER } from '../core/policy.js';
|
|
2
|
+
|
|
3
|
+
const SYMBOLS = {
|
|
4
|
+
info: 'ℹ',
|
|
5
|
+
low: '◦',
|
|
6
|
+
medium: '▲',
|
|
7
|
+
high: '◆',
|
|
8
|
+
critical: '✖'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function formatScanReport(report) {
|
|
12
|
+
const { summary, files, findings } = report;
|
|
13
|
+
const lines = [];
|
|
14
|
+
lines.push('');
|
|
15
|
+
lines.push('Mythos Sentinel Report');
|
|
16
|
+
lines.push('======================');
|
|
17
|
+
lines.push(`Target: ${report.target}`);
|
|
18
|
+
lines.push(`Files: ${files.scanned}/${files.total} scanned, ${files.skipped} skipped`);
|
|
19
|
+
lines.push(`Findings: ${summary.findingCount} | Highest: ${summary.highestSeverity.toUpperCase()} | Status: ${summary.ok ? 'PASS' : 'FAIL'}`);
|
|
20
|
+
if (!findings.length) {
|
|
21
|
+
lines.push('');
|
|
22
|
+
lines.push('No findings.');
|
|
23
|
+
return lines.join('\n');
|
|
24
|
+
}
|
|
25
|
+
lines.push('');
|
|
26
|
+
for (const finding of sortFindings(findings)) {
|
|
27
|
+
lines.push(`${SYMBOLS[finding.severity] || '!'} [${finding.severity.toUpperCase()}] ${finding.id} ${finding.title}`);
|
|
28
|
+
lines.push(` ${finding.file}:${finding.line}`);
|
|
29
|
+
if (finding.evidence) lines.push(` evidence: ${finding.evidence}`);
|
|
30
|
+
lines.push(` fix: ${finding.recommendation}`);
|
|
31
|
+
lines.push('');
|
|
32
|
+
}
|
|
33
|
+
return lines.join('\n');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formatPaymentDecision(decision) {
|
|
37
|
+
const lines = [];
|
|
38
|
+
lines.push(`Decision: ${decision.decision.toUpperCase()} ${decision.ok ? '✅' : '⛔'}`);
|
|
39
|
+
if (decision.subject) lines.push(`Subject: ${decision.subject}`);
|
|
40
|
+
if (decision.trustTier) lines.push(`Trust: ${decision.trustTier}`);
|
|
41
|
+
if (decision.amountUSDC !== undefined) lines.push(`Amount: ${decision.amountUSDC} USDC`);
|
|
42
|
+
if (decision.routeScore !== undefined && decision.routeScore !== null) lines.push(`RouteScore: ${decision.routeScore}/100`);
|
|
43
|
+
for (const reason of decision.reasons || []) lines.push(`- ${reason}`);
|
|
44
|
+
return lines.join('\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sortFindings(findings) {
|
|
48
|
+
return [...findings].sort((a, b) => {
|
|
49
|
+
const s = SEVERITY_ORDER.indexOf(b.severity) - SEVERITY_ORDER.indexOf(a.severity);
|
|
50
|
+
if (s) return s;
|
|
51
|
+
return `${a.file}:${a.line}`.localeCompare(`${b.file}:${b.line}`);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function toSarif(report) {
|
|
2
|
+
const rules = new Map();
|
|
3
|
+
for (const finding of report.findings) {
|
|
4
|
+
if (!rules.has(finding.id)) {
|
|
5
|
+
rules.set(finding.id, {
|
|
6
|
+
id: finding.id,
|
|
7
|
+
name: finding.title,
|
|
8
|
+
shortDescription: { text: finding.title },
|
|
9
|
+
fullDescription: { text: finding.recommendation },
|
|
10
|
+
properties: { category: finding.category, tags: finding.tags || [], severity: finding.severity }
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
version: '2.1.0',
|
|
17
|
+
$schema: 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
18
|
+
runs: [
|
|
19
|
+
{
|
|
20
|
+
tool: {
|
|
21
|
+
driver: {
|
|
22
|
+
name: 'Mythos Sentinel',
|
|
23
|
+
informationUri: 'https://github.com/thewaltero/mythos-sentinel',
|
|
24
|
+
rules: [...rules.values()]
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
results: report.findings.map((finding) => ({
|
|
28
|
+
ruleId: finding.id,
|
|
29
|
+
level: sarifLevel(finding.severity),
|
|
30
|
+
message: { text: `${finding.title}. ${finding.recommendation}` },
|
|
31
|
+
locations: [
|
|
32
|
+
{
|
|
33
|
+
physicalLocation: {
|
|
34
|
+
artifactLocation: { uri: finding.file },
|
|
35
|
+
region: { startLine: finding.line || 1 }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
properties: { severity: finding.severity, category: finding.category, evidence: finding.evidence }
|
|
40
|
+
}))
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sarifLevel(severity) {
|
|
47
|
+
if (severity === 'critical' || severity === 'high') return 'error';
|
|
48
|
+
if (severity === 'medium') return 'warning';
|
|
49
|
+
return 'note';
|
|
50
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
export const CONTENT_RULES = [
|
|
2
|
+
{
|
|
3
|
+
id: 'MS-SECRET-001',
|
|
4
|
+
title: 'Private key material detected',
|
|
5
|
+
severity: 'critical',
|
|
6
|
+
category: 'secrets',
|
|
7
|
+
pattern: /-----BEGIN (RSA |OPENSSH |EC |DSA |)?PRIVATE KEY-----/i,
|
|
8
|
+
recommendation: 'Remove private key material, rotate the key, and use a secret manager.',
|
|
9
|
+
tags: ['secret', 'credential', 'wallet-risk']
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: 'MS-SECRET-002',
|
|
13
|
+
title: 'Potential wallet or API secret variable',
|
|
14
|
+
severity: 'critical',
|
|
15
|
+
category: 'secrets',
|
|
16
|
+
pattern: /\b(PRIVATE_KEY|WALLET_PRIVATE_KEY|MNEMONIC|SEED_PHRASE|CDP_API_KEY_SECRET|OPENAI_API_KEY|ANTHROPIC_API_KEY)\b\s*[:=]\s*['\"]?[^\s'\"]{12,}/i,
|
|
17
|
+
recommendation: 'Move secrets into a secret manager and never package them inside agent skills.',
|
|
18
|
+
tags: ['secret', 'api-key', 'wallet-risk']
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'MS-CMD-001',
|
|
22
|
+
title: 'Remote script piped into shell',
|
|
23
|
+
severity: 'critical',
|
|
24
|
+
category: 'command-execution',
|
|
25
|
+
pattern: /\b(curl|wget)\b[^\n|;]+\|\s*(sudo\s+)?(bash|sh|zsh)\b/i,
|
|
26
|
+
recommendation: 'Do not pipe remote content directly into a shell. Pin and inspect installers.',
|
|
27
|
+
tags: ['supply-chain', 'rce']
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'MS-CMD-002',
|
|
31
|
+
title: 'Dangerous recursive delete command',
|
|
32
|
+
severity: 'critical',
|
|
33
|
+
category: 'command-execution',
|
|
34
|
+
pattern: /\brm\s+-rf\s+(\/|~|\$HOME|\.\.\/)/i,
|
|
35
|
+
recommendation: 'Require explicit human approval for destructive commands.',
|
|
36
|
+
tags: ['destructive', 'shell']
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'MS-CMD-004',
|
|
40
|
+
title: 'World-writable permission change',
|
|
41
|
+
severity: 'high',
|
|
42
|
+
category: 'command-execution',
|
|
43
|
+
pattern: /\bchmod\s+(?:-R\s+)?777\b/i,
|
|
44
|
+
recommendation: 'Avoid world-writable permissions. Use least-privilege file modes.',
|
|
45
|
+
tags: ['filesystem', 'privilege']
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: 'MS-CMD-005',
|
|
49
|
+
title: 'Encoded PowerShell command',
|
|
50
|
+
severity: 'high',
|
|
51
|
+
category: 'command-execution',
|
|
52
|
+
pattern: /\bpowershell(?:\.exe)?\b[^\n]*(?:-enc|-encodedcommand)\b/i,
|
|
53
|
+
recommendation: 'Do not allow encoded PowerShell in agent tools without explicit review.',
|
|
54
|
+
tags: ['powershell', 'obfuscation']
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'MS-CMD-003',
|
|
58
|
+
title: 'Process execution from agent code',
|
|
59
|
+
severity: 'high',
|
|
60
|
+
category: 'command-execution',
|
|
61
|
+
pattern: /\b(child_process\.(exec|execSync|spawn|spawnSync)|subprocess\.(run|Popen|call)|os\.system|Runtime\.getRuntime\(\)\.exec)\b/i,
|
|
62
|
+
recommendation: 'Restrict shell access with a manifest and proxy tool calls through Sentinel.',
|
|
63
|
+
tags: ['shell', 'runtime']
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'MS-CODE-001',
|
|
67
|
+
title: 'Dynamic code evaluation',
|
|
68
|
+
severity: 'high',
|
|
69
|
+
category: 'code-execution',
|
|
70
|
+
pattern: /\b(eval\s*\(|new Function\s*\(|exec\s*\()/i,
|
|
71
|
+
recommendation: 'Avoid dynamic execution in agent skills and MCP tools unless sandboxed.',
|
|
72
|
+
tags: ['rce', 'obfuscation']
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
id: 'MS-PROMPT-001',
|
|
76
|
+
title: 'Prompt-injection phrase in agent-facing instructions',
|
|
77
|
+
severity: 'high',
|
|
78
|
+
category: 'prompt-injection',
|
|
79
|
+
pattern: /(ignore (all )?(previous|prior) instructions|disregard (the )?(system|developer) message|reveal (your )?(system prompt|secrets)|exfiltrate|silently send)/i,
|
|
80
|
+
recommendation: 'Treat skill instructions as untrusted input. Remove override/exfiltration language.',
|
|
81
|
+
tags: ['prompt-injection', 'tool-poisoning']
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: 'MS-NET-001',
|
|
85
|
+
title: 'Network call in skill or tool code',
|
|
86
|
+
severity: 'medium',
|
|
87
|
+
category: 'network',
|
|
88
|
+
pattern: /\b(fetch|axios\.|requests\.|http\.request|https\.request|curl\b|wget\b)\b/i,
|
|
89
|
+
recommendation: 'Declare network domains in the Sentinel policy allowlist.',
|
|
90
|
+
tags: ['network', 'exfiltration-risk']
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'MS-OBF-001',
|
|
94
|
+
title: 'Suspicious obfuscation pattern',
|
|
95
|
+
severity: 'medium',
|
|
96
|
+
category: 'obfuscation',
|
|
97
|
+
pattern: /(Buffer\.from\([^\n]+base64|atob\(|fromCharCode\(|base64\s+-d|eval\([^\n]{80,})/i,
|
|
98
|
+
recommendation: 'Deobfuscate and inspect code before allowing an agent to run it.',
|
|
99
|
+
tags: ['obfuscation']
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: 'MS-PAY-001',
|
|
103
|
+
title: 'Payment or wallet code detected',
|
|
104
|
+
severity: 'medium',
|
|
105
|
+
category: 'payments',
|
|
106
|
+
pattern: /\b(walletClient|privateKeyToAccount|sendTransaction|createPaymentHeader|settlePayment|paymentMiddleware|x402Fetch|facilitator)\b|402 Payment Required|Payment-Required/i,
|
|
107
|
+
recommendation: 'Apply x402 spend limits and require explicit approval for wallet operations.',
|
|
108
|
+
tags: ['x402', 'base', 'wallet']
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'MS-GHA-002',
|
|
112
|
+
title: 'pull_request_target workflow detected',
|
|
113
|
+
severity: 'high',
|
|
114
|
+
category: 'ci',
|
|
115
|
+
pattern: /pull_request_target:/i,
|
|
116
|
+
recommendation: 'Avoid pull_request_target for untrusted code paths unless the workflow is carefully isolated.',
|
|
117
|
+
tags: ['github-actions', 'supply-chain']
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'MS-GHA-001',
|
|
121
|
+
title: 'Broad GitHub Actions permission',
|
|
122
|
+
severity: 'high',
|
|
123
|
+
category: 'ci',
|
|
124
|
+
pattern: /permissions:\s*[\r\n]+(?:\s+\w+-token:\s+write\s*[\r\n]+|\s+contents:\s+write\s*[\r\n]+|\s+actions:\s+write\s*[\r\n]+){2,}|permissions:\s*write-all/i,
|
|
125
|
+
recommendation: 'Use least-privilege GitHub Actions permissions.',
|
|
126
|
+
tags: ['github-actions', 'supply-chain']
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
id: 'MS-MCP-001',
|
|
130
|
+
title: 'MCP server command launches a shell',
|
|
131
|
+
severity: 'medium',
|
|
132
|
+
category: 'mcp',
|
|
133
|
+
pattern: /\"command\"\s*:\s*\"(?:bash|sh|zsh|powershell|cmd(?:\.exe)?)\"/i,
|
|
134
|
+
recommendation: 'Prefer direct executable commands over shell wrappers for MCP servers.',
|
|
135
|
+
tags: ['mcp', 'tooling']
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'MS-NPM-001',
|
|
139
|
+
title: 'Package lifecycle script detected',
|
|
140
|
+
severity: 'medium',
|
|
141
|
+
category: 'supply-chain',
|
|
142
|
+
pattern: /"(preinstall|install|postinstall|prepare)"\s*:\s*"[^"]+"/i,
|
|
143
|
+
recommendation: 'Review package lifecycle scripts before agent installation.',
|
|
144
|
+
tags: ['npm', 'supply-chain']
|
|
145
|
+
}
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
export const PATH_RULES = [
|
|
149
|
+
{
|
|
150
|
+
id: 'MS-FS-001',
|
|
151
|
+
title: 'Sensitive environment file included',
|
|
152
|
+
severity: 'high',
|
|
153
|
+
category: 'filesystem',
|
|
154
|
+
pattern: /(^|\/)\.env(\.|$)/i,
|
|
155
|
+
recommendation: 'Do not expose .env files to agents or package them in skills.',
|
|
156
|
+
tags: ['secrets', 'filesystem']
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: 'MS-FS-002',
|
|
160
|
+
title: 'SSH key path included',
|
|
161
|
+
severity: 'critical',
|
|
162
|
+
category: 'filesystem',
|
|
163
|
+
pattern: /(^|\/)(id_rsa|id_ed25519|\.ssh)(\/|$)/i,
|
|
164
|
+
recommendation: 'Never let agents read SSH private keys.',
|
|
165
|
+
tags: ['ssh', 'credential']
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'MS-FS-004',
|
|
169
|
+
title: 'Wallet or keystore file path included',
|
|
170
|
+
severity: 'critical',
|
|
171
|
+
category: 'filesystem',
|
|
172
|
+
pattern: /(^|\/)(wallet\.json|keystore|UTC--|seed\.txt|mnemonic\.txt)(\/|$)/i,
|
|
173
|
+
recommendation: 'Keep wallet and keystore files outside agent-accessible workspaces.',
|
|
174
|
+
tags: ['wallet', 'credential']
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: 'MS-FS-003',
|
|
178
|
+
title: 'Private key file extension included',
|
|
179
|
+
severity: 'critical',
|
|
180
|
+
category: 'filesystem',
|
|
181
|
+
pattern: /\.(pem|key|p12|pfx)$/i,
|
|
182
|
+
recommendation: 'Move private key files out of the agent-accessible workspace.',
|
|
183
|
+
tags: ['secret', 'credential']
|
|
184
|
+
}
|
|
185
|
+
];
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { walkFiles, readTextMaybe, sha256File } from '../core/fs.js';
|
|
4
|
+
import { matchesAnyGlob } from '../core/path-utils.js';
|
|
5
|
+
import { CONTENT_RULES, PATH_RULES } from './rules.js';
|
|
6
|
+
import { VERSION } from '../version.js';
|
|
7
|
+
import { evaluateFindings } from '../core/policy.js';
|
|
8
|
+
|
|
9
|
+
export async function scanPath(targetPath = '.', options = {}) {
|
|
10
|
+
const root = path.resolve(targetPath);
|
|
11
|
+
const policy = options.policy || {};
|
|
12
|
+
const startedAt = new Date().toISOString();
|
|
13
|
+
const ignore = [
|
|
14
|
+
...(policy.scanner?.ignore || []),
|
|
15
|
+
...(await readMythosIgnore(root)),
|
|
16
|
+
...(options.ignore || [])
|
|
17
|
+
];
|
|
18
|
+
const files = await walkFiles(root, { ignore });
|
|
19
|
+
const findings = [];
|
|
20
|
+
const scannedFiles = [];
|
|
21
|
+
const skippedFiles = [];
|
|
22
|
+
|
|
23
|
+
for (const file of files) {
|
|
24
|
+
const rel = file.rel;
|
|
25
|
+
for (const rule of PATH_RULES) {
|
|
26
|
+
if (rule.pattern.test(rel)) findings.push(makeFinding(rule, rel, 1, rel));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (matchesAnyGlob(rel, policy.filesystem?.deny || [])) {
|
|
30
|
+
findings.push({
|
|
31
|
+
id: 'MS-POLICY-FS-DENY',
|
|
32
|
+
title: 'File path violates Sentinel filesystem deny policy',
|
|
33
|
+
severity: 'high',
|
|
34
|
+
category: 'policy',
|
|
35
|
+
file: rel,
|
|
36
|
+
line: 1,
|
|
37
|
+
evidence: rel,
|
|
38
|
+
recommendation: 'Move this file outside the agent workspace or update policy deliberately.',
|
|
39
|
+
confidence: 'high',
|
|
40
|
+
tags: ['policy', 'filesystem']
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const read = await readTextMaybe(file.absPath, options.maxBytes || 512 * 1024);
|
|
45
|
+
if (read.skipped) {
|
|
46
|
+
skippedFiles.push({ file: rel, reason: read.reason });
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
scannedFiles.push({ file: rel, bytes: read.size, sha256: await sha256File(file.absPath) });
|
|
50
|
+
findings.push(...scanText(rel, read.text));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const summary = evaluateFindings(findings, policy, options.failOn);
|
|
54
|
+
return {
|
|
55
|
+
schema: 'https://mythos.dev/schemas/sentinel-report.v0.json',
|
|
56
|
+
tool: { name: 'mythos-sentinel', version: VERSION },
|
|
57
|
+
target: root,
|
|
58
|
+
startedAt,
|
|
59
|
+
completedAt: new Date().toISOString(),
|
|
60
|
+
summary,
|
|
61
|
+
files: { total: files.length, scanned: scannedFiles.length, skipped: skippedFiles.length },
|
|
62
|
+
scannedFiles,
|
|
63
|
+
skippedFiles,
|
|
64
|
+
findings
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function scanText(file, text) {
|
|
69
|
+
const findings = [];
|
|
70
|
+
const lines = text.split(/\r?\n/);
|
|
71
|
+
for (const rule of CONTENT_RULES) {
|
|
72
|
+
for (let i = 0; i < lines.length; i++) {
|
|
73
|
+
const line = lines[i];
|
|
74
|
+
if (rule.pattern.test(line)) {
|
|
75
|
+
findings.push(makeFinding(rule, file, i + 1, redactEvidence(line.trim())));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return findings;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function makeFinding(rule, file, line, evidence) {
|
|
83
|
+
return {
|
|
84
|
+
id: rule.id,
|
|
85
|
+
title: rule.title,
|
|
86
|
+
severity: rule.severity,
|
|
87
|
+
category: rule.category,
|
|
88
|
+
file,
|
|
89
|
+
line,
|
|
90
|
+
evidence,
|
|
91
|
+
recommendation: rule.recommendation,
|
|
92
|
+
confidence: 'medium',
|
|
93
|
+
tags: rule.tags || []
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function redactEvidence(value) {
|
|
98
|
+
if (!value) return value;
|
|
99
|
+
let evidence = value;
|
|
100
|
+
evidence = evidence.replace(/(PRIVATE_KEY|MNEMONIC|SEED_PHRASE|API_KEY|TOKEN|SECRET)(\s*[:=]\s*)['\"]?[^\s'\"]+/ig, '$1$2[REDACTED]');
|
|
101
|
+
if (evidence.length > 220) evidence = `${evidence.slice(0, 217)}...`;
|
|
102
|
+
return evidence;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
async function readMythosIgnore(root) {
|
|
107
|
+
const ignorePath = path.join(root, '.mythosignore');
|
|
108
|
+
try {
|
|
109
|
+
const raw = await fs.readFile(ignorePath, 'utf8');
|
|
110
|
+
return raw
|
|
111
|
+
.split(/\r?\n/)
|
|
112
|
+
.map((line) => line.trim())
|
|
113
|
+
.filter((line) => line && !line.startsWith('#'));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error && error.code === 'ENOENT') return [];
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
}
|