huaweicloud-devkit 1.0.2-next.3 → 1.0.3-dev.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/.agents/plugins/marketplace.json +20 -20
- package/LICENSE +201 -201
- package/README.md +139 -163
- package/README.zh-CN.md +139 -161
- package/bin/setup.cjs +8 -8
- package/integrations/opencode/commands/huaweicloud-doctor.md +14 -14
- package/integrations/opencode/opencode.json +14 -14
- package/integrations/opencode/skills/huaweicloud-core/SKILL.md +32 -32
- package/package.json +44 -47
- package/plugins/huaweicloud-core/.claude-plugin/plugin.json +44 -44
- package/plugins/huaweicloud-core/.codex-plugin/plugin.json +43 -43
- package/plugins/huaweicloud-core/.cursor-plugin/plugin.json +44 -44
- package/plugins/huaweicloud-core/.mcp.json +15 -15
- package/plugins/huaweicloud-core/.workbuddy-plugin/plugin.json +44 -44
- package/plugins/huaweicloud-core/hooks/hooks.json +26 -26
- package/plugins/huaweicloud-core/hooks/huaweicloud-safety.py +136 -136
- package/plugins/huaweicloud-core/safety/policy.json +99 -99
- package/plugins/huaweicloud-core/safety/rules/cloud-risk-rules.json +199 -199
- package/plugins/huaweicloud-core/skills/huawei-apig/SKILL.md +164 -164
- package/plugins/huaweicloud-core/skills/huawei-billing/SKILL.md +48 -48
- package/plugins/huaweicloud-core/skills/huawei-cbr/SKILL.md +50 -50
- package/plugins/huaweicloud-core/skills/huawei-cce/SKILL.md +101 -101
- package/plugins/huaweicloud-core/skills/huawei-cloud-eye/SKILL.md +90 -90
- package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/SKILL.md +190 -190
- package/plugins/huaweicloud-core/skills/huawei-cloud-find-skills/scripts/search-skills.mjs +124 -124
- package/plugins/huaweicloud-core/skills/huawei-cts/SKILL.md +55 -55
- package/plugins/huaweicloud-core/skills/huawei-dds-dcs/SKILL.md +70 -70
- package/plugins/huaweicloud-core/skills/huawei-deployment/SKILL.md +56 -56
- package/plugins/huaweicloud-core/skills/huawei-dew/SKILL.md +97 -97
- package/plugins/huaweicloud-core/skills/huawei-dew/references/csms-usage.md +29 -29
- package/plugins/huaweicloud-core/skills/huawei-dew/references/kms-usage.md +23 -23
- package/plugins/huaweicloud-core/skills/huawei-ecs/SKILL.md +185 -185
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/create-instance.md +133 -133
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/evs.md +31 -31
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/flavors.md +55 -55
- package/plugins/huaweicloud-core/skills/huawei-ecs/references/troubleshooting.md +52 -52
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/SKILL.md +131 -131
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/apig-event-format.md +58 -58
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/create-function.md +71 -71
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/deploy-workflow.md +119 -119
- package/plugins/huaweicloud-core/skills/huawei-functiongraph/references/triggers.md +142 -142
- package/plugins/huaweicloud-core/skills/huawei-gaussdb/SKILL.md +56 -56
- package/plugins/huaweicloud-core/skills/huawei-getting-started/SKILL.md +65 -65
- package/plugins/huaweicloud-core/skills/huawei-iam/SKILL.md +111 -111
- package/plugins/huaweicloud-core/skills/huawei-iam/references/policy-examples.md +122 -122
- package/plugins/huaweicloud-core/skills/huawei-iam/references/quick-ref.md +56 -56
- package/plugins/huaweicloud-core/skills/huawei-modelarts/SKILL.md +65 -65
- package/plugins/huaweicloud-core/skills/huawei-obs/SKILL.md +127 -133
- package/plugins/huaweicloud-core/skills/huawei-obs/references/bucket-create.md +49 -49
- package/plugins/huaweicloud-core/skills/huawei-obs/references/bucket-lifecycle.md +26 -26
- package/plugins/huaweicloud-core/skills/huawei-obs/references/common-pitfalls.md +100 -100
- package/plugins/huaweicloud-core/skills/huawei-obs/references/single-file-share.md +40 -40
- package/plugins/huaweicloud-core/skills/huawei-obs/references/static-website.md +56 -56
- package/plugins/huaweicloud-core/skills/huawei-rds/SKILL.md +224 -224
- package/plugins/huaweicloud-core/skills/huawei-sandbox/SKILL.md +134 -139
- package/plugins/huaweicloud-core/skills/huawei-smn-dms/SKILL.md +69 -69
- package/plugins/huaweicloud-core/skills/huawei-vpc/SKILL.md +84 -84
- package/plugins/huaweicloud-core/skills/huawei-vpc/references/network.md +76 -76
- package/plugins/huaweicloud-core/skills/huawei-vpc/references/security-group.md +59 -59
- package/plugins/huaweicloud-core/skills/huawei-vpc/references/subnet.md +50 -50
- package/plugins/huaweicloud-core/skills/huawei-waf-aad/SKILL.md +63 -63
- package/plugins/huaweicloud-core/skills/huaweicloud-api-and-sdk/SKILL.md +56 -56
- package/plugins/huaweicloud-core/skills/huaweicloud-api-and-sdk/agents/openai.yaml +4 -4
- package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/SKILL.md +47 -51
- package/plugins/huaweicloud-core/skills/huaweicloud-capability-discovery/agents/openai.yaml +4 -4
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/SKILL.md +128 -128
- package/plugins/huaweicloud-core/skills/huaweicloud-cli-and-auth/agents/openai.yaml +4 -4
- package/plugins/huaweicloud-core/skills/huaweicloud-core/SKILL.md +95 -110
- package/plugins/huaweicloud-core/skills/huaweicloud-core/agents/openai.yaml +4 -4
- package/plugins/huaweicloud-core/skills/huaweicloud-core/references/report-issue.md +19 -19
- package/plugins/huaweicloud-core/skills/huaweicloud-core/references/select.md +27 -27
- package/plugins/huaweicloud-core/skills/huaweicloud-safety/SKILL.md +60 -60
- package/plugins/huaweicloud-core/skills/huaweicloud-safety/agents/openai.yaml +4 -4
- package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/SKILL.md +83 -83
- package/plugins/huaweicloud-core/skills/huaweicloud-troubleshooting/agents/openai.yaml +4 -4
- package/plugins/huaweicloud-core/src/auth/agent-registration.mjs +87 -105
- package/plugins/huaweicloud-core/src/auth/credentials.mjs +95 -95
- package/plugins/huaweicloud-core/src/auth/service.mjs +63 -63
- package/plugins/huaweicloud-core/src/hcloud-cli.mjs +265 -265
- package/plugins/huaweicloud-core/src/mcp-server.mjs +129 -129
- package/plugins/huaweicloud-core/src/risk-rule-engine.mjs +137 -137
- package/plugins/huaweicloud-core/src/safety-policy.mjs +302 -302
- package/plugins/huaweicloud-core/src/sandbox/hdkitservice-api.mjs +87 -91
- package/plugins/huaweicloud-core/src/sandbox/hwlink-api.mjs +153 -162
- package/plugins/huaweicloud-core/src/sandbox/session-manager.mjs +105 -112
- package/plugins/huaweicloud-core/src/search-market.mjs +169 -174
- package/plugins/huaweicloud-core/src/setup-cli.mjs +1594 -2017
- package/plugins/huaweicloud-core/src/tools.mjs +993 -1008
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-exec-client.js +427 -427
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-fair-queue.js +132 -132
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-multiplexer.js +227 -227
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-packet.js +202 -202
- package/plugins/huaweicloud-core/src/ws-exec/hwlink-terminal-channel.js +158 -158
- package/plugins/huaweicloud-core/src/ws-exec/index.js +19 -19
- package/plugins/huaweicloud-core/src/ws-exec/ws-exec-client.js +338 -338
- package/plugins/huaweicloud-core/src/proxy/proxy-agent.mjs +0 -155
- package/plugins/huaweicloud-core/src/proxy/proxy-config.mjs +0 -87
|
@@ -1,129 +1,129 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { stdin, stdout } from 'node:process';
|
|
3
|
-
import { rmSync, existsSync } from 'node:fs';
|
|
4
|
-
import { resolve, dirname } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { TOOL_DEFINITIONS, callTool } from './tools.mjs';
|
|
7
|
-
|
|
8
|
-
// The MCP server is now loaded by a live agent session. Clear the install marker
|
|
9
|
-
// in this plugin dir so `doctor` no longer reports "restart needed".
|
|
10
|
-
try {
|
|
11
|
-
const pluginDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
-
const marker = resolve(pluginDir, '.installed');
|
|
13
|
-
if (existsSync(marker)) rmSync(marker, { force: true });
|
|
14
|
-
} catch {}
|
|
15
|
-
|
|
16
|
-
let buffer = Buffer.alloc(0);
|
|
17
|
-
let useContentLengthFraming = true;
|
|
18
|
-
|
|
19
|
-
stdin.on('data', (chunk) => {
|
|
20
|
-
buffer = Buffer.concat([buffer, chunk]);
|
|
21
|
-
readFrames();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
function readFrames() {
|
|
25
|
-
while (true) {
|
|
26
|
-
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
27
|
-
if (headerEnd !== -1) {
|
|
28
|
-
useContentLengthFraming = true;
|
|
29
|
-
const consumed = parseContentLengthFrame(headerEnd);
|
|
30
|
-
if (!consumed) return;
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const lf = buffer.indexOf('\n');
|
|
35
|
-
if (lf !== -1) {
|
|
36
|
-
useContentLengthFraming = false;
|
|
37
|
-
const line = buffer.subarray(0, lf).toString('utf8').trim();
|
|
38
|
-
buffer = buffer.subarray(lf + 1);
|
|
39
|
-
if (line) void handleMessage(JSON.parse(line));
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function parseContentLengthFrame(headerEnd) {
|
|
48
|
-
const header = buffer.subarray(0, headerEnd).toString('utf8');
|
|
49
|
-
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
50
|
-
if (!match) {
|
|
51
|
-
buffer = Buffer.alloc(0);
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
const length = Number(match[1]);
|
|
55
|
-
const bodyStart = headerEnd + 4;
|
|
56
|
-
const bodyEnd = bodyStart + length;
|
|
57
|
-
if (buffer.length < bodyEnd) return false;
|
|
58
|
-
const body = buffer.subarray(bodyStart, bodyEnd).toString('utf8');
|
|
59
|
-
buffer = buffer.subarray(bodyEnd);
|
|
60
|
-
void handleMessage(JSON.parse(body));
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function handleMessage(message) {
|
|
65
|
-
if (!Object.hasOwn(message, 'id')) {
|
|
66
|
-
if (message.method === 'notifications/initialized') return;
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
try {
|
|
70
|
-
const result = await dispatch(message.method, message.params || {});
|
|
71
|
-
writeMessage({ jsonrpc: '2.0', id: message.id, result });
|
|
72
|
-
} catch (error) {
|
|
73
|
-
writeMessage({
|
|
74
|
-
jsonrpc: '2.0',
|
|
75
|
-
id: message.id,
|
|
76
|
-
error: {
|
|
77
|
-
code: -32603,
|
|
78
|
-
message: error.message,
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async function dispatch(method, params) {
|
|
85
|
-
if (method === 'initialize') {
|
|
86
|
-
return {
|
|
87
|
-
protocolVersion: params.protocolVersion || '2024-11-05',
|
|
88
|
-
capabilities: {
|
|
89
|
-
tools: {},
|
|
90
|
-
},
|
|
91
|
-
serverInfo: {
|
|
92
|
-
name: 'huaweicloud-devkit',
|
|
93
|
-
version: '0.1.0',
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (method === 'tools/list') {
|
|
99
|
-
return { tools: TOOL_DEFINITIONS };
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (method === 'tools/call') {
|
|
103
|
-
const result = await callTool(params.name, params.arguments || {});
|
|
104
|
-
return {
|
|
105
|
-
content: [
|
|
106
|
-
{
|
|
107
|
-
type: 'text',
|
|
108
|
-
text: JSON.stringify(result, null, 2),
|
|
109
|
-
},
|
|
110
|
-
],
|
|
111
|
-
isError: false,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (method === 'resources/list') {
|
|
116
|
-
return { resources: [] };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
throw new Error(`Unsupported method: ${method}`);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function writeMessage(message) {
|
|
123
|
-
const json = JSON.stringify(message);
|
|
124
|
-
if (useContentLengthFraming) {
|
|
125
|
-
stdout.write(`Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`);
|
|
126
|
-
} else {
|
|
127
|
-
stdout.write(json + '\n');
|
|
128
|
-
}
|
|
129
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { stdin, stdout } from 'node:process';
|
|
3
|
+
import { rmSync, existsSync } from 'node:fs';
|
|
4
|
+
import { resolve, dirname } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { TOOL_DEFINITIONS, callTool } from './tools.mjs';
|
|
7
|
+
|
|
8
|
+
// The MCP server is now loaded by a live agent session. Clear the install marker
|
|
9
|
+
// in this plugin dir so `doctor` no longer reports "restart needed".
|
|
10
|
+
try {
|
|
11
|
+
const pluginDir = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
12
|
+
const marker = resolve(pluginDir, '.installed');
|
|
13
|
+
if (existsSync(marker)) rmSync(marker, { force: true });
|
|
14
|
+
} catch {}
|
|
15
|
+
|
|
16
|
+
let buffer = Buffer.alloc(0);
|
|
17
|
+
let useContentLengthFraming = true;
|
|
18
|
+
|
|
19
|
+
stdin.on('data', (chunk) => {
|
|
20
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
21
|
+
readFrames();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
function readFrames() {
|
|
25
|
+
while (true) {
|
|
26
|
+
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
27
|
+
if (headerEnd !== -1) {
|
|
28
|
+
useContentLengthFraming = true;
|
|
29
|
+
const consumed = parseContentLengthFrame(headerEnd);
|
|
30
|
+
if (!consumed) return;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const lf = buffer.indexOf('\n');
|
|
35
|
+
if (lf !== -1) {
|
|
36
|
+
useContentLengthFraming = false;
|
|
37
|
+
const line = buffer.subarray(0, lf).toString('utf8').trim();
|
|
38
|
+
buffer = buffer.subarray(lf + 1);
|
|
39
|
+
if (line) void handleMessage(JSON.parse(line));
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseContentLengthFrame(headerEnd) {
|
|
48
|
+
const header = buffer.subarray(0, headerEnd).toString('utf8');
|
|
49
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
50
|
+
if (!match) {
|
|
51
|
+
buffer = Buffer.alloc(0);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
const length = Number(match[1]);
|
|
55
|
+
const bodyStart = headerEnd + 4;
|
|
56
|
+
const bodyEnd = bodyStart + length;
|
|
57
|
+
if (buffer.length < bodyEnd) return false;
|
|
58
|
+
const body = buffer.subarray(bodyStart, bodyEnd).toString('utf8');
|
|
59
|
+
buffer = buffer.subarray(bodyEnd);
|
|
60
|
+
void handleMessage(JSON.parse(body));
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function handleMessage(message) {
|
|
65
|
+
if (!Object.hasOwn(message, 'id')) {
|
|
66
|
+
if (message.method === 'notifications/initialized') return;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const result = await dispatch(message.method, message.params || {});
|
|
71
|
+
writeMessage({ jsonrpc: '2.0', id: message.id, result });
|
|
72
|
+
} catch (error) {
|
|
73
|
+
writeMessage({
|
|
74
|
+
jsonrpc: '2.0',
|
|
75
|
+
id: message.id,
|
|
76
|
+
error: {
|
|
77
|
+
code: -32603,
|
|
78
|
+
message: error.message,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function dispatch(method, params) {
|
|
85
|
+
if (method === 'initialize') {
|
|
86
|
+
return {
|
|
87
|
+
protocolVersion: params.protocolVersion || '2024-11-05',
|
|
88
|
+
capabilities: {
|
|
89
|
+
tools: {},
|
|
90
|
+
},
|
|
91
|
+
serverInfo: {
|
|
92
|
+
name: 'huaweicloud-devkit',
|
|
93
|
+
version: '0.1.0',
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (method === 'tools/list') {
|
|
99
|
+
return { tools: TOOL_DEFINITIONS };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (method === 'tools/call') {
|
|
103
|
+
const result = await callTool(params.name, params.arguments || {});
|
|
104
|
+
return {
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: 'text',
|
|
108
|
+
text: JSON.stringify(result, null, 2),
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
isError: false,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (method === 'resources/list') {
|
|
116
|
+
return { resources: [] };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throw new Error(`Unsupported method: ${method}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function writeMessage(message) {
|
|
123
|
+
const json = JSON.stringify(message);
|
|
124
|
+
if (useContentLengthFraming) {
|
|
125
|
+
stdout.write(`Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`);
|
|
126
|
+
} else {
|
|
127
|
+
stdout.write(json + '\n');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -1,137 +1,137 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const defaultRulesPath = join(__dirname, '..', 'safety', 'rules', 'cloud-risk-rules.json');
|
|
7
|
-
|
|
8
|
-
const SEVERITY_RANK = {
|
|
9
|
-
deny: 3,
|
|
10
|
-
warn: 2,
|
|
11
|
-
info: 1,
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export function loadRiskRules(options = {}) {
|
|
15
|
-
const path = options.path || defaultRulesPath;
|
|
16
|
-
return JSON.parse(readFileSync(path, 'utf8'));
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function redactEvidence(text) {
|
|
20
|
-
return String(text)
|
|
21
|
-
.replace(/((?:access[_-]?key|secret[_-]?key|security[_-]?token|x[_-]?auth[_-]?token|authorization|password|passwd|adminPass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi, '$1<redacted>')
|
|
22
|
-
.replace(/(AK|SK)\s*[:=]\s*("[^"]*"|'[^']*'|[^\s,;]+)/g, '$1=<redacted>');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function normalizeText(value) {
|
|
26
|
-
if (typeof value === 'string') return value;
|
|
27
|
-
return JSON.stringify(value, null, 2);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function evaluationContext(stage, input) {
|
|
31
|
-
if (stage === 'command') {
|
|
32
|
-
const command = normalizeText(input.command || input.text || '');
|
|
33
|
-
return { text: command, command };
|
|
34
|
-
}
|
|
35
|
-
if (stage === 'artifact') {
|
|
36
|
-
const path = String(input.path || '');
|
|
37
|
-
const content = normalizeText(input.content || '');
|
|
38
|
-
return { text: `${path}\n${content}`, path, content };
|
|
39
|
-
}
|
|
40
|
-
if (stage === 'deploy_plan') {
|
|
41
|
-
const plan = normalizeText(input.plan || input.text || input);
|
|
42
|
-
return { text: plan, plan };
|
|
43
|
-
}
|
|
44
|
-
return { text: normalizeText(input) };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function conditionMatches(condition, context) {
|
|
48
|
-
const field = condition.field || 'text';
|
|
49
|
-
const value = Object.hasOwn(context, field) ? context[field] : context.text;
|
|
50
|
-
return new RegExp(condition.regex, 'ims').test(String(value || ''));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function ruleMatches(rule, context) {
|
|
54
|
-
const match = rule.match || {};
|
|
55
|
-
const all = match.all;
|
|
56
|
-
const any = match.any;
|
|
57
|
-
const none = match.none;
|
|
58
|
-
if (Array.isArray(all) && !all.every((condition) => conditionMatches(condition, context))) {
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
if (Array.isArray(any) && !any.some((condition) => conditionMatches(condition, context))) {
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
if (Array.isArray(none) && none.some((condition) => conditionMatches(condition, context))) {
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
67
|
-
return Array.isArray(all) || Array.isArray(any);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function excerpt(text) {
|
|
71
|
-
const compact = redactEvidence(String(text).replace(/\s+/g, ' ').trim());
|
|
72
|
-
if (compact.length <= 240) return compact;
|
|
73
|
-
return `${compact.slice(0, 237)}...`;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function evaluate(stage, inputs, options = {}) {
|
|
77
|
-
const catalog = options.catalog || loadRiskRules(options);
|
|
78
|
-
const items = Array.isArray(inputs) ? inputs : [inputs];
|
|
79
|
-
const findings = [];
|
|
80
|
-
|
|
81
|
-
for (const input of items) {
|
|
82
|
-
const context = evaluationContext(stage, input || {});
|
|
83
|
-
for (const rule of catalog.rules) {
|
|
84
|
-
if (!rule.stages.includes(stage)) continue;
|
|
85
|
-
if (!ruleMatches(rule, context)) continue;
|
|
86
|
-
findings.push({
|
|
87
|
-
ruleId: rule.id,
|
|
88
|
-
title: rule.title,
|
|
89
|
-
category: rule.category,
|
|
90
|
-
severity: rule.severity,
|
|
91
|
-
message: rule.message,
|
|
92
|
-
remediation: rule.remediation,
|
|
93
|
-
source: input?.path || stage,
|
|
94
|
-
evidence: excerpt(context.text),
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
findings.sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
|
|
100
|
-
const hasDeny = findings.some((finding) => finding.severity === 'deny');
|
|
101
|
-
const hasWarn = findings.some((finding) => finding.severity === 'warn');
|
|
102
|
-
return {
|
|
103
|
-
decision: hasDeny ? 'deny' : hasWarn ? 'warn' : 'allow',
|
|
104
|
-
findings,
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function evaluateCommandRisk(command, options = {}) {
|
|
109
|
-
return evaluate('command', { command }, options);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function evaluateArtifacts(artifacts, options = {}) {
|
|
113
|
-
return evaluate('artifact', Array.isArray(artifacts) ? artifacts : [], options);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function evaluateDeployPlan(plan, options = {}) {
|
|
117
|
-
return evaluate('deploy_plan', { plan }, options);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function mergeRiskDecision(base, risk) {
|
|
121
|
-
if (!risk || !risk.findings?.length) return base;
|
|
122
|
-
if (risk.decision === 'deny') {
|
|
123
|
-
const topFinding = risk.findings[0];
|
|
124
|
-
return {
|
|
125
|
-
...base,
|
|
126
|
-
decision: 'deny',
|
|
127
|
-
risk: topFinding.category,
|
|
128
|
-
reason: topFinding.message,
|
|
129
|
-
blockedByRiskRule: true,
|
|
130
|
-
findings: risk.findings,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
return {
|
|
134
|
-
...base,
|
|
135
|
-
warnings: [...(base.warnings || []), ...risk.findings],
|
|
136
|
-
};
|
|
137
|
-
}
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const defaultRulesPath = join(__dirname, '..', 'safety', 'rules', 'cloud-risk-rules.json');
|
|
7
|
+
|
|
8
|
+
const SEVERITY_RANK = {
|
|
9
|
+
deny: 3,
|
|
10
|
+
warn: 2,
|
|
11
|
+
info: 1,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function loadRiskRules(options = {}) {
|
|
15
|
+
const path = options.path || defaultRulesPath;
|
|
16
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function redactEvidence(text) {
|
|
20
|
+
return String(text)
|
|
21
|
+
.replace(/((?:access[_-]?key|secret[_-]?key|security[_-]?token|x[_-]?auth[_-]?token|authorization|password|passwd|adminPass|credential)\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi, '$1<redacted>')
|
|
22
|
+
.replace(/(AK|SK)\s*[:=]\s*("[^"]*"|'[^']*'|[^\s,;]+)/g, '$1=<redacted>');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeText(value) {
|
|
26
|
+
if (typeof value === 'string') return value;
|
|
27
|
+
return JSON.stringify(value, null, 2);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function evaluationContext(stage, input) {
|
|
31
|
+
if (stage === 'command') {
|
|
32
|
+
const command = normalizeText(input.command || input.text || '');
|
|
33
|
+
return { text: command, command };
|
|
34
|
+
}
|
|
35
|
+
if (stage === 'artifact') {
|
|
36
|
+
const path = String(input.path || '');
|
|
37
|
+
const content = normalizeText(input.content || '');
|
|
38
|
+
return { text: `${path}\n${content}`, path, content };
|
|
39
|
+
}
|
|
40
|
+
if (stage === 'deploy_plan') {
|
|
41
|
+
const plan = normalizeText(input.plan || input.text || input);
|
|
42
|
+
return { text: plan, plan };
|
|
43
|
+
}
|
|
44
|
+
return { text: normalizeText(input) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function conditionMatches(condition, context) {
|
|
48
|
+
const field = condition.field || 'text';
|
|
49
|
+
const value = Object.hasOwn(context, field) ? context[field] : context.text;
|
|
50
|
+
return new RegExp(condition.regex, 'ims').test(String(value || ''));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ruleMatches(rule, context) {
|
|
54
|
+
const match = rule.match || {};
|
|
55
|
+
const all = match.all;
|
|
56
|
+
const any = match.any;
|
|
57
|
+
const none = match.none;
|
|
58
|
+
if (Array.isArray(all) && !all.every((condition) => conditionMatches(condition, context))) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(any) && !any.some((condition) => conditionMatches(condition, context))) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(none) && none.some((condition) => conditionMatches(condition, context))) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return Array.isArray(all) || Array.isArray(any);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function excerpt(text) {
|
|
71
|
+
const compact = redactEvidence(String(text).replace(/\s+/g, ' ').trim());
|
|
72
|
+
if (compact.length <= 240) return compact;
|
|
73
|
+
return `${compact.slice(0, 237)}...`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function evaluate(stage, inputs, options = {}) {
|
|
77
|
+
const catalog = options.catalog || loadRiskRules(options);
|
|
78
|
+
const items = Array.isArray(inputs) ? inputs : [inputs];
|
|
79
|
+
const findings = [];
|
|
80
|
+
|
|
81
|
+
for (const input of items) {
|
|
82
|
+
const context = evaluationContext(stage, input || {});
|
|
83
|
+
for (const rule of catalog.rules) {
|
|
84
|
+
if (!rule.stages.includes(stage)) continue;
|
|
85
|
+
if (!ruleMatches(rule, context)) continue;
|
|
86
|
+
findings.push({
|
|
87
|
+
ruleId: rule.id,
|
|
88
|
+
title: rule.title,
|
|
89
|
+
category: rule.category,
|
|
90
|
+
severity: rule.severity,
|
|
91
|
+
message: rule.message,
|
|
92
|
+
remediation: rule.remediation,
|
|
93
|
+
source: input?.path || stage,
|
|
94
|
+
evidence: excerpt(context.text),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
findings.sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
|
|
100
|
+
const hasDeny = findings.some((finding) => finding.severity === 'deny');
|
|
101
|
+
const hasWarn = findings.some((finding) => finding.severity === 'warn');
|
|
102
|
+
return {
|
|
103
|
+
decision: hasDeny ? 'deny' : hasWarn ? 'warn' : 'allow',
|
|
104
|
+
findings,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function evaluateCommandRisk(command, options = {}) {
|
|
109
|
+
return evaluate('command', { command }, options);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function evaluateArtifacts(artifacts, options = {}) {
|
|
113
|
+
return evaluate('artifact', Array.isArray(artifacts) ? artifacts : [], options);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function evaluateDeployPlan(plan, options = {}) {
|
|
117
|
+
return evaluate('deploy_plan', { plan }, options);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function mergeRiskDecision(base, risk) {
|
|
121
|
+
if (!risk || !risk.findings?.length) return base;
|
|
122
|
+
if (risk.decision === 'deny') {
|
|
123
|
+
const topFinding = risk.findings[0];
|
|
124
|
+
return {
|
|
125
|
+
...base,
|
|
126
|
+
decision: 'deny',
|
|
127
|
+
risk: topFinding.category,
|
|
128
|
+
reason: topFinding.message,
|
|
129
|
+
blockedByRiskRule: true,
|
|
130
|
+
findings: risk.findings,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
...base,
|
|
135
|
+
warnings: [...(base.warnings || []), ...risk.findings],
|
|
136
|
+
};
|
|
137
|
+
}
|