@dozimple/abap-adt 1.0.0 → 1.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/CHANGELOG.md +89 -0
- package/README.es.md +8 -6
- package/README.md +8 -6
- package/SECURITY.md +12 -11
- package/dist/core/atc.js +2 -1
- package/dist/core/audit.js +81 -12
- package/dist/core/auditkey.js +85 -0
- package/dist/core/catalog.en.js +1 -0
- package/dist/core/catalog.js +1 -0
- package/dist/core/config.js +89 -1
- package/dist/core/confirm.js +19 -7
- package/dist/core/connection.js +46 -1
- package/dist/core/credentials.js +10 -3
- package/dist/core/datapolicy.js +15 -7
- package/dist/core/errors.js +34 -6
- package/dist/core/feeds.js +11 -1
- package/dist/core/objects.js +10 -0
- package/dist/core/policy.js +10 -1
- package/dist/core/registry.js +97 -15
- package/dist/index.js +8 -2
- package/dist/scripts/audit-verify.js +25 -6
- package/dist/tools/core/api_release_state.js +5 -0
- package/dist/tools/core/atc_quickfix.js +2 -2
- package/dist/tools/core/ddic_type_info.js +2 -0
- package/dist/tools/core/dumps.js +2 -1
- package/dist/tools/core/gateway_errors.js +2 -1
- package/dist/tools/core/get_source.js +2 -2
- package/dist/tools/core/revert_source.js +111 -0
- package/dist/tools/core/run_atc.js +9 -4
- package/dist/tools/core/run_unit_tests.js +6 -1
- package/dist/tools/core/sql_query.js +18 -2
- package/dist/tools/core/support.js +6 -3
- package/dist/tools/core/syntax_check.js +17 -2
- package/dist/tools/core/table_contents.js +43 -7
- package/dist/tools/core/text_elements.js +11 -3
- package/dist/tools/core/transport_diff.js +10 -1
- package/dist/tools/core/where_used.js +6 -1
- package/dist/tools/core/write_source.js +14 -2
- package/dist/tools/docs/docs.js +8 -0
- package/dist/tools/local/sap_systems.js +4 -0
- package/dist/tools/transport-risk/_service.js +7 -2
- package/docs/THREAT_MODEL.md +4 -3
- package/docs/TOOLS.md +44 -2
- package/package.json +5 -3
|
@@ -16,6 +16,13 @@ export default defineTool({
|
|
|
16
16
|
source: z.string().optional().describe("Fuente completa a comprobar (sin guardar)"),
|
|
17
17
|
main_program: z.string().optional().describe("URI del programa principal, solo para includes ambiguos"),
|
|
18
18
|
},
|
|
19
|
+
output: {
|
|
20
|
+
object: z.string(),
|
|
21
|
+
checked: z.enum(["proposed", "saved"]).describe("proposed = la fuente pasada sin guardar; saved = lo último guardado"),
|
|
22
|
+
errors: z.number().int(),
|
|
23
|
+
warnings: z.number().int(),
|
|
24
|
+
messages: z.array(z.object({ severity: z.string(), line: z.number(), offset: z.number(), text: z.string(), uri: z.string() })),
|
|
25
|
+
},
|
|
19
26
|
async run({ object_name, object_type, include, source, main_program }, { sap }) {
|
|
20
27
|
const c = await sap.adt();
|
|
21
28
|
const obj = await resolveObject(c, object_name, object_type);
|
|
@@ -23,12 +30,20 @@ export default defineTool({
|
|
|
23
30
|
const content = source ?? (await c.getObjectSource(url, { version: "inactive" }));
|
|
24
31
|
const msgs = await syntaxCheck(c, obj, url, content, main_program);
|
|
25
32
|
const what = source ? "fuente propuesta (no guardada)" : "última versión guardada";
|
|
26
|
-
if (!msgs.length)
|
|
27
|
-
return `${obj.name}: sin errores ni avisos de sintaxis (${what}, comprobado por SAP).`;
|
|
28
33
|
const errs = msgs.filter(isError).length;
|
|
34
|
+
const structured = {
|
|
35
|
+
object: obj.name,
|
|
36
|
+
checked: source ? "proposed" : "saved",
|
|
37
|
+
errors: errs,
|
|
38
|
+
warnings: msgs.length - errs,
|
|
39
|
+
messages: msgs.map((m) => ({ severity: m.severity ?? "", line: m.line, offset: m.offset, text: m.text, uri: m.uri })),
|
|
40
|
+
};
|
|
41
|
+
if (!msgs.length)
|
|
42
|
+
return { text: `${obj.name}: sin errores ni avisos de sintaxis (${what}, comprobado por SAP).`, structured };
|
|
29
43
|
return {
|
|
30
44
|
text: `${obj.name}: ${errs} errores, ${msgs.length - errs} avisos (${what}).\n\n${renderSyntax(msgs)}`,
|
|
31
45
|
isError: errs > 0,
|
|
46
|
+
structured,
|
|
32
47
|
};
|
|
33
48
|
},
|
|
34
49
|
});
|
|
@@ -5,16 +5,52 @@ import { tsv } from "../../core/output.js";
|
|
|
5
5
|
import { defineTool } from "../../core/tool.js";
|
|
6
6
|
const NAME_RE = /^[A-Z0-9_/]+$/;
|
|
7
7
|
/**
|
|
8
|
-
* WHERE y ORDER BY se concatenan al SELECT:
|
|
9
|
-
*
|
|
8
|
+
* WHERE y ORDER BY se concatenan al SELECT, así que se validan por LISTA BLANCA: el fragmento se trocea y cada
|
|
9
|
+
* pieza tiene que ser una de las formas esperadas de una condición sobre columnas. Lo que no encaje (otra cláusula,
|
|
10
|
+
* subconsulta, comentario, «;», comillas dobles, host variables…) se rechaza, aunque una versión futura de ABAP SQL
|
|
11
|
+
* lo admitiera. Para consultas más ricas está sql_query.
|
|
10
12
|
*/
|
|
13
|
+
const CONDITION_WORDS = new Set(["AND", "OR", "NOT", "LIKE", "IN", "BETWEEN", "IS", "NULL", "INITIAL", "ESCAPE", "EQ", "NE", "LT", "GT", "LE", "GE"]);
|
|
14
|
+
/** Palabras de cláusula o de sentencia: nunca pueden aparecer como «columna» en un filtro. */
|
|
15
|
+
const CLAUSE_WORDS = new Set([
|
|
16
|
+
"SELECT", "FROM", "WHERE", "JOIN", "INNER", "OUTER", "LEFT", "RIGHT", "CROSS", "UNION", "INTERSECT", "EXCEPT", "INTO",
|
|
17
|
+
"GROUP", "ORDER", "BY", "HAVING", "UP", "TO", "ROWS", "CLIENT", "SPECIFIED", "USING", "EXISTS", "ALL", "ANY", "SOME",
|
|
18
|
+
"FOR", "UPDATE", "DELETE", "INSERT", "MODIFY", "DISTINCT", "SINGLE", "WITH", "AS", "CASE", "WHEN", "THEN", "ELSE",
|
|
19
|
+
"END", "BYPASSING", "BUFFER", "CONNECTION", "APPENDING", "PACKAGE", "SIZE", "OFFSET", "FIELDS",
|
|
20
|
+
]);
|
|
21
|
+
const TOKEN = /\s*(?:('(?:[^']|'')*')|(\d+(?:\.\d+)?)|([A-Za-z_/][A-Za-z0-9_/~]*)|(<>|<=|>=|!=|=|<|>)|([(),]))/y;
|
|
11
22
|
export function assertSafeFragment(frag, label) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
23
|
+
const fail = (why) => {
|
|
24
|
+
throw new ToolError("INPUT", `${label}: ${why}. Solo condiciones sobre columnas (columna, operador, literal, AND/OR/NOT, LIKE, IN, BETWEEN, IS NULL/INITIAL, paréntesis); para más, sql_query.`);
|
|
25
|
+
};
|
|
26
|
+
let depth = 0;
|
|
27
|
+
TOKEN.lastIndex = 0;
|
|
28
|
+
let pos = 0;
|
|
29
|
+
while (pos < frag.length) {
|
|
30
|
+
if (!frag.slice(pos).trim())
|
|
31
|
+
break;
|
|
32
|
+
TOKEN.lastIndex = pos;
|
|
33
|
+
const m = TOKEN.exec(frag);
|
|
34
|
+
if (!m)
|
|
35
|
+
fail(`«${frag.slice(pos).trim().slice(0, 12)}…» no es parte de una condición`);
|
|
36
|
+
pos = TOKEN.lastIndex;
|
|
37
|
+
const [, lit, , word, , punct] = m;
|
|
38
|
+
if (lit !== undefined && lit.length < 2)
|
|
39
|
+
fail("comilla sin cerrar");
|
|
40
|
+
if (word !== undefined) {
|
|
41
|
+
const w = word.toUpperCase();
|
|
42
|
+
if (CLAUSE_WORDS.has(w))
|
|
43
|
+
fail(`«${word}» es una cláusula, no una columna`);
|
|
44
|
+
if (!CONDITION_WORDS.has(w) && !/^[A-Za-z_/][A-Za-z0-9_/]*(~[A-Za-z0-9_/]+)?$/.test(word))
|
|
45
|
+
fail(`«${word}» no es un nombre de columna`);
|
|
46
|
+
}
|
|
47
|
+
if (punct === "(")
|
|
48
|
+
depth++;
|
|
49
|
+
if (punct === ")" && --depth < 0)
|
|
50
|
+
fail("paréntesis sin abrir");
|
|
17
51
|
}
|
|
52
|
+
if (depth !== 0)
|
|
53
|
+
fail("paréntesis sin cerrar");
|
|
18
54
|
}
|
|
19
55
|
export default defineTool({
|
|
20
56
|
name: "table_contents",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { textElementsUrl } from "abap-adt-api";
|
|
3
|
+
import { stateOf } from "../../core/confirm.js";
|
|
3
4
|
import { ToolError } from "../../core/errors.js";
|
|
4
5
|
import { resolveObject, TYPE_HELP } from "../../core/objects.js";
|
|
5
6
|
import { assertTrkorr } from "../../core/policy.js";
|
|
@@ -20,6 +21,8 @@ async function mergeTexts(c, obj, category, elements) {
|
|
|
20
21
|
}
|
|
21
22
|
return { current, merged };
|
|
22
23
|
}
|
|
24
|
+
/** Huella del conjunto de textos tal como está en SAP (orden estable), para comparar vista previa y escritura. */
|
|
25
|
+
const textsState = (ts) => stateOf(JSON.stringify(ts.map((t) => [t.id.toUpperCase(), t.text, t.maxLength ?? null]).sort((a, b) => String(a[0]).localeCompare(String(b[0])))));
|
|
23
26
|
const read = defineTool({
|
|
24
27
|
name: "text_elements",
|
|
25
28
|
title: "Símbolos de texto y textos de selección",
|
|
@@ -72,14 +75,15 @@ const write = defineTool({
|
|
|
72
75
|
return ` ~ ${id} «${prev.text}» → «${now.text}» [máx ${now.maxLength}]`;
|
|
73
76
|
});
|
|
74
77
|
const warn = requested ? await transportWarnings(sap, requested, system.user) : [];
|
|
75
|
-
|
|
78
|
+
const text = [
|
|
76
79
|
...(warn.length ? [`AVISOS DE LA ORDEN ${requested}:`, ...warn.map((w) => " ⚠ " + w), ""] : []),
|
|
77
80
|
`${obj.name} (${obj.type}) · ${category} · orden ${requested ?? "ninguna indicada"}`,
|
|
78
81
|
`Quedarán ${merged.size} (hoy ${current.length}); no se borra ninguno.`,
|
|
79
82
|
...lines,
|
|
80
|
-
]
|
|
83
|
+
];
|
|
84
|
+
return { text: text.join("\n"), state: textsState(current) };
|
|
81
85
|
},
|
|
82
|
-
async run({ object_name, object_type, category, elements, transport }, { sap }) {
|
|
86
|
+
async run({ object_name, object_type, category, elements, transport }, { sap, confirmedState }) {
|
|
83
87
|
const requested = transport ? assertTrkorr(transport) : undefined;
|
|
84
88
|
const c = await sap.adt();
|
|
85
89
|
const obj = await resolveObject(c, object_name, object_type);
|
|
@@ -88,6 +92,10 @@ const write = defineTool({
|
|
|
88
92
|
const res = await sap.stateful(async (s) => {
|
|
89
93
|
const lock = await s.lock(obj.uri);
|
|
90
94
|
try {
|
|
95
|
+
// Igual que write_source: bajo el bloqueo, los textos tienen que seguir como en la vista previa.
|
|
96
|
+
if (confirmedState && textsState((await s.getTextElements(url, category)).textElements) !== confirmedState) {
|
|
97
|
+
return { ok: false, reason: `No se escribió nada: los ${category} de ${obj.name} cambiaron en SAP después de la vista previa. Pide una vista previa nueva.` };
|
|
98
|
+
}
|
|
91
99
|
const parent = lock.CORRNR ? (await orderHeaders(sap, [lock.CORRNR])).get(lock.CORRNR)?.parent : undefined;
|
|
92
100
|
const d = decideTransport(lock, requested, parent);
|
|
93
101
|
if (!d.ok)
|
|
@@ -22,6 +22,7 @@ async function pool(items, n, fn) {
|
|
|
22
22
|
}
|
|
23
23
|
export default defineTool({
|
|
24
24
|
name: "transport_diff",
|
|
25
|
+
timeoutMs: 180_000,
|
|
25
26
|
title: "Qué cambió una orden (diff de código)",
|
|
26
27
|
description: "Revisión de código de una orden: por cada objeto con fuente (programas, includes, clases, interfaces, FM, CDS) " +
|
|
27
28
|
"compara la versión grabada con esa orden (o sus tareas) contra la versión anterior, y muestra el diff unificado. " +
|
|
@@ -36,7 +37,8 @@ export default defineTool({
|
|
|
36
37
|
max_objects: z.number().int().min(1).max(60).default(20),
|
|
37
38
|
max_diff_lines: z.number().int().min(20).max(3000).default(300).describe("Tope de líneas de diff por objeto"),
|
|
38
39
|
},
|
|
39
|
-
async run({ transport, objects, context, summary_only, max_objects, max_diff_lines },
|
|
40
|
+
async run({ transport, objects, context, summary_only, max_objects, max_diff_lines }, ctx) {
|
|
41
|
+
const { sap } = ctx;
|
|
40
42
|
const tr = assertTrkorr(transport);
|
|
41
43
|
const heads = await orderHeaders(sap, [tr]);
|
|
42
44
|
const head = heads.get(tr);
|
|
@@ -51,7 +53,11 @@ export default defineTool({
|
|
|
51
53
|
const selected = source.filter((s) => !wanted || wanted.includes(s.name.toUpperCase()));
|
|
52
54
|
const shown = selected.slice(0, max_objects);
|
|
53
55
|
const c = await sap.adt();
|
|
56
|
+
let done = 0;
|
|
57
|
+
ctx.progress?.(`Comparando ${shown.length} objetos de ${root}…`, 0, shown.length);
|
|
54
58
|
const blocks = await pool(shown, 4, async (ref) => {
|
|
59
|
+
// Cancelación entre objetos: el que está en curso termina, el siguiente ya no empieza.
|
|
60
|
+
ctx.signal?.throwIfAborted();
|
|
55
61
|
try {
|
|
56
62
|
const res = await resolveRef(c, ref);
|
|
57
63
|
if (!res)
|
|
@@ -91,6 +97,9 @@ export default defineTool({
|
|
|
91
97
|
throw te;
|
|
92
98
|
return { name: ref.name, text: `■ ${ref.name}: no se pudo comparar (${te.kind}: ${te.message})`, added: 0, removed: 0 };
|
|
93
99
|
}
|
|
100
|
+
finally {
|
|
101
|
+
ctx.progress?.(`${++done} de ${shown.length} objetos comparados`, done, shown.length);
|
|
102
|
+
}
|
|
94
103
|
});
|
|
95
104
|
const tot = blocks.reduce((a, b) => ({ added: a.added + b.added, removed: a.removed + b.removed }), { added: 0, removed: 0 });
|
|
96
105
|
const out = [
|
|
@@ -4,6 +4,7 @@ import { tsv } from "../../core/output.js";
|
|
|
4
4
|
import { defineTool } from "../../core/tool.js";
|
|
5
5
|
export default defineTool({
|
|
6
6
|
name: "where_used",
|
|
7
|
+
timeoutMs: 120_000,
|
|
7
8
|
title: "Dónde se usa",
|
|
8
9
|
description: "Lista de uso (where-used) de un objeto: quién lo referencia, con paquete y responsable. Con snippets=true añade " +
|
|
9
10
|
"las líneas de código de cada uso (más lento). Ojo: no ve usos dinámicos ni exits que no declaran tipos.",
|
|
@@ -15,10 +16,13 @@ export default defineTool({
|
|
|
15
16
|
max_results: z.number().int().min(1).max(1000).default(100),
|
|
16
17
|
snippets: z.boolean().default(false),
|
|
17
18
|
},
|
|
18
|
-
async run({ object_name, object_type, max_results, snippets },
|
|
19
|
+
async run({ object_name, object_type, max_results, snippets }, ctx) {
|
|
20
|
+
const { sap } = ctx;
|
|
19
21
|
const c = await sap.adt();
|
|
20
22
|
const obj = await resolveObject(c, object_name, object_type);
|
|
23
|
+
ctx.progress?.(`Buscando usos de ${obj.name} (en objetos muy usados tarda hasta un minuto)…`);
|
|
21
24
|
const refs = (await c.usageReferences(obj.uri)).filter((r) => r.isResult);
|
|
25
|
+
ctx.signal?.throwIfAborted();
|
|
22
26
|
if (!refs.length)
|
|
23
27
|
return `${obj.name} (${obj.type}): el where-used se ejecutó y no encontró usos estáticos.`;
|
|
24
28
|
const shown = refs.slice(0, max_results);
|
|
@@ -27,6 +31,7 @@ export default defineTool({
|
|
|
27
31
|
"\n\n" +
|
|
28
32
|
tsv(["nombre", "tipo", "paquete", "responsable", "uso"], shown.map((r) => [r["adtcore:name"], r["adtcore:type"], r.packageRef?.["adtcore:name"], r["adtcore:responsible"], r.usageInformation]));
|
|
29
33
|
if (snippets) {
|
|
34
|
+
ctx.progress?.(`${refs.length} usos; leyendo fragmentos de los primeros ${Math.min(shown.length, 30)}…`);
|
|
30
35
|
const sn = await c.usageReferenceSnippets(shown.slice(0, 30));
|
|
31
36
|
const lines = sn.flatMap((s) => s.snippets.map((x) => `${s.objectIdentifier} L${x.uri?.start?.line ?? "?"}: ${x.content.trim()}`));
|
|
32
37
|
out += `\n\nFragmentos (hasta 30 objetos):\n${lines.join("\n")}`;
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { activateObject } from "../../core/activation.js";
|
|
3
3
|
import { isError, renderSyntax, syntaxCheck } from "../../core/checks.js";
|
|
4
4
|
import { CLASS_INCLUDES, resolveObject, sourceUrl, TYPE_HELP } from "../../core/objects.js";
|
|
5
|
+
import { stateOf } from "../../core/confirm.js";
|
|
5
6
|
import { diffLines, unified } from "../../core/diff.js";
|
|
6
7
|
import { assertTrkorr } from "../../core/policy.js";
|
|
7
8
|
import { decideTransport, orderHeaders, transportWarnings } from "../../core/transport.js";
|
|
@@ -55,9 +56,10 @@ export default defineTool({
|
|
|
55
56
|
out.push(lines.slice(0, 400).join("\n") + (lines.length > 400 ? `\n[… ${lines.length - 400} líneas más de diff]` : ""));
|
|
56
57
|
}
|
|
57
58
|
out.push("", "Si el objeto está bloqueado en otra orden, la escritura se detendrá sin guardar (compruébalo antes con edit_preflight).");
|
|
58
|
-
|
|
59
|
+
// La huella de la fuente mostrada viaja con la confirmación: si cambia antes de escribir, no se escribe.
|
|
60
|
+
return { text: out.join("\n"), state: stateOf(current) };
|
|
59
61
|
},
|
|
60
|
-
async run({ object_name, object_type, include, source, transport, activate, skip_syntax_check }, { sap }) {
|
|
62
|
+
async run({ object_name, object_type, include, source, transport, activate, skip_syntax_check }, { sap, confirmedState }) {
|
|
61
63
|
const requested = transport ? assertTrkorr(transport) : undefined;
|
|
62
64
|
const c = await sap.adt();
|
|
63
65
|
const obj = await resolveObject(c, object_name, object_type);
|
|
@@ -71,6 +73,16 @@ export default defineTool({
|
|
|
71
73
|
const saved = await sap.stateful(async (s) => {
|
|
72
74
|
const lock = await s.lock(obj.uri);
|
|
73
75
|
try {
|
|
76
|
+
// Con el objeto ya bloqueado nadie más puede guardarlo: es el momento de comprobar que sigue siendo la
|
|
77
|
+
// versión que el usuario vio en la vista previa. Si otra persona guardó entretanto, sus cambios se
|
|
78
|
+
// perderían en silencio: no se escribe.
|
|
79
|
+
if (confirmedState && stateOf(await s.getObjectSource(url)) !== confirmedState) {
|
|
80
|
+
return {
|
|
81
|
+
ok: false,
|
|
82
|
+
reason: `No se escribió nada: ${obj.name} cambió en SAP después de la vista previa (alguien lo guardó entretanto). ` +
|
|
83
|
+
`Pide una vista previa nueva: el diff que se aprobó ya no corresponde a lo que hay.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
74
86
|
const parent = lock.CORRNR ? (await orderHeaders(sap, [lock.CORRNR])).get(lock.CORRNR)?.parent : undefined;
|
|
75
87
|
const d = decideTransport(lock, requested, parent);
|
|
76
88
|
if (!d.ok)
|
package/dist/tools/docs/docs.js
CHANGED
|
@@ -59,6 +59,14 @@ export function assertDocId(id, cfg) {
|
|
|
59
59
|
catch {
|
|
60
60
|
throw new ToolError("INPUT", `«${id}» no es un id de documento válido.`);
|
|
61
61
|
}
|
|
62
|
+
// La forma se comprueba sobre el id YA decodificado: un «%2F» no puede colar una URL de otro host.
|
|
63
|
+
const shapes = [
|
|
64
|
+
/^sap-help-[A-Za-z0-9_-]+(~[A-Za-z0-9._-]+)?$/,
|
|
65
|
+
/^community-\d+$/,
|
|
66
|
+
/^community-url-https:\/\/community\.sap\.com\/[A-Za-z0-9._~\/?=&#%+-]*$/,
|
|
67
|
+
];
|
|
68
|
+
if (!shapes.some((re) => re.test(decoded)))
|
|
69
|
+
throw new ToolError("INPUT", `«${id}» no tiene la forma de un documento de SAP Help o SAP Community.`);
|
|
62
70
|
assertPublicQuery(decoded.replace(/[-/:.?=&#]/g, " "), cfg, sc.blockTerms);
|
|
63
71
|
return;
|
|
64
72
|
}
|
|
@@ -30,6 +30,10 @@ export default defineTool({
|
|
|
30
30
|
if (!check)
|
|
31
31
|
continue;
|
|
32
32
|
const sap = pool.get(s);
|
|
33
|
+
if (sap.circuitOpenFor()) {
|
|
34
|
+
out.push(" circuito abierto por fallos de red seguidos: se cierra y se reintenta ahora");
|
|
35
|
+
sap.resetCircuit();
|
|
36
|
+
}
|
|
33
37
|
try {
|
|
34
38
|
const caps = await sap.capabilities(refresh_discovery);
|
|
35
39
|
const rel = await sap.release();
|
|
@@ -3,7 +3,7 @@ import axios from "axios";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { tlsOptions } from "../../core/connection.js";
|
|
5
5
|
import { getPassword } from "../../core/credentials.js";
|
|
6
|
-
import { ToolError } from "../../core/errors.js";
|
|
6
|
+
import { sanitizeMessage, ToolError } from "../../core/errors.js";
|
|
7
7
|
/**
|
|
8
8
|
* DoZimple Transport Risk: analizador de riesgo de transportes instalado en el
|
|
9
9
|
* sistema de desarrollo del cliente (nodo ICF /sap/bc/zdz_risk, clase
|
|
@@ -31,6 +31,11 @@ export async function callRiskService(system, params) {
|
|
|
31
31
|
auth: { username: system.user, password: await getPassword(system) },
|
|
32
32
|
httpsAgent: tlsOptions(system).httpsAgent ?? new https.Agent(),
|
|
33
33
|
timeout: 180_000,
|
|
34
|
+
// Un servicio ICF de solo lectura no redirige: sin redirecciones, las credenciales Basic nunca viajan a otra URL
|
|
35
|
+
// (sin depender de que una librería transitiva las quite al cambiar de host).
|
|
36
|
+
maxRedirects: 0,
|
|
37
|
+
// Tope de tamaño de la respuesta antes de procesarla (un sistema averiado no agota la memoria del proceso).
|
|
38
|
+
maxContentLength: 50 * 1024 * 1024,
|
|
34
39
|
// Los 4xx/5xx traen un JSON con la causa: se lee en vez de lanzar.
|
|
35
40
|
validateStatus: () => true,
|
|
36
41
|
responseType: "text",
|
|
@@ -56,7 +61,7 @@ export async function callRiskService(system, params) {
|
|
|
56
61
|
return { text: `El servicio ${RISK_SERVICE_PATH} no existe en este sistema.\n${BACKEND_HINT}`, isError: true };
|
|
57
62
|
if (response.status !== 200) {
|
|
58
63
|
const detail = json && typeof json === "object" && "error" in json ? String(json.error) : body;
|
|
59
|
-
return { text: `El servicio de riesgo respondió HTTP ${response.status}: ${detail}`, isError: true };
|
|
64
|
+
return { text: `El servicio de riesgo respondió HTTP ${response.status}: ${sanitizeMessage(detail, 300)}`, isError: true };
|
|
60
65
|
}
|
|
61
66
|
return body;
|
|
62
67
|
}
|
package/docs/THREAT_MODEL.md
CHANGED
|
@@ -30,8 +30,9 @@ flowchart LR
|
|
|
30
30
|
|---|---|---|---|
|
|
31
31
|
| **S**uplantación | Otro proceso lee la contraseña del llavero | Llavero del SO; `--strict` (opt-in) exige confirmación en cada lectura | Sin `--strict`, un proceso del mismo usuario que invoque `security` puede leerla. Solución de raíz prevista: certificado de cliente / SSO (roadmap) |
|
|
32
32
|
| **S**uplantación | TLS interceptado | Verificación TLS; `caFile` | `allowSelfSigned` sigue existiendo como último recurso, marcado |
|
|
33
|
-
| **T**ampering | Cambiar `systems.json` para habilitar escritura
|
|
34
|
-
| **T**ampering | Retocar el registro de auditoría | Cadena de hash
|
|
33
|
+
| **T**ampering | Cambiar `systems.json` para habilitar escritura, redirigir a otro host o cambiar el comando de un componente (ejecución de código) | Rechazo si es modificable por otros o de otro usuario: permisos en macOS/Linux, ACL por SID en Windows; si no se puede comprobar, no se arranca | Un proceso del propio usuario puede modificarlo |
|
|
34
|
+
| **T**ampering | Retocar el registro de auditoría | Cadena de hash + firma HMAC con clave en el llavero del SO (fecha de creación incluida: quitar firmas se detecta); bloqueo entre procesos; `audit:verify` imprime el último hash para anclarlo fuera | Quien pueda leer el llavero del usuario (su propia sesión, sin `--strict`) podría recalcularlo; el anclaje externo del último hash cubre ese caso. En Windows, sin almacén, solo cadena |
|
|
35
|
+
| **T**ampering | Una escritura aprobada se aplica sobre una versión distinta de la mostrada | La huella de lo mostrado viaja con la confirmación y se compara bajo el bloqueo del objeto; si cambió, no se escribe | — |
|
|
35
36
|
| **R**epudio | «La IA lo cambió sola» o «no hay constancia de ese cambio» | Confirmación humana con vista previa + auditoría con forma de confirmación y huella del contenido. **Fail-closed**: la intención se registra antes de tocar SAP, y si no se puede registrar (disco lleno, permiso denegado, ruta inválida) la escritura no se ejecuta. Si lo que falla es solo el registro del resultado, la respuesta lo avisa explícitamente. Cubierto por tests que simulan ENOSPC y EACCES | El registro es local; un corte de energía entre intención y resultado deja una intención sin resultado, que se interpreta como «estado desconocido, verificar en SAP» |
|
|
36
37
|
| **I**nformation disclosure | Leer hashes, almacén seguro, PSE, OAuth, dumps | Veto directo y a través de vistas y CDS | Tablas Z que copien esos datos no se detectan por nombre |
|
|
37
38
|
| **I**nformation disclosure | Volcar datos personales al proveedor LLM | Clase de datos, enmascarado, tope de filas | Columnas personales con nombres no estándar (Z) no se enmascaran; el tope limita el volumen |
|
|
@@ -44,7 +45,7 @@ flowchart LR
|
|
|
44
45
|
|
|
45
46
|
| Riesgo | Cómo aplica a un MCP de SAP | Control |
|
|
46
47
|
|---|---|---|
|
|
47
|
-
| LLM01 Prompt injection | Comentarios en fuente ABAP, textos, datos de tablas, documentación o posts de comunidad con instrucciones | Marca de «dato, nunca instrucciones» en toda respuesta; ninguna escritura sin confirmación humana
|
|
48
|
+
| LLM01 Prompt injection | Comentarios en fuente ABAP, textos, datos de tablas, documentación o posts de comunidad con instrucciones | Marca de «dato, nunca instrucciones» en toda respuesta (probabilística: viaja en el mismo canal que los datos y el contenido podría imitarla; el protocolo MCP aún no separa instrucciones de datos); marcado neutralizado tras decodificar HTML; **la barrera efectiva**: ninguna escritura sin confirmación humana, tools de escritura destructivas para el cliente, y no existen tools para liberar, importar ni borrar |
|
|
48
49
|
| LLM02 Divulgación de información sensible | Datos personales y material de seguridad en respuestas que llegan al proveedor | Vetos, enmascarado, tope de filas, filtro de salida a internet |
|
|
49
50
|
| LLM03 Cadena de suministro | Dependencias npm, MCP de terceros | Versiones exactas, firmas, SBOM, `ignore-scripts`, terceros aislados tras revisión |
|
|
50
51
|
| LLM05 Manejo inadecuado de la salida | El agente usa una respuesta como si fuera verdad | Errores tipados que nunca parecen vacíos; «tope alcanzado» explícito; calidad de la evidencia en diffs |
|
package/docs/TOOLS.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Referencia de tools — abap-adt-doZimple
|
|
2
2
|
|
|
3
|
-
Generado desde el código con `npm run docs`.
|
|
3
|
+
Generado desde el código con `npm run docs`. 48 tools en 9 grupos y 3 flujos guiados.
|
|
4
4
|
Producto de [DoZimple](https://dozimple.cl).
|
|
5
5
|
|
|
6
6
|
## Índice
|
|
@@ -11,7 +11,7 @@ Producto de [DoZimple](https://dozimple.cl).
|
|
|
11
11
|
- [Consulta de datos](#datos) — 2 tools: `sql_query`, `table_contents`
|
|
12
12
|
- [Diagnóstico de incidentes](#diagnostico) — 4 tools: `dumps`, `jobs`, `application_log`, `gateway_errors`
|
|
13
13
|
- [Documentación SAP](#documentacion) — 7 tools: `abap_feature_matrix`, `docs_search`, `docs_fetch`, `clean_core_objects`, `clean_core_object`, `abap_lint`, `docs_community_search`
|
|
14
|
-
- [Escritura controlada](#escritura) —
|
|
14
|
+
- [Escritura controlada](#escritura) — 5 tools: `write_source`, `revert_source`, `activate`, `write_text_elements`, `create_transport`
|
|
15
15
|
- [DoZimple Transport Risk](#transport-risk) — 7 tools: `analyze_transport_risk`, `import_health`, `failure_ranking`, `change_audit`, `object_transport_history`, `remote_source`, `transport_source_check`
|
|
16
16
|
- [Operación y crecimiento](#operacion) — 4 tools: `sap_systems`, `report_gap`, `close_gap`, `usage_stats`
|
|
17
17
|
- [Flujos guiados](#flujos-guiados)
|
|
@@ -206,6 +206,16 @@ Chequeo de sintaxis real de SAP (no abaplint). Si pasas `source`, se comprueba E
|
|
|
206
206
|
|
|
207
207
|
\* obligatorio
|
|
208
208
|
|
|
209
|
+
Salida estructurada (`structuredContent`, además del texto):
|
|
210
|
+
|
|
211
|
+
| Campo | Tipo | Descripción |
|
|
212
|
+
|---|---|---|
|
|
213
|
+
| `object` | string | |
|
|
214
|
+
| `checked` | `proposed` \| `saved` | proposed = la fuente pasada sin guardar; saved = lo último guardado |
|
|
215
|
+
| `errors` | number | |
|
|
216
|
+
| `warnings` | number | |
|
|
217
|
+
| `messages` | lista de { severity, line, offset, text, uri } | |
|
|
218
|
+
|
|
209
219
|
### `run_unit_tests` — Ejecutar ABAP Unit
|
|
210
220
|
|
|
211
221
|
Ejecuta los tests ABAP Unit de una clase o programa y devuelve el resultado por método, con el detalle de cada fallo. Solo corre tests RISK LEVEL HARMLESS y DURATION SHORT. Si no hay clases de test lo dice: cero tests no es un éxito.
|
|
@@ -417,6 +427,16 @@ Ejecuta un SELECT de ABAP SQL (con WHERE, JOIN, ORDER BY, subconsultas) vía la
|
|
|
417
427
|
|
|
418
428
|
\* obligatorio
|
|
419
429
|
|
|
430
|
+
Salida estructurada (`structuredContent`, además del texto):
|
|
431
|
+
|
|
432
|
+
| Campo | Tipo | Descripción |
|
|
433
|
+
|---|---|---|
|
|
434
|
+
| `rows` | number | Filas devueltas |
|
|
435
|
+
| `columns` | lista de string | |
|
|
436
|
+
| `values` | lista de record | Filas como objetos columna→valor (hasta 500; el texto las trae todas) |
|
|
437
|
+
| `truncated` | boolean | true si values no incluye todas las filas |
|
|
438
|
+
| `notes` | lista de string | Avisos de la política de datos (enmascarado, tope de filas) |
|
|
439
|
+
|
|
420
440
|
### `table_contents` — Contenido de una tabla
|
|
421
441
|
|
|
422
442
|
Filas de una tabla, vista o CDS, con columnas y filtro opcionales. Atajo de sql_query para el caso típico «enséñame lo que hay en ZTABLA donde …». Para JOIN, subconsultas o agregados usa sql_query.
|
|
@@ -684,6 +704,28 @@ Sustituye la fuente COMPLETA de un objeto existente (o de un include de clase),
|
|
|
684
704
|
|
|
685
705
|
\* obligatorio
|
|
686
706
|
|
|
707
|
+
### `revert_source` — Volver a una versión anterior
|
|
708
|
+
|
|
709
|
+
Deshace un cambio escribiendo de nuevo una versión anterior del objeto: la última activa (para limpiar un borrador inactivo tras un write_source cuya activación falló), la anterior a la activa, o una concreta del historial de object_versions. Pasa por la misma vista previa, confirmación, bloqueo y orden que write_source: nunca revierte por su cuenta. Solo objetos de código fuente.
|
|
710
|
+
|
|
711
|
+
| | |
|
|
712
|
+
|---|---|
|
|
713
|
+
| **Acceso** | Escribe (solo DEV con `allowWrite`; nunca QAS/PRD) |
|
|
714
|
+
| **Créditos** | [abap-adt-api](https://github.com/marcellourbani/abap-adt-api) — Marcello Urbani (MIT, dependencia) |
|
|
715
|
+
|
|
716
|
+
| Parámetro | Tipo | Por defecto | Descripción |
|
|
717
|
+
|---|---|---|---|
|
|
718
|
+
| `system` | string | | Sistema SAP configurado. Obligatorio si hay varios y ninguno por defecto. |
|
|
719
|
+
| `object_name` * | string | | |
|
|
720
|
+
| `object_type` | string | | Tipo corto: PROG, INCL, CLAS, INTF, FUGR, FUNC, DDLS, DDLX, DCLS, TABL, STRU, VIEW, DTEL, DOMA, TTYP, MSAG, XSLT, BDEF, SRVD, SRVB, ENHO. También vale el tipo ADT (p. ej. PROG/P). |
|
|
721
|
+
| `include` | `main` \| `definitions` \| `implementations` \| `macros` \| `testclasses` | "main" | |
|
|
722
|
+
| `target` | union | "active" | «active»: la última versión activa (deshace un borrador inactivo, p. ej. un write_source cuya activación falló). «previous»: la versión anterior a la activa (deshace la última activación). Un número N: la versión N tal como la lista object_versions. |
|
|
723
|
+
| `transport` | string | | Orden (o tarea) donde debe ir la reversión. Obligatoria salvo objetos locales |
|
|
724
|
+
| `activate` | boolean | true | |
|
|
725
|
+
| `confirm_token` | string | | Token de la vista previa. Sin él la tool no escribe: devuelve qué va a cambiar y el token, que se usa tras la conformidad del usuario (un solo uso, 10 min). |
|
|
726
|
+
|
|
727
|
+
\* obligatorio
|
|
728
|
+
|
|
687
729
|
### `activate` — Activar objeto
|
|
688
730
|
|
|
689
731
|
Activa un objeto y devuelve los mensajes de SAP tal cual (errores con línea, avisos, objetos que quedan inactivos).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dozimple/abap-adt",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "DoZimple MCP server for SAP ABAP: multi-system, secure by design — code review, ATC, diagnosis and documentation for ECC and S/4HANA",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sap",
|
|
@@ -43,11 +43,12 @@
|
|
|
43
43
|
"test": "vitest run",
|
|
44
44
|
"smoke": "node dist/scripts/smoke.js",
|
|
45
45
|
"inspector": "npx @modelcontextprotocol/inspector node dist/index.js",
|
|
46
|
-
"security": "npm audit --omit=dev && npm audit signatures && node scripts/scan-sensitive.mjs",
|
|
46
|
+
"security": "npm audit --omit=dev && npm audit signatures && node scripts/scan-sensitive.mjs && node scripts/scan-sensitive.mjs --history",
|
|
47
47
|
"check": "npm test && npm run build && npm run security",
|
|
48
48
|
"docs": "npm run build && node dist/scripts/gen-docs.js",
|
|
49
49
|
"audit:verify": "node dist/scripts/audit-verify.js",
|
|
50
|
-
"sbom": "npm sbom --omit=dev --sbom-format=cyclonedx --sbom-type=application > sbom.cdx.json"
|
|
50
|
+
"sbom": "npm sbom --omit=dev --sbom-format=cyclonedx --sbom-type=application > sbom.cdx.json",
|
|
51
|
+
"hooks": "git config core.hooksPath .githooks && echo \"hooks activos: pre-commit (código completo) y pre-push (historial)\""
|
|
51
52
|
},
|
|
52
53
|
"engines": {
|
|
53
54
|
"node": ">=22"
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
63
|
"@types/node": "22.20.2",
|
|
64
|
+
"fast-check": "4.10.2",
|
|
63
65
|
"typescript": "5.9.3",
|
|
64
66
|
"vitest": "5.0.0"
|
|
65
67
|
},
|