@linkup-ai/abap-ai 0.1.1 → 0.2.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/abap/security-agent/README.md +30 -0
- package/dist/abap/security-agent/zcl_lkp_ai_guard.clas.abap +282 -0
- package/dist/abap/security-agent/zcl_lkp_ai_icf_handler.clas.abap +275 -0
- package/dist/abap/security-agent/zcx_lkp_ai_denied.clas.abap +50 -0
- package/dist/abap/security-agent/zlkp_ai_audit.tabl.asdic +19 -0
- package/dist/abap/security-agent/zlkp_ai_config.tabl.asdic +21 -0
- package/dist/abap/security-agent/zlkp_ai_setup.prog.abap +359 -0
- package/dist/adt-client.js +63 -74
- package/dist/cli/init.js +40 -6
- package/dist/cli/install-agent.js +290 -0
- package/dist/cli.js +20 -2
- package/dist/index.js +59 -31
- package/dist/license-guard.js +43 -1
- package/dist/sap-policy-loader.js +255 -0
- package/dist/security-policy.js +41 -12
- package/dist/tests/create-payload.test.js +213 -0
- package/dist/tests/security-policy.test.js +167 -0
- package/dist/tool-handler.js +80 -0
- package/dist/tools/activate.js +27 -4
- package/dist/tools/create.js +2 -0
- package/dist/tools/delete.js +12 -8
- package/dist/tools/execute.js +112 -0
- package/dist/tools/transports.js +4 -3
- package/package.json +7 -4
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Instalação do ABAP Security Agent no sistema SAP.
|
|
4
|
+
*
|
|
5
|
+
* Cria os objetos ABAP (tabelas, classes) via ADT REST API.
|
|
6
|
+
* Executado após o init, quando o sistema é Enterprise/Corporate.
|
|
7
|
+
*
|
|
8
|
+
* Fluxo:
|
|
9
|
+
* 1. Verifica se os objetos já existem (SELECT COUNT(*) em ZLKP_AI_CONFIG)
|
|
10
|
+
* 2. Se não existem, oferece instalar
|
|
11
|
+
* 3. Cria tabelas, exception, guard class
|
|
12
|
+
* 4. Carrega policy padrão para o environment role
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.checkAgentInstalled = checkAgentInstalled;
|
|
49
|
+
exports.installAgent = installAgent;
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const https = __importStar(require("https"));
|
|
53
|
+
const http = __importStar(require("http"));
|
|
54
|
+
/**
|
|
55
|
+
* Verifica se o Security Agent já está instalado.
|
|
56
|
+
* Tenta ler a tabela ZLKP_AI_CONFIG via SQL Console.
|
|
57
|
+
*/
|
|
58
|
+
async function checkAgentInstalled(opts) {
|
|
59
|
+
try {
|
|
60
|
+
const result = await adtRequest(opts, "POST", "/datapreview/freestyle", "SELECT COUNT(*) AS CNT FROM ZLKP_AI_CONFIG", { rowNumber: "1" }, "application/vnd.sap.adt.datapreview.table.v1+xml");
|
|
61
|
+
return result.status >= 200 && result.status < 300;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Instala o Security Agent criando os objetos ABAP via ADT.
|
|
69
|
+
*
|
|
70
|
+
* Ordem:
|
|
71
|
+
* 1. ZCX_LKP_AI_DENIED (exception class — sem dependências)
|
|
72
|
+
* 2. ZLKP_AI_CONFIG (tabela config)
|
|
73
|
+
* 3. ZLKP_AI_AUDIT (tabela audit)
|
|
74
|
+
* 4. ZCL_LKP_AI_GUARD (classe — depende das tabelas + exception)
|
|
75
|
+
*
|
|
76
|
+
* Os fontes ABAP estão em src/abap/security-agent/*.
|
|
77
|
+
*/
|
|
78
|
+
async function installAgent(opts) {
|
|
79
|
+
const created = [];
|
|
80
|
+
const errors = [];
|
|
81
|
+
// Obter CSRF token
|
|
82
|
+
const csrf = await fetchCsrf(opts);
|
|
83
|
+
if (!csrf) {
|
|
84
|
+
return { success: false, message: "Falha ao obter CSRF token do SAP.", objects: [] };
|
|
85
|
+
}
|
|
86
|
+
// 1. Criar exception class ZCX_LKP_AI_DENIED
|
|
87
|
+
const exSource = readAbapSource("zcx_lkp_ai_denied.clas.abap");
|
|
88
|
+
const exResult = await createObject(opts, csrf, "CLAS/OC", "ZCX_LKP_AI_DENIED", "LKPABAP.ia: Exception - tool denied", opts.transportRequest);
|
|
89
|
+
if (exResult.ok) {
|
|
90
|
+
await writeSource(opts, csrf, "/oo/classes/ZCX_LKP_AI_DENIED/source/main", exSource);
|
|
91
|
+
await activateObject(opts, csrf, "CLAS/OC", "ZCX_LKP_AI_DENIED");
|
|
92
|
+
created.push("ZCX_LKP_AI_DENIED");
|
|
93
|
+
}
|
|
94
|
+
else if (exResult.exists) {
|
|
95
|
+
created.push("ZCX_LKP_AI_DENIED (já existe)");
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
errors.push(`ZCX_LKP_AI_DENIED: ${exResult.error}`);
|
|
99
|
+
}
|
|
100
|
+
// 2. Criar tabela ZLKP_AI_CONFIG
|
|
101
|
+
// Nota: tabelas transparentes precisam ser criadas via DDIC, não via ADT create+write.
|
|
102
|
+
// O ADT create para TABL/DT cria a estrutura, e o write define os campos.
|
|
103
|
+
const cfgResult = await createObject(opts, csrf, "TABL/DT", "ZLKP_AI_CONFIG", "LKPABAP.ia: Configuração por ambiente", opts.transportRequest);
|
|
104
|
+
if (cfgResult.ok) {
|
|
105
|
+
const cfgSource = readAbapSource("zlkp_ai_config.tabl.asdic");
|
|
106
|
+
await writeSource(opts, csrf, "/ddic/tables/ZLKP_AI_CONFIG/source/main", cfgSource);
|
|
107
|
+
await activateObject(opts, csrf, "TABL/DT", "ZLKP_AI_CONFIG");
|
|
108
|
+
created.push("ZLKP_AI_CONFIG");
|
|
109
|
+
}
|
|
110
|
+
else if (cfgResult.exists) {
|
|
111
|
+
created.push("ZLKP_AI_CONFIG (já existe)");
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
errors.push(`ZLKP_AI_CONFIG: ${cfgResult.error}`);
|
|
115
|
+
}
|
|
116
|
+
// 3. Criar tabela ZLKP_AI_AUDIT
|
|
117
|
+
const audResult = await createObject(opts, csrf, "TABL/DT", "ZLKP_AI_AUDIT", "LKPABAP.ia: Audit Log de Operações", opts.transportRequest);
|
|
118
|
+
if (audResult.ok) {
|
|
119
|
+
const audSource = readAbapSource("zlkp_ai_audit.tabl.asdic");
|
|
120
|
+
await writeSource(opts, csrf, "/ddic/tables/ZLKP_AI_AUDIT/source/main", audSource);
|
|
121
|
+
await activateObject(opts, csrf, "TABL/DT", "ZLKP_AI_AUDIT");
|
|
122
|
+
created.push("ZLKP_AI_AUDIT");
|
|
123
|
+
}
|
|
124
|
+
else if (audResult.exists) {
|
|
125
|
+
created.push("ZLKP_AI_AUDIT (já existe)");
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
errors.push(`ZLKP_AI_AUDIT: ${audResult.error}`);
|
|
129
|
+
}
|
|
130
|
+
// 4. Criar guard class ZCL_LKP_AI_GUARD
|
|
131
|
+
const guardSource = readAbapSource("zcl_lkp_ai_guard.clas.abap");
|
|
132
|
+
const guardResult = await createObject(opts, csrf, "CLAS/OC", "ZCL_LKP_AI_GUARD", "LKPABAP.ia: Security Guard", opts.transportRequest);
|
|
133
|
+
if (guardResult.ok) {
|
|
134
|
+
await writeSource(opts, csrf, "/oo/classes/ZCL_LKP_AI_GUARD/source/main", guardSource);
|
|
135
|
+
await activateObject(opts, csrf, "CLAS/OC", "ZCL_LKP_AI_GUARD");
|
|
136
|
+
created.push("ZCL_LKP_AI_GUARD");
|
|
137
|
+
}
|
|
138
|
+
else if (guardResult.exists) {
|
|
139
|
+
created.push("ZCL_LKP_AI_GUARD (já existe)");
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
errors.push(`ZCL_LKP_AI_GUARD: ${guardResult.error}`);
|
|
143
|
+
}
|
|
144
|
+
if (errors.length > 0) {
|
|
145
|
+
return {
|
|
146
|
+
success: false,
|
|
147
|
+
message: `Instalação parcial. Erros:\n${errors.map((e) => ` - ${e}`).join("\n")}`,
|
|
148
|
+
objects: created,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
success: true,
|
|
153
|
+
message: `Security Agent instalado com sucesso. ${created.length} objetos criados/verificados.`,
|
|
154
|
+
objects: created,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
// ─── ADT helpers (standalone, sem depender de adt-client.ts) ─────
|
|
158
|
+
function readAbapSource(filename) {
|
|
159
|
+
// Fontes estão em src/abap/security-agent/ (dev) ou dist/abap/security-agent/ (runtime)
|
|
160
|
+
const candidates = [
|
|
161
|
+
path.resolve(__dirname, "..", "abap", "security-agent", filename), // dist/cli/../abap/ = dist/abap/
|
|
162
|
+
path.resolve(__dirname, "abap", "security-agent", filename), // fallback
|
|
163
|
+
path.resolve(__dirname, "..", "..", "src", "abap", "security-agent", filename), // dev mode
|
|
164
|
+
];
|
|
165
|
+
for (const p of candidates) {
|
|
166
|
+
try {
|
|
167
|
+
return fs.readFileSync(p, "utf-8");
|
|
168
|
+
}
|
|
169
|
+
catch { /* try next */ }
|
|
170
|
+
}
|
|
171
|
+
throw new Error(`Fonte ABAP não encontrado: ${filename}`);
|
|
172
|
+
}
|
|
173
|
+
function getAuth(opts) {
|
|
174
|
+
return "Basic " + Buffer.from(`${opts.user}:${opts.pass}`).toString("base64");
|
|
175
|
+
}
|
|
176
|
+
/** Session cookies capturados no fetchCsrf — necessários em todas as chamadas subsequentes */
|
|
177
|
+
let sessionCookies = "";
|
|
178
|
+
async function fetchCsrf(opts) {
|
|
179
|
+
try {
|
|
180
|
+
const result = await adtRequest(opts, "GET", "/discovery", "", {}, "application/atomsvc+xml", { "X-CSRF-Token": "Fetch" });
|
|
181
|
+
// Capturar cookies da sessão (sap-contextid, SAP_SESSIONID)
|
|
182
|
+
const raw = result.headers["set-cookie"];
|
|
183
|
+
if (raw) {
|
|
184
|
+
const cookies = Array.isArray(raw) ? raw : [raw];
|
|
185
|
+
sessionCookies = cookies.map((c) => c.split(";")[0]).join("; ");
|
|
186
|
+
}
|
|
187
|
+
return result.headers["x-csrf-token"] || null;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function createObject(opts, csrf, objType, objName, description, transport) {
|
|
194
|
+
const TYPE_TO_PATH = {
|
|
195
|
+
"CLAS/OC": "oo/classes",
|
|
196
|
+
"TABL/DT": "ddic/tables",
|
|
197
|
+
};
|
|
198
|
+
const adtPath = TYPE_TO_PATH[objType];
|
|
199
|
+
if (!adtPath)
|
|
200
|
+
return { ok: false, exists: false, error: `Tipo ${objType} não suportado` };
|
|
201
|
+
const CONTENT_TYPES = {
|
|
202
|
+
"CLAS/OC": "application/vnd.sap.adt.oo.classes.v2+xml",
|
|
203
|
+
"TABL/DT": "application/vnd.sap.adt.tables.v2+xml",
|
|
204
|
+
};
|
|
205
|
+
const NS = {
|
|
206
|
+
"CLAS/OC": `<class:abapClass xmlns:class="http://www.sap.com/adt/oo/classes" xmlns:adtcore="http://www.sap.com/adt/core" adtcore:description="${description}" adtcore:language="${opts.language}" adtcore:name="${objName}" adtcore:responsible="${opts.user}"><adtcore:packageRef adtcore:name="$TMP"/></class:abapClass>`,
|
|
207
|
+
"TABL/DT": `<blue:blueSource xmlns:blue="http://www.sap.com/wbobj/blue" xmlns:adtcore="http://www.sap.com/adt/core" adtcore:description="${description}" adtcore:language="${opts.language}" adtcore:name="${objName}" adtcore:responsible="${opts.user}"><adtcore:packageRef adtcore:name="$TMP"/></blue:blueSource>`,
|
|
208
|
+
};
|
|
209
|
+
const payload = `<?xml version="1.0" encoding="UTF-8"?>\n${NS[objType]}`;
|
|
210
|
+
const params = {};
|
|
211
|
+
if (transport)
|
|
212
|
+
params.corrNr = transport;
|
|
213
|
+
try {
|
|
214
|
+
const result = await adtRequest(opts, "POST", `/${adtPath}`, payload, params, CONTENT_TYPES[objType], { "X-CSRF-Token": csrf, "Content-Type": CONTENT_TYPES[objType] });
|
|
215
|
+
if (result.status === 200 || result.status === 201) {
|
|
216
|
+
return { ok: true, exists: false };
|
|
217
|
+
}
|
|
218
|
+
if (result.status === 409 || result.body.includes("already exists") || result.body.includes("já existe")) {
|
|
219
|
+
return { ok: false, exists: true };
|
|
220
|
+
}
|
|
221
|
+
return { ok: false, exists: false, error: `HTTP ${result.status}: ${result.body.substring(0, 200)}` };
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
return { ok: false, exists: false, error: e.message };
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async function writeSource(opts, csrf, adtPath, source) {
|
|
228
|
+
// GET etag primeiro
|
|
229
|
+
const getResult = await adtRequest(opts, "GET", adtPath, "", {}, "text/plain");
|
|
230
|
+
const etag = getResult.headers["etag"] || "";
|
|
231
|
+
await adtRequest(opts, "PUT", adtPath, source, {}, "text/plain", {
|
|
232
|
+
"X-CSRF-Token": csrf,
|
|
233
|
+
"Content-Type": "text/plain",
|
|
234
|
+
"If-Match": etag,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
async function activateObject(opts, csrf, objType, objName) {
|
|
238
|
+
const TYPE_TO_PATH = { "CLAS/OC": "oo/classes", "TABL/DT": "ddic/tables" };
|
|
239
|
+
const adtPath = TYPE_TO_PATH[objType];
|
|
240
|
+
const adtUri = `/sap/bc/adt/${adtPath}/${objName}`;
|
|
241
|
+
const payload = `<?xml version="1.0" encoding="UTF-8"?><adtcore:objectReferences xmlns:adtcore="http://www.sap.com/adt/core"><adtcore:objectReference adtcore:uri="${adtUri}" adtcore:name="${objName}"/></adtcore:objectReferences>`;
|
|
242
|
+
await adtRequest(opts, "POST", "/activation", payload, {}, "application/xml", {
|
|
243
|
+
"X-CSRF-Token": csrf,
|
|
244
|
+
"Content-Type": "application/xml",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
function adtRequest(opts, method, adtPath, body, params, accept, extraHeaders) {
|
|
248
|
+
const baseUrl = new URL(`${opts.url}/sap/bc/adt${adtPath}`);
|
|
249
|
+
baseUrl.searchParams.set("sap-client", opts.client);
|
|
250
|
+
baseUrl.searchParams.set("sap-language", opts.language);
|
|
251
|
+
for (const [k, v] of Object.entries(params)) {
|
|
252
|
+
baseUrl.searchParams.set(k, v);
|
|
253
|
+
}
|
|
254
|
+
const headers = {
|
|
255
|
+
Authorization: getAuth(opts),
|
|
256
|
+
Accept: accept,
|
|
257
|
+
...(sessionCookies ? { Cookie: sessionCookies } : {}),
|
|
258
|
+
...extraHeaders,
|
|
259
|
+
};
|
|
260
|
+
const transport = baseUrl.protocol === "https:" ? https : http;
|
|
261
|
+
const requestOpts = {
|
|
262
|
+
hostname: baseUrl.hostname,
|
|
263
|
+
port: baseUrl.port || (baseUrl.protocol === "https:" ? 443 : 80),
|
|
264
|
+
path: baseUrl.pathname + baseUrl.search,
|
|
265
|
+
method,
|
|
266
|
+
headers,
|
|
267
|
+
timeout: 30000,
|
|
268
|
+
};
|
|
269
|
+
if (opts.selfSignedSsl) {
|
|
270
|
+
requestOpts.rejectUnauthorized = false;
|
|
271
|
+
}
|
|
272
|
+
return new Promise((resolve, reject) => {
|
|
273
|
+
const req = transport.request(requestOpts, (res) => {
|
|
274
|
+
let data = "";
|
|
275
|
+
res.on("data", (chunk) => (data += chunk));
|
|
276
|
+
res.on("end", () => {
|
|
277
|
+
resolve({
|
|
278
|
+
status: res.statusCode || 0,
|
|
279
|
+
body: data,
|
|
280
|
+
headers: res.headers,
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
req.on("error", reject);
|
|
285
|
+
req.on("timeout", () => { req.destroy(); reject(new Error("Timeout")); });
|
|
286
|
+
if (body)
|
|
287
|
+
req.write(body);
|
|
288
|
+
req.end();
|
|
289
|
+
});
|
|
290
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,8 @@ const HELP = `
|
|
|
17
17
|
|
|
18
18
|
Comandos:
|
|
19
19
|
init [--local] Configura conexão com sistema SAP (wizard interativo)
|
|
20
|
+
init --name dev --url https://host:8443 --user USER --pass PASS [opts]
|
|
21
|
+
Modo non-interactive (para automação/CI)
|
|
20
22
|
link Vincula sistemas globais a este workspace (VS Code)
|
|
21
23
|
activate <LICENSE_KEY> Ativa licença do produto
|
|
22
24
|
status Mostra licença, sistemas e métricas de uso
|
|
@@ -50,9 +52,25 @@ async function main() {
|
|
|
50
52
|
}
|
|
51
53
|
const local = args.includes("--local");
|
|
52
54
|
switch (command) {
|
|
53
|
-
case "init":
|
|
54
|
-
|
|
55
|
+
case "init": {
|
|
56
|
+
const getFlag = (flag) => {
|
|
57
|
+
const idx = args.indexOf(flag);
|
|
58
|
+
return idx >= 0 && idx + 1 < args.length ? args[idx + 1] : undefined;
|
|
59
|
+
};
|
|
60
|
+
await (0, init_js_1.init)({
|
|
61
|
+
local,
|
|
62
|
+
name: getFlag("--name"),
|
|
63
|
+
url: getFlag("--url"),
|
|
64
|
+
client: getFlag("--client"),
|
|
65
|
+
user: getFlag("--user"),
|
|
66
|
+
pass: getFlag("--pass"),
|
|
67
|
+
language: getFlag("--language"),
|
|
68
|
+
selfSignedSsl: args.includes("--no-ssl-verify") ? true : undefined,
|
|
69
|
+
envRole: getFlag("--env-role"),
|
|
70
|
+
skipTest: args.includes("--skip-test"),
|
|
71
|
+
});
|
|
55
72
|
break;
|
|
73
|
+
}
|
|
56
74
|
case "activate": {
|
|
57
75
|
const key = args[1];
|
|
58
76
|
if (!key) {
|
package/dist/index.js
CHANGED
|
@@ -29,7 +29,7 @@ const function_group_js_1 = require("./tools/function-group.js");
|
|
|
29
29
|
const message_class_js_1 = require("./tools/message-class.js");
|
|
30
30
|
const abap_doc_js_1 = require("./tools/abap-doc.js");
|
|
31
31
|
const release_transport_js_1 = require("./tools/release-transport.js");
|
|
32
|
-
|
|
32
|
+
// abapDeleteObject removido — delete bloqueado permanentemente (decisão de produto)
|
|
33
33
|
const refactor_rename_js_1 = require("./tools/refactor-rename.js");
|
|
34
34
|
const code_completion_js_1 = require("./tools/code-completion.js");
|
|
35
35
|
const navigate_js_1 = require("./tools/navigate.js");
|
|
@@ -56,8 +56,10 @@ const breakpoints_js_1 = require("./tools/breakpoints.js");
|
|
|
56
56
|
const knowledge_js_1 = require("./tools/knowledge.js");
|
|
57
57
|
const create_dcl_js_1 = require("./tools/create-dcl.js");
|
|
58
58
|
const create_amdp_js_1 = require("./tools/create-amdp.js");
|
|
59
|
+
const execute_js_1 = require("./tools/execute.js");
|
|
59
60
|
const system_profile_js_1 = require("./system-profile.js");
|
|
60
61
|
const security_policy_js_1 = require("./security-policy.js");
|
|
62
|
+
const sap_policy_loader_js_1 = require("./sap-policy-loader.js");
|
|
61
63
|
const security_audit_js_1 = require("./security-audit.js");
|
|
62
64
|
const license_guard_js_1 = require("./license-guard.js");
|
|
63
65
|
const server = new mcp_js_1.McpServer({
|
|
@@ -817,35 +819,22 @@ server.tool("abap_release_transport", "Libera uma ordem de transporte no SAP. AT
|
|
|
817
819
|
};
|
|
818
820
|
}
|
|
819
821
|
});
|
|
820
|
-
// Tool: abap_delete
|
|
821
|
-
server.tool("abap_delete", "
|
|
822
|
-
object_type: zod_1.z
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
try {
|
|
837
|
-
const result = await (0, delete_js_1.abapDeleteObject)({ object_type, object_name, transport_request });
|
|
838
|
-
return {
|
|
839
|
-
content: [{ type: "text", text: result }],
|
|
840
|
-
};
|
|
841
|
-
}
|
|
842
|
-
catch (error) {
|
|
843
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
844
|
-
return {
|
|
845
|
-
content: [{ type: "text", text: `Erro ao excluir objeto: ${message}` }],
|
|
846
|
-
isError: true,
|
|
847
|
-
};
|
|
848
|
-
}
|
|
822
|
+
// Tool: abap_delete — BLOQUEADA permanentemente (decisão de produto)
|
|
823
|
+
server.tool("abap_delete", "Exclusão de objetos ABAP não é suportada pelo LKPABAP.ia. Use a transação SE80 no SAP GUI ou o Eclipse ADT para excluir objetos.", {
|
|
824
|
+
object_type: zod_1.z.string().describe("Tipo do objeto SAP."),
|
|
825
|
+
object_name: zod_1.z.string().describe("Nome do objeto ABAP."),
|
|
826
|
+
}, async () => {
|
|
827
|
+
return {
|
|
828
|
+
content: [{
|
|
829
|
+
type: "text",
|
|
830
|
+
text: "BLOQUEADO: O LKPABAP.ia não executa exclusão de objetos ABAP.\n\n"
|
|
831
|
+
+ "Exclusão é uma operação irreversível que deve ser feita manualmente pelo desenvolvedor:\n"
|
|
832
|
+
+ " • SAP GUI: transação SE80 → selecionar objeto → Excluir\n"
|
|
833
|
+
+ " • Eclipse ADT: botão direito → Delete\n\n"
|
|
834
|
+
+ "Esta restrição é permanente e não pode ser sobreposta por configuração.",
|
|
835
|
+
}],
|
|
836
|
+
isError: true,
|
|
837
|
+
};
|
|
849
838
|
});
|
|
850
839
|
// Tool: abap_refactor_rename
|
|
851
840
|
server.tool("abap_refactor_rename", "Renomeia um símbolo (variável, método, atributo) em um objeto ABAP usando o refactoring ADT. Aplica a renomeação em todas as referências.", {
|
|
@@ -1415,6 +1404,31 @@ server.resource("system-profile", "abap://system-profile", { description: "Perfi
|
|
|
1415
1404
|
}],
|
|
1416
1405
|
};
|
|
1417
1406
|
});
|
|
1407
|
+
// Tool: abap_execute — executa programas ABAP via ICF handler
|
|
1408
|
+
server.tool("abap_execute", "Executa um programa ABAP (Z* ou Y*) no sistema SAP e retorna o output. Requer ICF handler /sap/z/lkpai/guard ativado no SICF. Útil para programas de setup, migração, testes ou utilities.", {
|
|
1409
|
+
program_name: zod_1.z
|
|
1410
|
+
.string()
|
|
1411
|
+
.describe("Nome do programa ABAP a executar (ex: ZLKP_AI_SETUP). Apenas Z* e Y*."),
|
|
1412
|
+
variant: zod_1.z
|
|
1413
|
+
.string()
|
|
1414
|
+
.optional()
|
|
1415
|
+
.describe("Variante de seleção (opcional). Se não informada, executa com valores default dos parâmetros."),
|
|
1416
|
+
}, async ({ program_name, variant }) => {
|
|
1417
|
+
const blocked = securityGuard("abap_execute");
|
|
1418
|
+
if (blocked)
|
|
1419
|
+
return blocked;
|
|
1420
|
+
try {
|
|
1421
|
+
const result = await (0, execute_js_1.abapExecute)({ program_name, variant });
|
|
1422
|
+
return { content: [{ type: "text", text: result }] };
|
|
1423
|
+
}
|
|
1424
|
+
catch (error) {
|
|
1425
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1426
|
+
return {
|
|
1427
|
+
content: [{ type: "text", text: `Erro ao executar programa: ${message}` }],
|
|
1428
|
+
isError: true,
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
});
|
|
1418
1432
|
// Resource: security-policy
|
|
1419
1433
|
server.resource("security-policy", "abap://security-policy", { description: "Política de segurança ativa — environment role, níveis permitidos, tools bloqueadas, limites. Configurável via env ABAP_AI_ENV_ROLE ou arquivo ~/.abap-ai/security-policy.json." }, async () => {
|
|
1420
1434
|
const policy = (0, security_policy_js_1.getPolicy)();
|
|
@@ -1440,10 +1454,24 @@ async function main() {
|
|
|
1440
1454
|
(0, security_policy_js_1.validateToolRiskMap)(_registeredToolNames);
|
|
1441
1455
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
1442
1456
|
await server.connect(transport);
|
|
1457
|
+
// Tentar carregar policy do SAP (ABAP Security Agent)
|
|
1458
|
+
// Se o agente está instalado (ZLKP_AI_CONFIG existe), a policy SAP tem prioridade
|
|
1459
|
+
const sapUser = process.env.SAP_USER || "";
|
|
1460
|
+
let policySource = "local";
|
|
1461
|
+
try {
|
|
1462
|
+
const sapPolicy = await (0, sap_policy_loader_js_1.loadPolicyFromSap)(sapUser);
|
|
1463
|
+
if (sapPolicy) {
|
|
1464
|
+
(0, security_policy_js_1.setPolicy)(sapPolicy);
|
|
1465
|
+
policySource = "SAP (ZLKP_AI_CONFIG)";
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
catch {
|
|
1469
|
+
// Fallback silencioso para policy local
|
|
1470
|
+
}
|
|
1443
1471
|
const profile = (0, system_profile_js_1.getProfile)();
|
|
1444
1472
|
const policy = (0, security_policy_js_1.getPolicy)();
|
|
1445
1473
|
const licLabel = (0, license_guard_js_1.licenseStatusLabel)();
|
|
1446
|
-
process.stderr.write(`abap-mcp-server v${VERSION} iniciado | License: ${licLabel} | System: ${profile.system.type} | ABAP: ${profile.system.abapPlatform} | Security: ${policy.environment_role} (${policy.allowed_levels.join("+")})\n`);
|
|
1474
|
+
process.stderr.write(`abap-mcp-server v${VERSION} iniciado | License: ${licLabel} | System: ${profile.system.type} | ABAP: ${profile.system.abapPlatform} | Security: ${policy.environment_role} (${policy.allowed_levels.join("+")}) [${policySource}]\n`);
|
|
1447
1475
|
}
|
|
1448
1476
|
main().catch((err) => {
|
|
1449
1477
|
process.stderr.write(`Falha ao iniciar servidor: ${err}\n`);
|
package/dist/license-guard.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* License Guard — valida licença antes de executar qualquer tool.
|
|
3
|
+
* License Guard — valida licença e rate limiting antes de executar qualquer tool.
|
|
4
4
|
*
|
|
5
5
|
* Singleton: lê ~/.abap-ai/license.json uma vez e cacheia.
|
|
6
6
|
* licenseGuard() retorna null se OK, ou MCP error response se bloqueado.
|
|
@@ -11,6 +11,7 @@ exports.reloadLicense = reloadLicense;
|
|
|
11
11
|
exports.licenseGuard = licenseGuard;
|
|
12
12
|
exports.licenseStatusLabel = licenseStatusLabel;
|
|
13
13
|
const activate_js_1 = require("./cli/activate.js");
|
|
14
|
+
const security_audit_js_1 = require("./security-audit.js");
|
|
14
15
|
// ─── Singleton ───────────────────────────────────────────────────
|
|
15
16
|
let cachedLicense = undefined;
|
|
16
17
|
/**
|
|
@@ -65,8 +66,49 @@ function licenseGuard() {
|
|
|
65
66
|
isError: true,
|
|
66
67
|
};
|
|
67
68
|
}
|
|
69
|
+
// Rate limiting (Starter: 200 calls/dia)
|
|
70
|
+
const limit = license.limits.calls_per_day;
|
|
71
|
+
if (limit > 0) {
|
|
72
|
+
const used = getDailyCallCount();
|
|
73
|
+
if (used >= limit) {
|
|
74
|
+
return {
|
|
75
|
+
content: [{
|
|
76
|
+
type: "text",
|
|
77
|
+
text: `LKPABAP.ia — Limite diário atingido (${used}/${limit} chamadas).\n\n` +
|
|
78
|
+
`Seu plano ${license.plan} permite ${limit} chamadas por dia.\n` +
|
|
79
|
+
`O contador reseta à meia-noite.\n\n` +
|
|
80
|
+
`Para uso ilimitado, faça upgrade para o plano Pro:\n` +
|
|
81
|
+
` https://lkpabap.ai/pricing`,
|
|
82
|
+
}],
|
|
83
|
+
isError: true,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
68
87
|
return null;
|
|
69
88
|
}
|
|
89
|
+
// ─── Rate Limiting ──────────────────────────────────────────────
|
|
90
|
+
/** Cache do count diário para não reler o arquivo a cada chamada */
|
|
91
|
+
let dailyCountCache = null;
|
|
92
|
+
/**
|
|
93
|
+
* Conta chamadas ALLOWED do dia (audit log do dia + sessão atual).
|
|
94
|
+
* Cache é invalidado quando o dia muda.
|
|
95
|
+
*/
|
|
96
|
+
function getDailyCallCount() {
|
|
97
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
98
|
+
// Invalidar cache se mudou o dia
|
|
99
|
+
if (dailyCountCache && dailyCountCache.date !== today) {
|
|
100
|
+
dailyCountCache = null;
|
|
101
|
+
}
|
|
102
|
+
// Ler do arquivo apenas 1x por sessão (chamadas anteriores de hoje)
|
|
103
|
+
if (!dailyCountCache) {
|
|
104
|
+
const todayEntries = (0, security_audit_js_1.readTodayAudit)();
|
|
105
|
+
const previousAllowed = todayEntries.filter((e) => e.result === "ALLOWED").length;
|
|
106
|
+
dailyCountCache = { date: today, count: previousAllowed };
|
|
107
|
+
}
|
|
108
|
+
// Incrementar com a chamada atual (este guard roda ANTES do audit, então +1)
|
|
109
|
+
dailyCountCache.count++;
|
|
110
|
+
return dailyCountCache.count;
|
|
111
|
+
}
|
|
70
112
|
/**
|
|
71
113
|
* Retorna string de status para log no startup.
|
|
72
114
|
*/
|