@linkup-ai/abap-ai 2.0.0 → 2.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/README.md +4 -4
- package/dist/cli/activate.js +41 -28
- package/dist/cli/init.js +16 -1
- package/dist/cli/status.js +1 -1
- package/dist/index.js +107 -3
- package/dist/security-audit.js +136 -0
- package/dist/security-policy.js +322 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ MCP (Model Context Protocol) Server that connects [Claude Code](https://claude.a
|
|
|
4
4
|
|
|
5
5
|
Once configured, Claude can read, write, create, activate, search, refactor, test and deploy ABAP objects — including RAP/Fiori stacks — directly from your editor without any manual SAP GUI interaction.
|
|
6
6
|
|
|
7
|
-
**55 tools** covering the full ABAP development lifecycle, powered by a **curated knowledge base** with
|
|
7
|
+
**55 tools** covering the full ABAP development lifecycle, powered by a **curated knowledge base** with 50 reference guides on ABAP, CDS, RAP, Fiori and CAP.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -132,11 +132,11 @@ Once configured, Claude can read, write, create, activate, search, refactor, tes
|
|
|
132
132
|
|
|
133
133
|
The knowledge base is the core differentiator of this MCP server. It provides Claude with **curated, LLM-optimized reference material** (70% code examples, 30% rules and anti-patterns) so that generated code follows best practices out of the box.
|
|
134
134
|
|
|
135
|
-
**
|
|
135
|
+
**50 guides** organized in 4 domains:
|
|
136
136
|
|
|
137
137
|
| Domain | Topics | Examples |
|
|
138
138
|
|--------|--------|----------|
|
|
139
|
-
| **ABAP Core** |
|
|
139
|
+
| **ABAP Core** | 26 | Internal tables, ABAP SQL, RAP (EML, draft, feature control, numbering, unmanaged), OO, enhancements/BAdIs, clean code, performance, unit testing, cloud development |
|
|
140
140
|
| **ABAP CDS** | 6 | Annotations, associations, access control, expressions, functions, metadata extensions |
|
|
141
141
|
| **Fiori / UI5** | 11 | Fiori Elements, annotations, value lists, side effects, XML views, controllers, routing, data binding, fragments, manifest, deployment |
|
|
142
142
|
| **CAP (Node.js)** | 7 | CDL syntax, CQL queries, service definitions, event handlers, authentication, Fiori integration, deployment |
|
|
@@ -242,7 +242,7 @@ See [docs/CLI_GUIDE.md](docs/CLI_GUIDE.md) for the full step-by-step guide.
|
|
|
242
242
|
| Command | Description |
|
|
243
243
|
|---------|-------------|
|
|
244
244
|
| `abap-ai init` | Interactive wizard to configure SAP system connections |
|
|
245
|
-
| `abap-ai activate <KEY>` | Activate product license
|
|
245
|
+
| `abap-ai activate <KEY>` | Activate product license |
|
|
246
246
|
| `abap-ai status` | Show license, systems (with connection test), usage metrics and knowledge base |
|
|
247
247
|
| `abap-ai systems` | List configured SAP systems |
|
|
248
248
|
| `abap-ai remove <name>` | Remove a SAP system from mcp.json |
|
package/dist/cli/activate.js
CHANGED
|
@@ -35,12 +35,36 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.readLicense = readLicense;
|
|
37
37
|
exports.activate = activate;
|
|
38
|
+
const crypto = __importStar(require("crypto"));
|
|
38
39
|
const fs = __importStar(require("fs"));
|
|
39
40
|
const path = __importStar(require("path"));
|
|
40
41
|
const LICENSE_DIR = path.join(process.env.HOME || "~", ".abap-ai");
|
|
41
42
|
const LICENSE_PATH = path.join(LICENSE_DIR, "license.json");
|
|
42
|
-
//
|
|
43
|
-
|
|
43
|
+
// ── Beta key validation (offline, HMAC-based) ───────────────────────
|
|
44
|
+
// Em produção será substituído por fetch ao license server (api.lkpabap.ai)
|
|
45
|
+
const BETA_SECRET = "lkp-beta-2026-xK9mQ3vR";
|
|
46
|
+
const PLAN_MAP = { ST: "starter", PR: "pro", EN: "enterprise" };
|
|
47
|
+
function validateBetaKey(key) {
|
|
48
|
+
const upper = key.toUpperCase();
|
|
49
|
+
const match = upper.match(/^LK-([A-Z]{2})([A-Z]{2})-([A-Z0-9]{4})-([A-Z0-9]{4})$/);
|
|
50
|
+
if (!match)
|
|
51
|
+
return null;
|
|
52
|
+
const [, planCode, modeCode, rand, checksum] = match;
|
|
53
|
+
const plan = PLAN_MAP[planCode];
|
|
54
|
+
if (!plan || modeCode !== "BK")
|
|
55
|
+
return null;
|
|
56
|
+
// Recalcula HMAC e compara com o checksum da chave
|
|
57
|
+
const payload = `${planCode}${modeCode}-${rand}`;
|
|
58
|
+
const expected = crypto
|
|
59
|
+
.createHmac("sha256", BETA_SECRET)
|
|
60
|
+
.update(payload)
|
|
61
|
+
.digest("hex")
|
|
62
|
+
.toUpperCase()
|
|
63
|
+
.slice(0, 4);
|
|
64
|
+
if (checksum !== expected)
|
|
65
|
+
return null;
|
|
66
|
+
return { plan };
|
|
67
|
+
}
|
|
44
68
|
function saveLicense(license) {
|
|
45
69
|
if (!fs.existsSync(LICENSE_DIR)) {
|
|
46
70
|
fs.mkdirSync(LICENSE_DIR, { recursive: true });
|
|
@@ -58,10 +82,6 @@ function readLicense() {
|
|
|
58
82
|
}
|
|
59
83
|
async function activate(key) {
|
|
60
84
|
console.log("\n ⏳ Validando licença...");
|
|
61
|
-
// -----------------------------------------------------------------
|
|
62
|
-
// STUB: Enquanto o license server não existe, simula validação local
|
|
63
|
-
// Aceita qualquer chave no formato LK-XXXX-XXXX-XXXX
|
|
64
|
-
// -----------------------------------------------------------------
|
|
65
85
|
const keyPattern = /^LK-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}$/i;
|
|
66
86
|
if (!keyPattern.test(key)) {
|
|
67
87
|
console.log(` ✗ Formato de chave inválido.`);
|
|
@@ -69,25 +89,27 @@ async function activate(key) {
|
|
|
69
89
|
console.log(` Adquira em: https://lkpabap.ai\n`);
|
|
70
90
|
process.exit(1);
|
|
71
91
|
}
|
|
72
|
-
//
|
|
73
|
-
// TODO:
|
|
74
|
-
const
|
|
75
|
-
|
|
92
|
+
// ── Tenta validação offline (beta keys com HMAC) ──────────────────
|
|
93
|
+
// TODO: Quando o license server existir, tentar fetch a api.lkpabap.ai primeiro
|
|
94
|
+
const beta = validateBetaKey(key);
|
|
95
|
+
if (!beta) {
|
|
96
|
+
console.log(` ✗ Chave inválida ou expirada.`);
|
|
97
|
+
console.log(` Verifique a chave recebida ou solicite uma nova.\n`);
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
const { plan } = beta;
|
|
76
101
|
const license = {
|
|
77
102
|
key: key.toUpperCase(),
|
|
78
103
|
plan,
|
|
79
|
-
mode: isAllInOne ? "allinone" : "byok",
|
|
80
104
|
limits: {
|
|
81
105
|
systems: plan === "starter" ? 1 : plan === "pro" ? 3 : -1,
|
|
82
106
|
calls_per_day: plan === "starter" ? 200 : -1,
|
|
83
|
-
tokens_per_month: isAllInOne ? (plan === "starter" ? 500000 : plan === "pro" ? 5000000 : 20000000) : undefined,
|
|
84
107
|
},
|
|
85
|
-
expires: new Date(Date.now() +
|
|
108
|
+
expires: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString().split("T")[0],
|
|
86
109
|
validated_at: new Date().toISOString(),
|
|
87
|
-
proxy_url: isAllInOne ? "https://proxy.lkpabap.ai/v1" : null,
|
|
88
110
|
};
|
|
89
111
|
saveLicense(license);
|
|
90
|
-
const planLabel = `${plan.charAt(0).toUpperCase() + plan.slice(1)} (
|
|
112
|
+
const planLabel = `${plan.charAt(0).toUpperCase() + plan.slice(1)} (BYOK)`;
|
|
91
113
|
const systemsLabel = license.limits.systems === -1 ? "Ilimitado" : `até ${license.limits.systems}`;
|
|
92
114
|
const callsLabel = license.limits.calls_per_day === -1 ? "Ilimitado" : `${license.limits.calls_per_day}/dia`;
|
|
93
115
|
console.log(` ✓ Licença válida!`);
|
|
@@ -96,18 +118,9 @@ async function activate(key) {
|
|
|
96
118
|
│ Plano: ${planLabel.padEnd(25)}│
|
|
97
119
|
│ Válido até: ${license.expires.padEnd(25)}│
|
|
98
120
|
│ Sistemas: ${systemsLabel.padEnd(25)}│
|
|
99
|
-
│ Chamadas/dia: ${callsLabel.padEnd(25)}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
: "N/A";
|
|
104
|
-
console.log(` │ Tokens/mês: ${tokensLabel.padEnd(25)}│`);
|
|
105
|
-
console.log(` │${" ".repeat(42)}│`);
|
|
106
|
-
console.log(` │ ✓ Claude proxy configurado${" ".repeat(13)}│`);
|
|
107
|
-
console.log(` │ URL: ${(license.proxy_url || "").padEnd(32)}│`);
|
|
108
|
-
}
|
|
109
|
-
console.log(` │${" ".repeat(42)}│`);
|
|
110
|
-
console.log(` │ Licença salva em ~/.abap-ai/license${" ".repeat(4)}│`);
|
|
111
|
-
console.log(` ╰──────────────────────────────────────────╯`);
|
|
121
|
+
│ Chamadas/dia: ${callsLabel.padEnd(25)}│
|
|
122
|
+
│${" ".repeat(42)}│
|
|
123
|
+
│ Licença salva em ~/.abap-ai/license${" ".repeat(4)}│
|
|
124
|
+
╰──────────────────────────────────────────╯`);
|
|
112
125
|
console.log(`\n Próximo passo: abap-ai init\n`);
|
|
113
126
|
}
|
package/dist/cli/init.js
CHANGED
|
@@ -149,6 +149,7 @@ function addSystemToConfig(config, system) {
|
|
|
149
149
|
SAP_USER: system.user,
|
|
150
150
|
SAP_PASS: system.pass,
|
|
151
151
|
SAP_LANGUAGE: system.language,
|
|
152
|
+
ABAP_AI_ENV_ROLE: system.environmentRole,
|
|
152
153
|
};
|
|
153
154
|
if (system.selfSignedSsl) {
|
|
154
155
|
env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
|
|
@@ -214,6 +215,17 @@ async function promptSystem() {
|
|
|
214
215
|
message: "Certificado SSL self-signed?",
|
|
215
216
|
initial: true,
|
|
216
217
|
},
|
|
218
|
+
{
|
|
219
|
+
type: "select",
|
|
220
|
+
name: "environmentRole",
|
|
221
|
+
message: "Papel do ambiente (controla quais operações o LKPABAP.ia pode executar)",
|
|
222
|
+
choices: [
|
|
223
|
+
{ title: "DEVELOPMENT — leitura + escrita + criação (padrão para DEV)", value: "DEVELOPMENT" },
|
|
224
|
+
{ title: "QUALITY — somente leitura (padrão para QAS/QA)", value: "QUALITY" },
|
|
225
|
+
{ title: "PRODUCTION — somente leitura, mais restritivo (padrão para PRD)", value: "PRODUCTION" },
|
|
226
|
+
],
|
|
227
|
+
initial: 0,
|
|
228
|
+
},
|
|
217
229
|
], {
|
|
218
230
|
onCancel: () => {
|
|
219
231
|
console.log("\n Cancelado.");
|
|
@@ -231,6 +243,7 @@ async function promptSystem() {
|
|
|
231
243
|
pass: response.pass,
|
|
232
244
|
language: (response.language?.trim() || "PT").toUpperCase(),
|
|
233
245
|
selfSignedSsl: response.selfSignedSsl ?? true,
|
|
246
|
+
environmentRole: response.environmentRole || "DEVELOPMENT",
|
|
234
247
|
};
|
|
235
248
|
}
|
|
236
249
|
// ---------------------------------------------------------------------------
|
|
@@ -318,7 +331,9 @@ async function init() {
|
|
|
318
331
|
const sn = `abap-${name}`;
|
|
319
332
|
const env = config.mcpServers[sn]?.env || {};
|
|
320
333
|
const client = env.SAP_CLIENT || "?";
|
|
321
|
-
const
|
|
334
|
+
const role = env.ABAP_AI_ENV_ROLE || "DEV";
|
|
335
|
+
const roleTag = role === "PRODUCTION" ? "PRD" : role === "QUALITY" ? "QAS" : "DEV";
|
|
336
|
+
const line = ` │ • ${name} (client ${client}, ${roleTag})`;
|
|
322
337
|
console.log(`${line}${" ".repeat(Math.max(1, 48 - line.length))}│`);
|
|
323
338
|
}
|
|
324
339
|
console.log(` │${" ".repeat(46)}│
|
package/dist/cli/status.js
CHANGED
|
@@ -158,7 +158,7 @@ async function status() {
|
|
|
158
158
|
const expires = new Date(license.expires);
|
|
159
159
|
const daysLeft = Math.ceil((expires.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
160
160
|
const expired = daysLeft <= 0;
|
|
161
|
-
const planLabel = `${license.plan.charAt(0).toUpperCase() + license.plan.slice(1)} (
|
|
161
|
+
const planLabel = `${license.plan.charAt(0).toUpperCase() + license.plan.slice(1)} (BYOK)`;
|
|
162
162
|
console.log(` Plano: ${planLabel}`);
|
|
163
163
|
console.log(` Válido até: ${license.expires} (${expired ? "EXPIRADA" : `${daysLeft} dias restantes`})`);
|
|
164
164
|
console.log(` Status: ${expired ? "✗ Expirada — renove em https://lkpabap.ai" : "✓ Ativa"}`);
|
package/dist/index.js
CHANGED
|
@@ -55,10 +55,42 @@ const knowledge_js_1 = require("./tools/knowledge.js");
|
|
|
55
55
|
const create_dcl_js_1 = require("./tools/create-dcl.js");
|
|
56
56
|
const create_amdp_js_1 = require("./tools/create-amdp.js");
|
|
57
57
|
const system_profile_js_1 = require("./system-profile.js");
|
|
58
|
+
const security_policy_js_1 = require("./security-policy.js");
|
|
59
|
+
const security_audit_js_1 = require("./security-audit.js");
|
|
58
60
|
const server = new mcp_js_1.McpServer({
|
|
59
61
|
name: "abap-adt",
|
|
60
62
|
version: "2.0.0",
|
|
61
63
|
});
|
|
64
|
+
// ─── Security Guard ───────────────────────────────────────────────
|
|
65
|
+
/**
|
|
66
|
+
* Verifica se uma tool pode ser executada sob a security policy atual.
|
|
67
|
+
* Retorna null se permitido, ou uma resposta de erro MCP se bloqueado.
|
|
68
|
+
*/
|
|
69
|
+
function securityGuard(toolName, transportRequest) {
|
|
70
|
+
const policy = (0, security_policy_js_1.getPolicy)();
|
|
71
|
+
const riskLevel = (0, security_policy_js_1.getToolRisk)(toolName);
|
|
72
|
+
// 1. Verificar acesso pela policy
|
|
73
|
+
const accessCheck = (0, security_policy_js_1.checkToolAccess)(toolName, policy);
|
|
74
|
+
if (!accessCheck.allowed) {
|
|
75
|
+
(0, security_audit_js_1.auditBlocked)(toolName, riskLevel, policy.environment_role, accessCheck.reason || "policy");
|
|
76
|
+
return {
|
|
77
|
+
content: [{ type: "text", text: accessCheck.reason || `Tool ${toolName} bloqueada.` }],
|
|
78
|
+
isError: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// 2. Verificar transport request obrigatório
|
|
82
|
+
const transportCheck = (0, security_policy_js_1.checkTransportRequired)(toolName, transportRequest, policy);
|
|
83
|
+
if (!transportCheck.allowed) {
|
|
84
|
+
(0, security_audit_js_1.auditBlocked)(toolName, riskLevel, policy.environment_role, transportCheck.reason || "transport required");
|
|
85
|
+
return {
|
|
86
|
+
content: [{ type: "text", text: transportCheck.reason || `Transport request obrigatório.` }],
|
|
87
|
+
isError: true,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// 3. Permitido — registrar no audit
|
|
91
|
+
(0, security_audit_js_1.auditAllowed)(toolName, riskLevel, policy.environment_role);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
62
94
|
// Tool: abap_read
|
|
63
95
|
server.tool("abap_read", "Lê o código-fonte de um objeto ABAP no sistema SAP via ADT API. Para tabelas (TABL/DT), estruturas (TABL/DS) e table types (TTYP/TT), retorna a lista de campos com tipo e descrição.", {
|
|
64
96
|
object_type: zod_1.z
|
|
@@ -106,6 +138,9 @@ server.tool("abap_write", "Grava código-fonte em um objeto ABAP existente no si
|
|
|
106
138
|
.optional()
|
|
107
139
|
.describe("Ordem de transporte (ex: ECDK900123). Obrigatória para objetos em pacotes não-locais."),
|
|
108
140
|
}, async ({ object_type, object_name, source, class_include, transport_request }) => {
|
|
141
|
+
const blocked = securityGuard("abap_write", transport_request);
|
|
142
|
+
if (blocked)
|
|
143
|
+
return blocked;
|
|
109
144
|
try {
|
|
110
145
|
const result = await (0, write_js_1.abapWrite)({ object_type, object_name, source, class_include, transport_request });
|
|
111
146
|
return {
|
|
@@ -129,6 +164,9 @@ server.tool("abap_activate", "Ativa um objeto ABAP no sistema SAP via ADT API. R
|
|
|
129
164
|
.string()
|
|
130
165
|
.describe("Nome do objeto ABAP (ex: ZTESTEDENIS)."),
|
|
131
166
|
}, async ({ object_type, object_name }) => {
|
|
167
|
+
const blocked = securityGuard("abap_activate");
|
|
168
|
+
if (blocked)
|
|
169
|
+
return blocked;
|
|
132
170
|
try {
|
|
133
171
|
const result = await (0, activate_js_1.abapActivate)({ object_type, object_name });
|
|
134
172
|
return {
|
|
@@ -173,6 +211,9 @@ server.tool("abap_create", "Cria um novo objeto ABAP no sistema SAP via ADT API.
|
|
|
173
211
|
.optional()
|
|
174
212
|
.describe("Ordem de transporte (ex: ECDK900123). Se omitida, o SAP usa $TMP ou atribui automaticamente."),
|
|
175
213
|
}, async ({ object_type, object_name, description, package: pkg, srvd_name, binding_type, transport_request }) => {
|
|
214
|
+
const blocked = securityGuard("abap_create", transport_request);
|
|
215
|
+
if (blocked)
|
|
216
|
+
return blocked;
|
|
176
217
|
try {
|
|
177
218
|
const result = await (0, create_js_1.abapCreate)({ object_type, object_name, description, package: pkg, srvd_name, binding_type, transport_request });
|
|
178
219
|
return {
|
|
@@ -302,6 +343,9 @@ server.tool("abap_assign_transport", "Vincula um objeto ABAP existente a uma ord
|
|
|
302
343
|
.string()
|
|
303
344
|
.describe("Nome do objeto ABAP a vincular."),
|
|
304
345
|
}, async ({ transport_request, object_type, object_name }) => {
|
|
346
|
+
const blocked = securityGuard("abap_assign_transport", transport_request);
|
|
347
|
+
if (blocked)
|
|
348
|
+
return blocked;
|
|
305
349
|
try {
|
|
306
350
|
const result = await (0, transports_js_1.abapAssignTransport)({ transport_request, object_type, object_name });
|
|
307
351
|
return {
|
|
@@ -358,6 +402,9 @@ server.tool("abap_scaffold_rap", "Cria um stack RAP completo no SAP com 4 scenar
|
|
|
358
402
|
.optional()
|
|
359
403
|
.describe("Ordem de transporte para todos os objetos criados."),
|
|
360
404
|
}, async ({ base_name, description, scenario, child_entities, with_feature_control, with_business_events, package: pkg, binding_type, transport_request }) => {
|
|
405
|
+
const blocked = securityGuard("abap_scaffold_rap", transport_request);
|
|
406
|
+
if (blocked)
|
|
407
|
+
return blocked;
|
|
361
408
|
try {
|
|
362
409
|
const result = await (0, scaffold_rap_js_1.abapScaffoldRap)({ base_name, description, scenario, child_entities, with_feature_control, with_business_events, package: pkg, binding_type, transport_request });
|
|
363
410
|
return {
|
|
@@ -382,6 +429,9 @@ server.tool("abap_publish_binding", "Publica uma Service Binding (SRVB/SVB) no S
|
|
|
382
429
|
.optional()
|
|
383
430
|
.describe("Versão do serviço no Gateway (padrão: '0001')."),
|
|
384
431
|
}, async ({ binding_name, service_version }) => {
|
|
432
|
+
const blocked = securityGuard("abap_publish_binding");
|
|
433
|
+
if (blocked)
|
|
434
|
+
return blocked;
|
|
385
435
|
try {
|
|
386
436
|
const result = await (0, publish_binding_js_1.abapPublishBinding)({ binding_name, service_version });
|
|
387
437
|
return {
|
|
@@ -417,6 +467,9 @@ server.tool("abap_deploy_bsp", "Faz deploy de uma aplicação Fiori/UI5 para o A
|
|
|
417
467
|
.optional()
|
|
418
468
|
.describe("Ordem de transporte (ex: ECDK900123). Obrigatória se o pacote não for $TMP."),
|
|
419
469
|
}, async ({ bsp_name, folder_path, package: pkg, description, transport_request }) => {
|
|
470
|
+
const blocked = securityGuard("abap_deploy_bsp", transport_request);
|
|
471
|
+
if (blocked)
|
|
472
|
+
return blocked;
|
|
420
473
|
try {
|
|
421
474
|
const result = await (0, deploy_bsp_js_1.abapDeployBsp)({ bsp_name, folder_path, package: pkg, description, transport_request });
|
|
422
475
|
return {
|
|
@@ -492,8 +545,9 @@ server.tool("abap_data_preview", "Visualiza dados de uma tabela transparente ou
|
|
|
492
545
|
.optional()
|
|
493
546
|
.describe("Colunas a retornar, separadas por vírgula (ex: MATNR,MTART,MBRSH)."),
|
|
494
547
|
}, async ({ object_name, max_rows, where_clause, order_by, columns }) => {
|
|
548
|
+
const effectiveRows = (0, security_policy_js_1.getEffectiveMaxRows)(max_rows ?? 100, (0, security_policy_js_1.getPolicy)());
|
|
495
549
|
try {
|
|
496
|
-
const result = await (0, data_preview_js_1.abapDataPreview)({ object_name, max_rows, where_clause, order_by, columns });
|
|
550
|
+
const result = await (0, data_preview_js_1.abapDataPreview)({ object_name, max_rows: effectiveRows, where_clause, order_by, columns });
|
|
497
551
|
return {
|
|
498
552
|
content: [{ type: "text", text: result }],
|
|
499
553
|
};
|
|
@@ -721,6 +775,9 @@ server.tool("abap_release_transport", "Libera uma ordem de transporte no SAP. AT
|
|
|
721
775
|
.string()
|
|
722
776
|
.describe("Número da ordem de transporte a liberar (ex: ECDK900123)."),
|
|
723
777
|
}, async ({ transport_request }) => {
|
|
778
|
+
const blocked = securityGuard("abap_release_transport", transport_request);
|
|
779
|
+
if (blocked)
|
|
780
|
+
return blocked;
|
|
724
781
|
try {
|
|
725
782
|
const result = await (0, release_transport_js_1.abapReleaseTransport)({ transport_request });
|
|
726
783
|
return {
|
|
@@ -748,6 +805,9 @@ server.tool("abap_delete", "Exclui um objeto ABAP do sistema SAP. ATENÇÃO: ope
|
|
|
748
805
|
.optional()
|
|
749
806
|
.describe("Ordem de transporte (obrigatória para objetos em pacotes não-locais)."),
|
|
750
807
|
}, async ({ object_type, object_name, transport_request }) => {
|
|
808
|
+
const blocked = securityGuard("abap_delete", transport_request);
|
|
809
|
+
if (blocked)
|
|
810
|
+
return blocked;
|
|
751
811
|
try {
|
|
752
812
|
const result = await (0, delete_js_1.abapDeleteObject)({ object_type, object_name, transport_request });
|
|
753
813
|
return {
|
|
@@ -787,6 +847,9 @@ server.tool("abap_refactor_rename", "Renomeia um símbolo (variável, método, a
|
|
|
787
847
|
.optional()
|
|
788
848
|
.describe("Include da classe. Apenas para CLAS/OC. Padrão: main."),
|
|
789
849
|
}, async ({ object_type, object_name, old_name, new_name, line, column, class_include }) => {
|
|
850
|
+
const blocked = securityGuard("abap_refactor_rename");
|
|
851
|
+
if (blocked)
|
|
852
|
+
return blocked;
|
|
790
853
|
try {
|
|
791
854
|
const result = await (0, refactor_rename_js_1.abapRefactorRename)({ object_type, object_name, old_name, new_name, line, column, class_include });
|
|
792
855
|
return {
|
|
@@ -879,6 +942,9 @@ server.tool("abap_quick_fix", "Obtém e aplica correções rápidas (quick fixes
|
|
|
879
942
|
.describe("Include da classe. Apenas para CLAS/OC."),
|
|
880
943
|
transport_request: zod_1.z.string().optional().describe("Ordem de transporte para aplicar o fix."),
|
|
881
944
|
}, async ({ object_type, object_name, line, column, fix_index, class_include, transport_request }) => {
|
|
945
|
+
const blocked = securityGuard("abap_quick_fix", transport_request);
|
|
946
|
+
if (blocked)
|
|
947
|
+
return blocked;
|
|
882
948
|
try {
|
|
883
949
|
const result = await (0, quick_fix_js_1.abapQuickFix)({ object_type, object_name, line, column, fix_index, class_include, transport_request });
|
|
884
950
|
return { content: [{ type: "text", text: result }] };
|
|
@@ -903,6 +969,9 @@ server.tool("abap_extract_method", "Extrai um trecho de código ABAP selecionado
|
|
|
903
969
|
.describe("Include da classe. Apenas para CLAS/OC."),
|
|
904
970
|
transport_request: zod_1.z.string().optional().describe("Ordem de transporte (se necessário)."),
|
|
905
971
|
}, async ({ object_type, object_name, start_line, end_line, new_method_name, class_include, transport_request }) => {
|
|
972
|
+
const blocked = securityGuard("abap_extract_method", transport_request);
|
|
973
|
+
if (blocked)
|
|
974
|
+
return blocked;
|
|
906
975
|
try {
|
|
907
976
|
const result = await (0, extract_method_js_1.abapExtractMethod)({ object_type, object_name, start_line, end_line, new_method_name, class_include, transport_request });
|
|
908
977
|
return { content: [{ type: "text", text: result }] };
|
|
@@ -933,8 +1002,9 @@ server.tool("abap_sql_console", "Executa uma consulta ABAP SQL (Open SQL) no sis
|
|
|
933
1002
|
sql: zod_1.z.string().describe("Consulta ABAP SQL. Ex: SELECT * FROM mara WHERE matnr LIKE 'Z%' ORDER BY matnr UP TO 100 ROWS"),
|
|
934
1003
|
max_rows: zod_1.z.number().optional().describe("Número máximo de linhas retornadas (padrão: 100)."),
|
|
935
1004
|
}, async ({ sql, max_rows }) => {
|
|
1005
|
+
const effectiveRows = (0, security_policy_js_1.getEffectiveMaxRows)(max_rows ?? 100, (0, security_policy_js_1.getPolicy)());
|
|
936
1006
|
try {
|
|
937
|
-
const result = await (0, sql_console_js_1.abapSqlConsole)({ sql, max_rows });
|
|
1007
|
+
const result = await (0, sql_console_js_1.abapSqlConsole)({ sql, max_rows: effectiveRows });
|
|
938
1008
|
return { content: [{ type: "text", text: result }] };
|
|
939
1009
|
}
|
|
940
1010
|
catch (error) {
|
|
@@ -961,6 +1031,9 @@ server.tool("abap_create_transport", "Cria uma nova ordem de transporte (workben
|
|
|
961
1031
|
type: zod_1.z.enum(["W", "K"]).optional().describe("Tipo: W=Workbench (padrão), K=Customizing."),
|
|
962
1032
|
target: zod_1.z.string().optional().describe("Sistema de destino (ex: QAS). Opcional."),
|
|
963
1033
|
}, async ({ description, type, target }) => {
|
|
1034
|
+
const blocked = securityGuard("abap_create_transport");
|
|
1035
|
+
if (blocked)
|
|
1036
|
+
return blocked;
|
|
964
1037
|
try {
|
|
965
1038
|
const result = await (0, create_transport_js_1.abapCreateTransport)({ description, type, target });
|
|
966
1039
|
return { content: [{ type: "text", text: result }] };
|
|
@@ -1144,6 +1217,9 @@ server.tool("abap_git_pull", "Executa pull de um repositório abapGit no sistema
|
|
|
1144
1217
|
branch: zod_1.z.string().optional().describe("Nome do branch (padrão: main)."),
|
|
1145
1218
|
transport_request: zod_1.z.string().optional().describe("Ordem de transporte para os objetos importados."),
|
|
1146
1219
|
}, async ({ repo_id, branch, transport_request }) => {
|
|
1220
|
+
const blocked = securityGuard("abapgit_pull", transport_request);
|
|
1221
|
+
if (blocked)
|
|
1222
|
+
return blocked;
|
|
1147
1223
|
try {
|
|
1148
1224
|
const result = await (0, abapgit_js_1.abapGitPull)({ repo_id, branch, transport_request });
|
|
1149
1225
|
return { content: [{ type: "text", text: result }] };
|
|
@@ -1163,6 +1239,9 @@ server.tool("abap_git_stage", "Mostra status de staging e executa push para um r
|
|
|
1163
1239
|
message: zod_1.z.string().optional().describe("Mensagem de commit (obrigatória para push)."),
|
|
1164
1240
|
transport_request: zod_1.z.string().optional().describe("Ordem de transporte."),
|
|
1165
1241
|
}, async ({ repo_id, action, message: commitMsg, transport_request }) => {
|
|
1242
|
+
const blocked = securityGuard("abapgit_stage", transport_request);
|
|
1243
|
+
if (blocked)
|
|
1244
|
+
return blocked;
|
|
1166
1245
|
try {
|
|
1167
1246
|
const result = await (0, abapgit_js_1.abapGitStage)({ repo_id, action, message: commitMsg, transport_request });
|
|
1168
1247
|
return { content: [{ type: "text", text: result }] };
|
|
@@ -1233,6 +1312,9 @@ server.tool("abap_create_dcl", "Cria um objeto DCL (Data Control Language) para
|
|
|
1233
1312
|
package: zod_1.z.string().optional().describe("Pacote SAP. Padrão: '$TMP'."),
|
|
1234
1313
|
transport_request: zod_1.z.string().optional().describe("Ordem de transporte."),
|
|
1235
1314
|
}, async ({ dcl_name, cds_entity, auth_type, auth_mappings, literal_conditions, with_user_aspect, package: pkg, transport_request }) => {
|
|
1315
|
+
const blocked = securityGuard("abap_create_dcl", transport_request);
|
|
1316
|
+
if (blocked)
|
|
1317
|
+
return blocked;
|
|
1236
1318
|
try {
|
|
1237
1319
|
const result = await (0, create_dcl_js_1.abapCreateDcl)({ dcl_name, cds_entity, auth_type, auth_mappings, literal_conditions, with_user_aspect, package: pkg, transport_request });
|
|
1238
1320
|
return { content: [{ type: "text", text: result }] };
|
|
@@ -1262,6 +1344,9 @@ server.tool("abap_create_amdp", "Cria uma classe AMDP (ABAP Managed Database Pro
|
|
|
1262
1344
|
package: zod_1.z.string().optional().describe("Pacote SAP. Padrão: '$TMP'."),
|
|
1263
1345
|
transport_request: zod_1.z.string().optional().describe("Ordem de transporte."),
|
|
1264
1346
|
}, async ({ class_name, method_name, method_type, importing: imp, exporting_table, cds_entity, description, package: pkg, transport_request }) => {
|
|
1347
|
+
const blocked = securityGuard("abap_create_amdp", transport_request);
|
|
1348
|
+
if (blocked)
|
|
1349
|
+
return blocked;
|
|
1265
1350
|
try {
|
|
1266
1351
|
const result = await (0, create_amdp_js_1.abapCreateAmdp)({ class_name, method_name, method_type, importing: imp, exporting_table, cds_entity, description, package: pkg, transport_request });
|
|
1267
1352
|
return { content: [{ type: "text", text: result }] };
|
|
@@ -1305,12 +1390,31 @@ server.resource("system-profile", "abap://system-profile", { description: "Perfi
|
|
|
1305
1390
|
}],
|
|
1306
1391
|
};
|
|
1307
1392
|
});
|
|
1393
|
+
// Resource: security-policy
|
|
1394
|
+
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 () => {
|
|
1395
|
+
const policy = (0, security_policy_js_1.getPolicy)();
|
|
1396
|
+
const toolRiskMap = {
|
|
1397
|
+
blocked_tools: policy.always_blocked,
|
|
1398
|
+
environment: policy.environment_role,
|
|
1399
|
+
allowed_operations: policy.allowed_levels,
|
|
1400
|
+
require_transport: policy.require_transport,
|
|
1401
|
+
max_preview_rows: policy.max_preview_rows,
|
|
1402
|
+
};
|
|
1403
|
+
return {
|
|
1404
|
+
contents: [{
|
|
1405
|
+
uri: "abap://security-policy",
|
|
1406
|
+
mimeType: "application/json",
|
|
1407
|
+
text: JSON.stringify(toolRiskMap, null, 2),
|
|
1408
|
+
}],
|
|
1409
|
+
};
|
|
1410
|
+
});
|
|
1308
1411
|
// Inicializa o servidor via stdio (modo Claude Code MCP)
|
|
1309
1412
|
async function main() {
|
|
1310
1413
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
1311
1414
|
await server.connect(transport);
|
|
1312
1415
|
const profile = (0, system_profile_js_1.getProfile)();
|
|
1313
|
-
|
|
1416
|
+
const policy = (0, security_policy_js_1.getPolicy)();
|
|
1417
|
+
process.stderr.write(`abap-mcp-server v2.1.0 iniciado | System: ${profile.system.type} | ABAP: ${profile.system.abapPlatform} | Security: ${policy.environment_role} (${policy.allowed_levels.join("+")})\n`);
|
|
1314
1418
|
}
|
|
1315
1419
|
main().catch((err) => {
|
|
1316
1420
|
process.stderr.write(`Falha ao iniciar servidor: ${err}\n`);
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Security Audit Log — registro de operações de segurança.
|
|
4
|
+
*
|
|
5
|
+
* Registra todas as operações WRITE, CREATE, DESTRUCTIVE e ADMIN,
|
|
6
|
+
* incluindo tentativas bloqueadas pela security policy.
|
|
7
|
+
* Separado do logger de chamadas ADT — este é focado em governança.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.auditLog = auditLog;
|
|
11
|
+
exports.auditAllowed = auditAllowed;
|
|
12
|
+
exports.auditBlocked = auditBlocked;
|
|
13
|
+
exports.getSessionAudit = getSessionAudit;
|
|
14
|
+
exports.getAuditSummary = getAuditSummary;
|
|
15
|
+
exports.readTodayAudit = readTodayAudit;
|
|
16
|
+
const fs_1 = require("fs");
|
|
17
|
+
const path_1 = require("path");
|
|
18
|
+
// ─── Config ───────────────────────────────────────────────────────
|
|
19
|
+
const AUDIT_DIR = process.env.ABAP_AI_AUDIT_DIR || (0, path_1.join)(process.env.HOME || "~", ".abap-ai", "audit");
|
|
20
|
+
const AUDIT_ENABLED = process.env.ABAP_AI_AUDIT !== "false";
|
|
21
|
+
// ─── Estado da sessão ─────────────────────────────────────────────
|
|
22
|
+
const sessionAuditEntries = [];
|
|
23
|
+
// ─── Funções ──────────────────────────────────────────────────────
|
|
24
|
+
function ensureAuditDir() {
|
|
25
|
+
if (!(0, fs_1.existsSync)(AUDIT_DIR)) {
|
|
26
|
+
(0, fs_1.mkdirSync)(AUDIT_DIR, { recursive: true });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function auditFileName() {
|
|
30
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
31
|
+
return (0, path_1.join)(AUDIT_DIR, `security-${date}.jsonl`);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Extrai o hostname do SAP_URL para identificar o sistema no audit.
|
|
35
|
+
*/
|
|
36
|
+
function getSystemId() {
|
|
37
|
+
const url = process.env.SAP_URL || "unknown";
|
|
38
|
+
try {
|
|
39
|
+
return new URL(url).hostname;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return url;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Registra uma entrada no audit log de segurança.
|
|
47
|
+
*/
|
|
48
|
+
function auditLog(entry) {
|
|
49
|
+
const fullEntry = {
|
|
50
|
+
timestamp: new Date().toISOString(),
|
|
51
|
+
system: getSystemId(),
|
|
52
|
+
user: process.env.SAP_USER || "unknown",
|
|
53
|
+
...entry,
|
|
54
|
+
};
|
|
55
|
+
sessionAuditEntries.push(fullEntry);
|
|
56
|
+
if (!AUDIT_ENABLED)
|
|
57
|
+
return;
|
|
58
|
+
try {
|
|
59
|
+
ensureAuditDir();
|
|
60
|
+
(0, fs_1.appendFileSync)(auditFileName(), JSON.stringify(fullEntry) + "\n", "utf-8");
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Audit silencioso — não deve quebrar operações
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Atalho para registrar uma operação permitida.
|
|
68
|
+
*/
|
|
69
|
+
function auditAllowed(tool, riskLevel, environment, params) {
|
|
70
|
+
auditLog({
|
|
71
|
+
tool,
|
|
72
|
+
risk_level: riskLevel,
|
|
73
|
+
environment,
|
|
74
|
+
result: "ALLOWED",
|
|
75
|
+
params,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Atalho para registrar uma operação bloqueada.
|
|
80
|
+
*/
|
|
81
|
+
function auditBlocked(tool, riskLevel, environment, reason, params) {
|
|
82
|
+
auditLog({
|
|
83
|
+
tool,
|
|
84
|
+
risk_level: riskLevel,
|
|
85
|
+
environment,
|
|
86
|
+
result: "BLOCKED",
|
|
87
|
+
reason,
|
|
88
|
+
params,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Retorna as entradas de audit da sessão atual.
|
|
93
|
+
*/
|
|
94
|
+
function getSessionAudit() {
|
|
95
|
+
return [...sessionAuditEntries];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Retorna resumo do audit da sessão para exibição.
|
|
99
|
+
*/
|
|
100
|
+
function getAuditSummary() {
|
|
101
|
+
const allowed = sessionAuditEntries.filter((e) => e.result === "ALLOWED").length;
|
|
102
|
+
const blocked = sessionAuditEntries.filter((e) => e.result === "BLOCKED").length;
|
|
103
|
+
const byTool = {};
|
|
104
|
+
for (const entry of sessionAuditEntries) {
|
|
105
|
+
if (!byTool[entry.tool])
|
|
106
|
+
byTool[entry.tool] = { allowed: 0, blocked: 0 };
|
|
107
|
+
if (entry.result === "ALLOWED")
|
|
108
|
+
byTool[entry.tool].allowed++;
|
|
109
|
+
else
|
|
110
|
+
byTool[entry.tool].blocked++;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
total: sessionAuditEntries.length,
|
|
114
|
+
allowed,
|
|
115
|
+
blocked,
|
|
116
|
+
by_tool: byTool,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Lê o audit log do dia atual.
|
|
121
|
+
*/
|
|
122
|
+
function readTodayAudit() {
|
|
123
|
+
try {
|
|
124
|
+
const file = auditFileName();
|
|
125
|
+
if (!(0, fs_1.existsSync)(file))
|
|
126
|
+
return [];
|
|
127
|
+
const content = (0, fs_1.readFileSync)(file, "utf-8");
|
|
128
|
+
return content
|
|
129
|
+
.split("\n")
|
|
130
|
+
.filter((line) => line.trim())
|
|
131
|
+
.map((line) => JSON.parse(line));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return [];
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Security Policy — controle de acesso por camada de ambiente SAP.
|
|
4
|
+
*
|
|
5
|
+
* Implementa o princípio: "O LKPABAP.ia NUNCA sobrepõe as restrições de
|
|
6
|
+
* segurança do SAP. Em PRD, só leitura. Em QAS, leitura + debug.
|
|
7
|
+
* Em DEV, tudo liberado. abap_release_transport sempre bloqueado."
|
|
8
|
+
*
|
|
9
|
+
* A autorização de dados (S_TADIR, S_DEVELOP, etc.) é 100% delegada ao SAP.
|
|
10
|
+
* Esta camada controla quais CATEGORIAS de operação o MCP server aceita executar.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.getToolRisk = getToolRisk;
|
|
14
|
+
exports.loadPolicy = loadPolicy;
|
|
15
|
+
exports.checkToolAccess = checkToolAccess;
|
|
16
|
+
exports.getEffectiveMaxRows = getEffectiveMaxRows;
|
|
17
|
+
exports.checkTransportRequired = checkTransportRequired;
|
|
18
|
+
exports.getPolicy = getPolicy;
|
|
19
|
+
exports.setPolicy = setPolicy;
|
|
20
|
+
exports.getDefaultPolicy = getDefaultPolicy;
|
|
21
|
+
exports.listEnvironmentRoles = listEnvironmentRoles;
|
|
22
|
+
const fs_1 = require("fs");
|
|
23
|
+
const path_1 = require("path");
|
|
24
|
+
/**
|
|
25
|
+
* Mapa de cada tool para seu nível de risco.
|
|
26
|
+
* Tools não listadas aqui são consideradas READ (safe by default).
|
|
27
|
+
*/
|
|
28
|
+
const TOOL_RISK = {
|
|
29
|
+
// READ — sem guard extra (default para tools não listadas)
|
|
30
|
+
abap_read: "READ",
|
|
31
|
+
abap_search: "READ",
|
|
32
|
+
abap_where_used: "READ",
|
|
33
|
+
abap_check: "READ",
|
|
34
|
+
abap_object_structure: "READ",
|
|
35
|
+
abap_data_preview: "READ",
|
|
36
|
+
abap_sql_console: "READ",
|
|
37
|
+
abap_element_info: "READ",
|
|
38
|
+
abap_pretty_printer: "READ",
|
|
39
|
+
abap_unit_test: "READ",
|
|
40
|
+
abap_atc_check: "READ",
|
|
41
|
+
abap_package_contents: "READ",
|
|
42
|
+
abap_object_versions: "READ",
|
|
43
|
+
abap_function_group: "READ",
|
|
44
|
+
abap_message_class: "READ",
|
|
45
|
+
abap_doc: "READ",
|
|
46
|
+
abap_code_completion: "READ",
|
|
47
|
+
abap_navigate: "READ",
|
|
48
|
+
abap_call_hierarchy: "READ",
|
|
49
|
+
abap_code_coverage: "READ",
|
|
50
|
+
abap_transport_contents: "READ",
|
|
51
|
+
abap_cds_annotations: "READ",
|
|
52
|
+
abap_cds_dependencies: "READ",
|
|
53
|
+
abap_annotation_propagation: "READ",
|
|
54
|
+
abap_enhancement_spot: "READ",
|
|
55
|
+
abap_lock_object: "READ",
|
|
56
|
+
abap_auth_object: "READ",
|
|
57
|
+
abap_number_range: "READ",
|
|
58
|
+
abap_repository_tree: "READ",
|
|
59
|
+
abap_discovery: "READ",
|
|
60
|
+
abap_released_apis: "READ",
|
|
61
|
+
abap_object_documentation: "READ",
|
|
62
|
+
abap_system_info: "READ",
|
|
63
|
+
abap_list_transports: "READ",
|
|
64
|
+
abap_knowledge: "READ",
|
|
65
|
+
abap_traces: "READ",
|
|
66
|
+
abap_breakpoints: "READ",
|
|
67
|
+
abapgit_repos: "READ",
|
|
68
|
+
// WRITE — modifica objetos existentes
|
|
69
|
+
abap_write: "WRITE",
|
|
70
|
+
abap_activate: "WRITE",
|
|
71
|
+
abap_refactor_rename: "WRITE",
|
|
72
|
+
abap_extract_method: "WRITE",
|
|
73
|
+
abap_quick_fix: "WRITE",
|
|
74
|
+
abap_publish_binding: "WRITE",
|
|
75
|
+
abap_deploy_bsp: "WRITE",
|
|
76
|
+
abap_assign_transport: "WRITE",
|
|
77
|
+
abapgit_pull: "WRITE",
|
|
78
|
+
abapgit_stage: "WRITE",
|
|
79
|
+
// CREATE — cria objetos novos no SAP
|
|
80
|
+
abap_create: "CREATE",
|
|
81
|
+
abap_create_transport: "CREATE",
|
|
82
|
+
abap_create_dcl: "CREATE",
|
|
83
|
+
abap_create_amdp: "CREATE",
|
|
84
|
+
abap_scaffold_rap: "CREATE",
|
|
85
|
+
// WRITE — delete segue a mesma regra de escrita (permitido em DEV, bloqueado em QAS/PRD)
|
|
86
|
+
abap_delete: "WRITE",
|
|
87
|
+
// ADMIN — operações administrativas de alto impacto (sempre bloqueado)
|
|
88
|
+
abap_release_transport: "ADMIN",
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Retorna o nível de risco de uma tool.
|
|
92
|
+
*/
|
|
93
|
+
function getToolRisk(toolName) {
|
|
94
|
+
return TOOL_RISK[toolName] ?? "READ";
|
|
95
|
+
}
|
|
96
|
+
// ─── Policies padrão por ambiente ─────────────────────────────────
|
|
97
|
+
const POLICY_DEVELOPMENT = {
|
|
98
|
+
environment_role: "DEVELOPMENT",
|
|
99
|
+
allowed_levels: ["READ", "WRITE", "CREATE"],
|
|
100
|
+
always_blocked: ["abap_release_transport"],
|
|
101
|
+
require_transport: false,
|
|
102
|
+
max_preview_rows: 0,
|
|
103
|
+
};
|
|
104
|
+
const POLICY_QUALITY = {
|
|
105
|
+
environment_role: "QUALITY",
|
|
106
|
+
allowed_levels: ["READ"],
|
|
107
|
+
always_blocked: [
|
|
108
|
+
"abap_release_transport",
|
|
109
|
+
"abap_delete",
|
|
110
|
+
"abap_scaffold_rap",
|
|
111
|
+
"abap_create",
|
|
112
|
+
"abap_create_dcl",
|
|
113
|
+
"abap_create_amdp",
|
|
114
|
+
"abap_write",
|
|
115
|
+
"abap_activate",
|
|
116
|
+
"abap_refactor_rename",
|
|
117
|
+
"abap_extract_method",
|
|
118
|
+
"abap_quick_fix",
|
|
119
|
+
"abap_publish_binding",
|
|
120
|
+
"abap_deploy_bsp",
|
|
121
|
+
"abapgit_pull",
|
|
122
|
+
],
|
|
123
|
+
require_transport: true,
|
|
124
|
+
max_preview_rows: 100,
|
|
125
|
+
};
|
|
126
|
+
const POLICY_PRODUCTION = {
|
|
127
|
+
environment_role: "PRODUCTION",
|
|
128
|
+
allowed_levels: ["READ"],
|
|
129
|
+
always_blocked: [
|
|
130
|
+
"abap_release_transport",
|
|
131
|
+
"abap_delete",
|
|
132
|
+
"abap_scaffold_rap",
|
|
133
|
+
"abap_create",
|
|
134
|
+
"abap_create_dcl",
|
|
135
|
+
"abap_create_amdp",
|
|
136
|
+
"abap_create_transport",
|
|
137
|
+
"abap_write",
|
|
138
|
+
"abap_activate",
|
|
139
|
+
"abap_refactor_rename",
|
|
140
|
+
"abap_extract_method",
|
|
141
|
+
"abap_quick_fix",
|
|
142
|
+
"abap_publish_binding",
|
|
143
|
+
"abap_deploy_bsp",
|
|
144
|
+
"abapgit_pull",
|
|
145
|
+
"abapgit_stage",
|
|
146
|
+
"abap_assign_transport",
|
|
147
|
+
],
|
|
148
|
+
require_transport: true,
|
|
149
|
+
max_preview_rows: 50,
|
|
150
|
+
};
|
|
151
|
+
const DEFAULT_POLICIES = {
|
|
152
|
+
DEVELOPMENT: POLICY_DEVELOPMENT,
|
|
153
|
+
QUALITY: POLICY_QUALITY,
|
|
154
|
+
PRODUCTION: POLICY_PRODUCTION,
|
|
155
|
+
};
|
|
156
|
+
// ─── Carregamento da policy ───────────────────────────────────────
|
|
157
|
+
/**
|
|
158
|
+
* Caminho do arquivo de policy central (planos Enterprise/Corporate).
|
|
159
|
+
* Gestores podem travar a policy criando este arquivo.
|
|
160
|
+
*/
|
|
161
|
+
const CENTRAL_POLICY_PATH = (0, path_1.join)(process.env.HOME || "~", ".abap-ai", "security-policy.json");
|
|
162
|
+
/**
|
|
163
|
+
* Carrega a policy do ambiente a partir de:
|
|
164
|
+
* 1. Variável de ambiente ABAP_AI_SECURITY_POLICY (JSON inline)
|
|
165
|
+
* 2. Arquivo central ~/.abap-ai/security-policy.json (Enterprise/Corporate)
|
|
166
|
+
* 3. Variável ABAP_AI_ENV_ROLE (DEVELOPMENT | QUALITY | PRODUCTION)
|
|
167
|
+
* 4. Default: DEVELOPMENT
|
|
168
|
+
*/
|
|
169
|
+
function loadPolicy() {
|
|
170
|
+
// 1. JSON inline (env var)
|
|
171
|
+
const policyJson = process.env.ABAP_AI_SECURITY_POLICY;
|
|
172
|
+
if (policyJson) {
|
|
173
|
+
try {
|
|
174
|
+
const parsed = JSON.parse(policyJson);
|
|
175
|
+
return mergeWithDefault(parsed);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// fallthrough
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// 2. Arquivo central (gestores Enterprise/Corporate)
|
|
182
|
+
if ((0, fs_1.existsSync)(CENTRAL_POLICY_PATH)) {
|
|
183
|
+
try {
|
|
184
|
+
const raw = (0, fs_1.readFileSync)(CENTRAL_POLICY_PATH, "utf-8");
|
|
185
|
+
const parsed = JSON.parse(raw);
|
|
186
|
+
return mergeWithDefault(parsed);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// fallthrough
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// 3. Env role simples
|
|
193
|
+
const envRole = (process.env.ABAP_AI_ENV_ROLE || "").toUpperCase();
|
|
194
|
+
if (DEFAULT_POLICIES[envRole]) {
|
|
195
|
+
return deepClone(DEFAULT_POLICIES[envRole]);
|
|
196
|
+
}
|
|
197
|
+
// 4. Default: DEVELOPMENT
|
|
198
|
+
return deepClone(POLICY_DEVELOPMENT);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Verifica se uma tool pode ser executada sob a policy atual.
|
|
202
|
+
*/
|
|
203
|
+
function checkToolAccess(toolName, policy) {
|
|
204
|
+
// 1. abap_release_transport — SEMPRE bloqueado, independente de qualquer config
|
|
205
|
+
if (toolName === "abap_release_transport") {
|
|
206
|
+
return {
|
|
207
|
+
allowed: false,
|
|
208
|
+
reason: `BLOQUEADO: abap_release_transport é sempre bloqueado pelo LKPABAP.ia. `
|
|
209
|
+
+ `Liberar transportes é uma operação irreversível que pode propagar mudanças para ambientes produtivos. `
|
|
210
|
+
+ `Execute esta operação manualmente na transação SE09/SE10 do SAP GUI.`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
// 2. Tool na lista de always_blocked
|
|
214
|
+
if (policy.always_blocked.includes(toolName)) {
|
|
215
|
+
return {
|
|
216
|
+
allowed: false,
|
|
217
|
+
reason: `BLOQUEADO: A tool "${toolName}" não é permitida no ambiente ${policy.environment_role}. `
|
|
218
|
+
+ `Esta restrição é definida pela política de segurança do sistema. `
|
|
219
|
+
+ describeEnvironmentRestriction(policy.environment_role),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
// 3. Nível de risco da tool vs níveis permitidos
|
|
223
|
+
const riskLevel = getToolRisk(toolName);
|
|
224
|
+
if (!policy.allowed_levels.includes(riskLevel)) {
|
|
225
|
+
return {
|
|
226
|
+
allowed: false,
|
|
227
|
+
reason: `BLOQUEADO: A tool "${toolName}" requer nível "${riskLevel}", `
|
|
228
|
+
+ `mas o ambiente ${policy.environment_role} só permite: ${policy.allowed_levels.join(", ")}. `
|
|
229
|
+
+ describeEnvironmentRestriction(policy.environment_role),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
return { allowed: true };
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Retorna o limite de rows para preview/sql considerando a policy.
|
|
236
|
+
* Se a policy define um limite, usa o menor entre o limite da policy e o solicitado.
|
|
237
|
+
*/
|
|
238
|
+
function getEffectiveMaxRows(requestedRows, policy) {
|
|
239
|
+
if (policy.max_preview_rows <= 0)
|
|
240
|
+
return requestedRows;
|
|
241
|
+
return Math.min(requestedRows, policy.max_preview_rows);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Verifica se transport request é obrigatório e não foi fornecido.
|
|
245
|
+
*/
|
|
246
|
+
function checkTransportRequired(toolName, transportRequest, policy) {
|
|
247
|
+
const riskLevel = getToolRisk(toolName);
|
|
248
|
+
if (policy.require_transport
|
|
249
|
+
&& (riskLevel === "WRITE" || riskLevel === "CREATE")
|
|
250
|
+
&& !transportRequest) {
|
|
251
|
+
return {
|
|
252
|
+
allowed: false,
|
|
253
|
+
reason: `BLOQUEADO: O ambiente ${policy.environment_role} exige transport request para operações de ${riskLevel}. `
|
|
254
|
+
+ `Informe o parâmetro transport_request (ex: "DEVK900123"). `
|
|
255
|
+
+ `Use abap_list_transports ou abap_create_transport para obter um.`,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return { allowed: true };
|
|
259
|
+
}
|
|
260
|
+
// ─── Singleton ────────────────────────────────────────────────────
|
|
261
|
+
let currentPolicy = null;
|
|
262
|
+
/**
|
|
263
|
+
* Retorna a policy atual. Carrega do ambiente/arquivo na primeira chamada.
|
|
264
|
+
*/
|
|
265
|
+
function getPolicy() {
|
|
266
|
+
if (!currentPolicy) {
|
|
267
|
+
currentPolicy = loadPolicy();
|
|
268
|
+
}
|
|
269
|
+
return currentPolicy;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Força uma policy específica (usado em testes ou override programático).
|
|
273
|
+
*/
|
|
274
|
+
function setPolicy(policy) {
|
|
275
|
+
currentPolicy = policy;
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Retorna a policy padrão para um environment role.
|
|
279
|
+
*/
|
|
280
|
+
function getDefaultPolicy(role) {
|
|
281
|
+
return deepClone(DEFAULT_POLICIES[role] || POLICY_DEVELOPMENT);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Lista os environment roles disponíveis.
|
|
285
|
+
*/
|
|
286
|
+
function listEnvironmentRoles() {
|
|
287
|
+
return ["DEVELOPMENT", "QUALITY", "PRODUCTION"];
|
|
288
|
+
}
|
|
289
|
+
// ─── Helpers ──────────────────────────────────────────────────────
|
|
290
|
+
function describeEnvironmentRestriction(role) {
|
|
291
|
+
switch (role) {
|
|
292
|
+
case "PRODUCTION":
|
|
293
|
+
return "Em ambientes PRODUTIVOS, o LKPABAP.ia opera somente em modo leitura. "
|
|
294
|
+
+ "Qualquer modificação deve ser feita via transporte a partir do ambiente de desenvolvimento.";
|
|
295
|
+
case "QUALITY":
|
|
296
|
+
return "Em ambientes de QUALIDADE (QAS), o LKPABAP.ia opera somente em modo leitura. "
|
|
297
|
+
+ "Modificações devem ser transportadas a partir do ambiente de desenvolvimento.";
|
|
298
|
+
case "DEVELOPMENT":
|
|
299
|
+
return ""; // DEV não tem restrição descritiva
|
|
300
|
+
default:
|
|
301
|
+
return "";
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function mergeWithDefault(partial) {
|
|
305
|
+
const role = partial.environment_role || "DEVELOPMENT";
|
|
306
|
+
const base = deepClone(DEFAULT_POLICIES[role] || POLICY_DEVELOPMENT);
|
|
307
|
+
return {
|
|
308
|
+
environment_role: partial.environment_role ?? base.environment_role,
|
|
309
|
+
allowed_levels: partial.allowed_levels ?? base.allowed_levels,
|
|
310
|
+
always_blocked: [
|
|
311
|
+
...new Set([
|
|
312
|
+
...base.always_blocked,
|
|
313
|
+
...(partial.always_blocked ?? []),
|
|
314
|
+
]),
|
|
315
|
+
],
|
|
316
|
+
require_transport: partial.require_transport ?? base.require_transport,
|
|
317
|
+
max_preview_rows: partial.max_preview_rows ?? base.max_preview_rows,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
function deepClone(obj) {
|
|
321
|
+
return JSON.parse(JSON.stringify(obj));
|
|
322
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linkup-ai/abap-ai",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "LKPABAP.ia — AI-powered ABAP development tools for SAP S/4HANA via ADT REST API",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"README.md"
|
|
12
12
|
],
|
|
13
13
|
"publishConfig": {
|
|
14
|
-
"access": "
|
|
14
|
+
"access": "public"
|
|
15
15
|
},
|
|
16
16
|
"license": "SEE LICENSE IN LICENSE",
|
|
17
17
|
"keywords": [
|