@dozimple/abap-adt 1.0.0 → 1.0.1
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 +49 -0
- package/README.es.md +4 -3
- package/README.md +4 -3
- 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/config.js +89 -1
- package/dist/core/confirm.js +19 -7
- package/dist/core/connection.js +5 -1
- package/dist/core/credentials.js +10 -3
- package/dist/core/datapolicy.js +15 -7
- package/dist/core/errors.js +20 -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 +17 -8
- 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/run_unit_tests.js +2 -0
- package/dist/tools/core/support.js +6 -3
- package/dist/tools/core/table_contents.js +43 -7
- package/dist/tools/core/text_elements.js +11 -3
- package/dist/tools/core/write_source.js +14 -2
- package/dist/tools/docs/docs.js +8 -0
- package/dist/tools/transport-risk/_service.js +7 -2
- package/docs/THREAT_MODEL.md +4 -3
- package/package.json +5 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,55 @@ Formato basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/). V
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.0.1] - 2026-09-22
|
|
8
|
+
|
|
9
|
+
Versión de mantenimiento: seguimiento de la auditoría de seguridad, procedimiento de commit y pruebas por propiedades.
|
|
10
|
+
|
|
11
|
+
### Añadido
|
|
12
|
+
- **Pruebas por propiedades de la entrada hostil** (`test/property.test.ts`, fast-check): 15 invariantes sobre el
|
|
13
|
+
filtro de `table_contents`, el saneado de errores, el HTML de documentación y feeds, los nombres que acaban en una
|
|
14
|
+
ruta ADT, `SAP_USER_RE` y la huella de los tokens de confirmación. En vez de comprobar casos conocidos, cada prueba
|
|
15
|
+
afirma lo que la función garantiza para **cualquier** entrada y el generador busca el contraejemplo (miles por
|
|
16
|
+
ejecución; `FC_RUNS=5000 npm test` para una pasada profunda). Validadas rompiendo el código a propósito: cada
|
|
17
|
+
propiedad detecta la regresión que le toca.
|
|
18
|
+
|
|
19
|
+
### Cambiado
|
|
20
|
+
- La release de GitHub adjunta el bundle de la atestación también como `<paquete>.intoto.jsonl`, además de
|
|
21
|
+
`.sigstore.json`. Es el mismo bundle in-toto con los dos nombres: uno es el que verifica `gh attestation verify` y el
|
|
22
|
+
otro el que las herramientas de cadena de suministro reconocen como procedencia (OpenSSF Scorecard entre ellas).
|
|
23
|
+
|
|
24
|
+
### Seguridad
|
|
25
|
+
- El veto de datos sensibles incluye `ICF_PASSWD` (contraseña del usuario de inicio de sesión fijo de un servicio ICF)
|
|
26
|
+
y las columnas genéricas `PASSWD` / `PASSWORD`, en cualquier tabla, también cuando llegan por un `SELECT *` (se
|
|
27
|
+
comprueban las columnas del resultado y, si aparece una, no se muestra nada). Detectado al revisar la configuración
|
|
28
|
+
ICF de un servicio.
|
|
29
|
+
- Seguimiento de la auditoría (prioridades 1 y 2 del auditor y DZ-30): **permisos de `systems.json` en Windows**
|
|
30
|
+
comprobados por ACL y SID (antes no se comprobaba nada en esa plataforma), fail-closed si no se pueden leer;
|
|
31
|
+
**registro de auditoría firmado con HMAC-SHA256** con clave en el llavero del SO (quitar firmas se detecta), y
|
|
32
|
+
`audit:verify` comprueba cadena y firmas; el escáner falla si `package.json` declara scripts de instalación.
|
|
33
|
+
- **Auditoría de seguridad del 21-09-2026** (30 hallazgos, ninguno alto ni crítico): corregidos todos los accionables.
|
|
34
|
+
Los más relevantes: la escritura ya no aplica un diff aprobado sobre un objeto que cambió en SAP después de la vista
|
|
35
|
+
previa (huella comprobada bajo el bloqueo); filtro de `table_contents` por lista blanca; usuarios SAP validados en
|
|
36
|
+
filtros de feeds; nombres validados en rutas ADT; `allowSelfSigned` solo en DEV y avisado al arrancar; sin
|
|
37
|
+
redirecciones en el servicio de riesgo; errores saneados antes de llegar al modelo; contraseñas en memoria con
|
|
38
|
+
caducidad; toda tool `exec` decide expresamente si pide confirmación; tope de filas también en `jobs` y
|
|
39
|
+
`application_log`; columnas personales propias por cliente (`piiColumns`); registro de auditoría con bloqueo entre
|
|
40
|
+
procesos y lectura de la cola; marcado neutralizado tras decodificar HTML; ids online de `docs_fetch` validados ya
|
|
41
|
+
decodificados. Documentado lo que cada control garantiza y lo que no (token frente a elicitación, cadena de
|
|
42
|
+
auditoría sin secreto, aviso de «dato» probabilístico). Tests de regresión en `test/audit-2026-09.test.ts`.
|
|
43
|
+
- **Procedimiento de seguridad en cada commit** ([docs/COMMIT_SECURITY.md](docs/COMMIT_SECURITY.md)): el `pre-commit`
|
|
44
|
+
revisa el código completo y el contenido exacto preparado; un `pre-push` nuevo y el CI revisan todo el historial de
|
|
45
|
+
todas las ramas. El escáner detecta además direcciones (IPs reales, hosts con puertos de SAP, dominios internos),
|
|
46
|
+
más tipos de credenciales (Google, JWT, tokens de GitHub de grano fino, GitLab, cadenas de conexión, secretos
|
|
47
|
+
asignados, URLs con contraseña, webhooks) y archivos peligrosos por su nombre (`.env`, certificados, claves SSH, el
|
|
48
|
+
`systems.json` real). 21 tests adversariales.
|
|
49
|
+
|
|
50
|
+
### Corregido
|
|
51
|
+
- Workflow de release: la comprobación de `latest` fallaba porque npm tarda unos segundos en propagar la etiqueta
|
|
52
|
+
(la 1.0.0 se publicó bien, pero la release de GitHub no se creó y se completó a mano con los artefactos firmados del
|
|
53
|
+
mismo run). Ahora reintenta durante 2 minutos, no republica una versión existente y actualiza la release si ya
|
|
54
|
+
existe: reintentar un run es seguro.
|
|
55
|
+
|
|
7
56
|
## [1.0.0] - 2026-09-21
|
|
8
57
|
|
|
9
58
|
Primera versión estable, publicada en npm como `@dozimple/abap-adt` con procedencia.
|
package/README.es.md
CHANGED
|
@@ -291,7 +291,7 @@ Modelo de amenazas con STRIDE y OWASP Top 10 para aplicaciones LLM: **[docs/THRE
|
|
|
291
291
|
Con la versión exacta y sin scripts de instalación, igual que las dependencias del propio proyecto:
|
|
292
292
|
|
|
293
293
|
```sh
|
|
294
|
-
npm install -g --ignore-scripts @dozimple/abap-adt@1.0.
|
|
294
|
+
npm install -g --ignore-scripts @dozimple/abap-adt@1.0.1
|
|
295
295
|
PKG="$(npm root -g)/@dozimple/abap-adt"
|
|
296
296
|
mkdir -p ~/.config/abap-adt-dozimple && chmod 700 ~/.config/abap-adt-dozimple
|
|
297
297
|
cp "$PKG/config/systems.example.json" ~/.config/abap-adt-dozimple/systems.json # sistemas, roles y permisos
|
|
@@ -307,8 +307,9 @@ Registro en el cliente MCP:
|
|
|
307
307
|
```
|
|
308
308
|
|
|
309
309
|
Cada versión se publica desde el CI con [procedencia de npm](https://docs.npmjs.com/generating-provenance-statements):
|
|
310
|
-
`npm view @dozimple/abap-adt@1.0.
|
|
311
|
-
paquete, su bundle de Sigstore
|
|
310
|
+
`npm view @dozimple/abap-adt@1.0.1 dist.attestations` muestra la atestación, y la release de GitHub incluye el
|
|
311
|
+
paquete, su bundle de Sigstore (`.sigstore.json`), el mismo bundle como procedencia in-toto (`.intoto.jsonl`)
|
|
312
|
+
y el SBOM. Para comprobarlo: `gh attestation verify dozimple-abap-adt-1.0.1.tgz --repo <owner>/abap-adt-dozimple`.
|
|
312
313
|
|
|
313
314
|
### Desde el código fuente
|
|
314
315
|
|
package/README.md
CHANGED
|
@@ -291,7 +291,7 @@ Designed to pass a Security and Basis review without exceptions. Details: **[SEC
|
|
|
291
291
|
Pinned to an exact version, without install scripts, like the project's own dependencies:
|
|
292
292
|
|
|
293
293
|
```sh
|
|
294
|
-
npm install -g --ignore-scripts @dozimple/abap-adt@1.0.
|
|
294
|
+
npm install -g --ignore-scripts @dozimple/abap-adt@1.0.1
|
|
295
295
|
PKG="$(npm root -g)/@dozimple/abap-adt"
|
|
296
296
|
mkdir -p ~/.config/abap-adt-dozimple && chmod 700 ~/.config/abap-adt-dozimple
|
|
297
297
|
cp "$PKG/config/systems.example.json" ~/.config/abap-adt-dozimple/systems.json # systems, roles and permissions
|
|
@@ -307,8 +307,9 @@ MCP client registration:
|
|
|
307
307
|
```
|
|
308
308
|
|
|
309
309
|
Every release is published from CI with [npm provenance](https://docs.npmjs.com/generating-provenance-statements):
|
|
310
|
-
`npm view @dozimple/abap-adt@1.0.
|
|
311
|
-
tarball, its Sigstore bundle
|
|
310
|
+
`npm view @dozimple/abap-adt@1.0.1 dist.attestations` shows the attestation, and the GitHub release carries the
|
|
311
|
+
tarball, its Sigstore bundle (`.sigstore.json`), the same bundle as in-toto provenance (`.intoto.jsonl`) and the
|
|
312
|
+
SBOM. To check it: `gh attestation verify dozimple-abap-adt-1.0.1.tgz --repo <owner>/abap-adt-dozimple`.
|
|
312
313
|
|
|
313
314
|
### From source
|
|
314
315
|
|
package/SECURITY.md
CHANGED
|
@@ -8,26 +8,26 @@ aplicaciones LLM, riesgos residuales): **[docs/THREAT_MODEL.md](docs/THREAT_MODE
|
|
|
8
8
|
|
|
9
9
|
| Activo | Amenaza | Control |
|
|
10
10
|
|---|---|---|
|
|
11
|
-
| Credenciales SAP | Filtrado en archivos, logs, respuestas o procesos hijo | Llavero de macOS o Secret Service en Linux (`execFile` sin shell); opción `--strict` para que el llavero pregunte en cada lectura; la configuración no admite contraseñas; nunca en logs ni respuestas; los procesos hijo reciben un entorno explícito mínimo (`PATH`, `HOME`, `TMPDIR`, idioma y terminal) |
|
|
11
|
+
| Credenciales SAP | Filtrado en archivos, logs, respuestas o procesos hijo | Llavero de macOS o Secret Service en Linux (`execFile` sin shell); opción `--strict` para que el llavero pregunte en cada lectura; en memoria, caducan a los 15 min y se borran al cerrar; la configuración no admite contraseñas; nunca en logs ni respuestas; los procesos hijo reciben un entorno explícito mínimo (`PATH`, `HOME`, `TMPDIR`, idioma y terminal) |
|
|
12
12
|
| Sistemas SAP | Cambios no deseados por un agente | `read` / `exec` / `write` por tool; `write` solo en DEV con `allowWrite`; QAS y PRD nunca; tools de escritura ocultas si ningún sistema la permite; sin liberar, importar ni borrar |
|
|
13
|
-
| Escrituras | El agente escribe sin que la persona vea qué | **Confirmación obligatoria
|
|
14
|
-
| Trazabilidad | Una escritura sin rastro, o un rastro retocado | **Registro de auditoría** local encadenado por hash (`audit.jsonl`): intención, resultado y denegaciones de toda escritura y ejecución, con usuario SAP, sistema, forma de confirmación y huella sha256 del contenido (nunca el contenido). Sin registro no se escribe. `npm run audit:verify`
|
|
13
|
+
| Escrituras | El agente escribe sin que la persona vea qué | **Confirmación obligatoria** con vista previa (sintaxis de SAP + diff real + avisos de la orden). Dos niveles de garantía, y no son equivalentes: **elicitación MCP** (fuerte: la pregunta la responde una persona en el cliente) si el cliente la soporta; si no, **token** de un solo uso atado a tool, sistema y argumentos exactos (10 min), que es una garantía **de aviso**: el servidor no puede distinguir si una persona vio la vista previa, por eso el cliente no debe autoaprobar estas tools (`destructiveHint`). `npm run audit:verify` cuenta cuántas escrituras se confirmaron de cada forma. Además, la huella de lo mostrado viaja con la confirmación y se comprueba **bajo el bloqueo** antes de escribir: si el objeto cambió en SAP tras la vista previa, no se escribe |
|
|
14
|
+
| Trazabilidad | Una escritura sin rastro, o un rastro retocado | **Registro de auditoría** local encadenado por hash y **firmado con HMAC-SHA256** (`audit.jsonl`): intención, resultado y denegaciones de toda escritura y ejecución, con usuario SAP, sistema, forma de confirmación y huella sha256 del contenido (nunca el contenido). La clave se genera al primer uso y vive en el llavero del SO (se guarda por stdin, nunca como argumento de un proceso); la fecha de creación va con ella, así que quitar firmas se detecta. Sin registro no se escribe; escrituras de varios servidores a la vez, serializadas con bloqueo. `npm run audit:verify` comprueba cadena y firmas. **Límite:** quien pueda leer tu llavero (tu propia sesión sin `--strict`) podría recalcularlo todo; para evidencia fuera de tu control, anota fuera del equipo el «último hash» que imprime `audit:verify`. Sin almacén de secretos (Windows), la cadena queda sin firma y se avisa |
|
|
15
15
|
| ABAP Unit | Un test con efectos (BD, llamadas externas) | Solo tests `RISK LEVEL HARMLESS` y `DURATION SHORT`, pedido explícitamente; solo en DEV |
|
|
16
16
|
| Orden de transporte | El cambio cae en una orden ajena | `decideTransport`: orden explícita; si hay bloqueo CTS de otra orden, se para sin escribir |
|
|
17
17
|
| Material de seguridad en SAP | Lectura de hashes, almacén seguro, PSE, secretos OAuth o dumps | Veto de USR02/USH02/USRPWDHISTORY, RSECTAB/RSECACTB, SSF_PSE_*, USRACL, SNAP, OA2C_* y columnas de hash, **también a través de vistas (DD26S) y CDS (DDLDEPENDENCY + fuente DDL), hasta 3 niveles** |
|
|
18
18
|
| Datos de personal | Infotipos y nómina | PA\*, PB\*, PCL1-5 y HRPY_\* vetados, también vía vistas y CDS |
|
|
19
|
-
| Datos personales de negocio | Exposición masiva a un proveedor LLM | Clase de datos por sistema (`test` / `masked` / `prod`; por defecto DEV = test, QAS/PRD = prod). En masked/prod: **columnas personales enmascaradas** (nombres, direcciones, teléfonos, correos, identificadores fiscales, cuentas bancarias, fechas de nacimiento), prohibido usarlas en WHERE, alias o expresiones, y **tope de filas** por sistema (200 en prod, configurable) |
|
|
19
|
+
| Datos personales de negocio | Exposición masiva a un proveedor LLM | Clase de datos por sistema (`test` / `masked` / `prod`; por defecto DEV = test, QAS/PRD = prod). En masked/prod: **columnas personales enmascaradas** (nombres, direcciones, teléfonos, correos, identificadores fiscales, cuentas bancarias, fechas de nacimiento, y las columnas propias de cada cliente declaradas en `piiColumns`), prohibido usarlas en WHERE, alias o expresiones, y **tope de filas** por sistema (200 en prod, configurable), también en `jobs` y `application_log`. El enmascarado es por nombre de columna: un campo Z no declarado sale en claro |
|
|
20
20
|
| Parámetros de las tools | Un parámetro mal escrito o inventado que se ignora en silencio (p. ej. la orden de transporte) | Esquemas estrictos: un parámetro no declarado es error y la tool no se ejecuta; el esquema publicado declara `additionalProperties: false` |
|
|
21
|
-
| SQL | Escritura, varias sentencias o inyección en filtros | Solo `SELECT`/`WITH`, sin `;`; vista previa de ADT de solo lectura; literales escapados; en `table_contents` el filtro
|
|
22
|
-
| Configuración | Alguien redirige el servidor
|
|
21
|
+
| SQL | Escritura, varias sentencias o inyección en filtros | Solo `SELECT`/`WITH`, sin `;`; vista previa de ADT de solo lectura; literales escapados; en `table_contents` el filtro se valida por **lista blanca** (columnas, operadores, literales, AND/OR/NOT, LIKE, IN, BETWEEN, IS NULL/INITIAL, paréntesis equilibrados); usuarios SAP en filtros de feeds validados por formato; nombres en rutas ADT validados (sin `..`) |
|
|
22
|
+
| Configuración | Alguien redirige el servidor, habilita la escritura o cambia el comando de un componente | `systems.json` se rechaza si lo pueden modificar otros: en macOS/Linux por permisos y propietario; **en Windows por sus ACL**, leídas por SID (independiente del idioma): solo tu usuario, SYSTEM, Administradores y TrustedInstaller pueden escribirlo. Si la comprobación no puede hacerse, no se arranca (salvo `ABAP_DZ_SKIP_ACL_CHECK=1`, avisado en cada arranque). El sistema por defecto por variable de entorno se identifica en cada respuesta, y si no existe es error |
|
|
23
23
|
| Red | Exposición de un puerto | Solo stdio; no se abre ningún puerto |
|
|
24
|
-
| TLS | Intercepción (MitM) | Verificación por defecto; `caFile` para certificados propios; `allowSelfSigned` marcado con ⚠ |
|
|
24
|
+
| TLS | Intercepción (MitM) | Verificación por defecto; `caFile` para certificados propios; `allowSelfSigned` **solo en DEV** (rechazado en QAS/PRD), marcado con ⚠ y avisado en cada arranque. El servicio de riesgo no sigue redirecciones: las credenciales Basic no viajan a otra URL |
|
|
25
25
|
| Datos de clientes | Salida a internet vía documentación | Online apagado por defecto; si se habilita, `assertPublicQuery` bloquea objetos Z/Y, namespaces, órdenes, sistemas, SID, usuarios, dominios y términos configurados; `docs_fetch` solo acepta ids con forma de documento, y los online pasan el mismo filtro |
|
|
26
|
-
| Inyección de instrucciones | Fuente, textos o datos de SAP o de la web que intentan dirigir al agente | Toda respuesta de SAP y de documentación se marca como dato, nunca instrucciones; ninguna escritura sin confirmación humana con vista previa |
|
|
27
|
-
| Dependencias | Vulnerabilidades o código malicioso | 4 dependencias de producción con versión exacta y lockfile; `ignore-scripts` (sin scripts de instalación); `npm audit` y `npm audit signatures` (firmas y procedencia) en CI; SBOM CycloneDX como artefacto de cada build; acciones de CI fijadas por commit; Dependabot |
|
|
26
|
+
| Inyección de instrucciones | Fuente, textos o datos de SAP o de la web que intentan dirigir al agente | Toda respuesta de SAP y de documentación se marca como dato, nunca instrucciones (control **probabilístico**: reduce, no elimina, que el modelo obedezca contenido hostil); el marcado que revive al decodificar HTML se neutraliza; la barrera efectiva es que ninguna escritura ocurre sin confirmación humana con vista previa. Los errores que llegan al modelo se sanean (sin credenciales en URLs, sin cabeceras, sin marcado, con longitud acotada) |
|
|
27
|
+
| Dependencias | Vulnerabilidades o código malicioso | 4 dependencias de producción con versión exacta y lockfile; `ignore-scripts` (sin scripts de instalación) en el desarrollo, y el paquete publicado **no declara ningún script de instalación** (el escáner falla si aparece uno: el `.npmrc` no protege a quien instala el paquete); `npm audit` y `npm audit signatures` (firmas y procedencia) en CI; SBOM CycloneDX como artefacto de cada build; acciones de CI fijadas por commit; Dependabot |
|
|
28
28
|
| Terceros | Un MCP externo con acceso a credenciales | Solo como proceso hijo aislado, tras revisión, con entorno mínimo; sus tools pasan por la política de este servidor |
|
|
29
29
|
| Estado local | Lectura por otros usuarios del equipo | Carpetas `700`, archivos `600`; el registro de uso no guarda argumentos |
|
|
30
|
-
| Repositorio | Credenciales o datos de clientes en el historial |
|
|
30
|
+
| Repositorio | Credenciales, direcciones (IPs, hosts internos o de SAP) o datos de clientes en el código o en el historial | [Procedimiento en cada commit](docs/COMMIT_SECURITY.md): `pre-commit` revisa el código completo más lo preparado; `pre-push` y CI revisan todo el historial; `main` exige el CI; secret scanning con push protection. Los términos de clientes se leen de la configuración local, nunca se listan en el repo |
|
|
31
31
|
|
|
32
32
|
## Recomendaciones de despliegue
|
|
33
33
|
|
|
@@ -46,7 +46,8 @@ aplicaciones LLM, riesgos residuales): **[docs/THREAT_MODEL.md](docs/THREAT_MODE
|
|
|
46
46
|
|
|
47
47
|
```sh
|
|
48
48
|
npm run security # npm audit + firmas del registro + escáner de credenciales y datos de clientes
|
|
49
|
-
npm test # incluye los tests de política y de seguridad (test/security.test.ts)
|
|
49
|
+
npm test # incluye los tests de política y de seguridad (test/security.test.ts) y las
|
|
50
|
+
# pruebas por propiedades de la entrada hostil (test/property.test.ts, fast-check)
|
|
50
51
|
npm run audit:verify # integridad del registro de auditoría local
|
|
51
52
|
npm run sbom # SBOM CycloneDX de las dependencias de producción
|
|
52
53
|
```
|
package/dist/core/atc.js
CHANGED
|
@@ -31,7 +31,8 @@ export async function defaultVariant(c) {
|
|
|
31
31
|
const v = cust.properties.find((p) => p.name === "systemCheckVariant")?.value;
|
|
32
32
|
return typeof v === "string" && v ? v : "DEFAULT";
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
/** Escapado XML completo (texto y atributos con comillas simples o dobles). */
|
|
35
|
+
const xmlEsc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
35
36
|
/**
|
|
36
37
|
* Ejecución ATC sobre varios objetos a la vez. La librería solo admite una
|
|
37
38
|
* URI; en NW 7.50 una orden no sirve como conjunto («No URI-Mapping defined
|
package/dist/core/audit.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { appendFileSync, closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, rmSync, statSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { auditKey, macOf } from "./auditkey.js";
|
|
4
5
|
import { stateDir } from "./telemetry.js";
|
|
5
6
|
/**
|
|
6
7
|
* Registro de auditoría de todo lo que modifica o ejecuta en SAP. Solo
|
|
@@ -15,7 +16,7 @@ export const AUDIT_FILE = "audit.jsonl";
|
|
|
15
16
|
const GENESIS = "0".repeat(64);
|
|
16
17
|
const INLINE_MAX = 120;
|
|
17
18
|
/** E/S del registro, sustituible en tests para simular disco lleno o permisos. */
|
|
18
|
-
export const auditIo = { appendFileSync, existsSync, mkdirSync, readFileSync };
|
|
19
|
+
export const auditIo = { appendFileSync, existsSync, mkdirSync, readFileSync, openSync, closeSync, fstatSync, readSync, statSync, rmSync };
|
|
19
20
|
const sha256 = (s) => createHash("sha256").update(s).digest("hex");
|
|
20
21
|
/** Argumentos aptos para el registro: valores cortos tal cual, el resto como huella. */
|
|
21
22
|
export function auditArgs(args) {
|
|
@@ -39,14 +40,63 @@ export function auditArgs(args) {
|
|
|
39
40
|
function entryHash(e) {
|
|
40
41
|
return sha256(JSON.stringify(e));
|
|
41
42
|
}
|
|
43
|
+
/** Última entrada leyendo solo la cola del archivo (el registro no se relee entero en cada escritura). */
|
|
42
44
|
function lastEntry(path) {
|
|
43
45
|
if (!auditIo.existsSync(path))
|
|
44
46
|
return { seq: 0, hash: GENESIS };
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
const fd = auditIo.openSync(path, "r");
|
|
48
|
+
try {
|
|
49
|
+
const size = auditIo.fstatSync(fd).size;
|
|
50
|
+
if (!size)
|
|
51
|
+
return { seq: 0, hash: GENESIS };
|
|
52
|
+
for (let window = 16 * 1024;; window *= 4) {
|
|
53
|
+
const start = Math.max(0, size - window);
|
|
54
|
+
const buf = Buffer.alloc(size - start);
|
|
55
|
+
auditIo.readSync(fd, buf, 0, buf.length, start);
|
|
56
|
+
const lines = buf.toString("utf8").trimEnd().split("\n");
|
|
57
|
+
// Si la ventana empieza a mitad de una línea, la primera está cortada: vale la última si hay al menos dos.
|
|
58
|
+
if (lines.length >= 2 || start === 0) {
|
|
59
|
+
const last = JSON.parse(lines[lines.length - 1]);
|
|
60
|
+
return { seq: last.seq, hash: last.hash };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
auditIo.closeSync(fd);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Bloqueo entre procesos (dos clientes MCP pueden tener cada uno su servidor): sin él, dos escrituras simultáneas
|
|
70
|
+
* leerían el mismo «prev» y romperían la cadena, que sería indistinguible de una manipulación.
|
|
71
|
+
*/
|
|
72
|
+
function withLock(dir, fn) {
|
|
73
|
+
const lock = join(dir, AUDIT_FILE + ".lock");
|
|
74
|
+
const deadline = Date.now() + 5_000;
|
|
75
|
+
for (;;) {
|
|
76
|
+
try {
|
|
77
|
+
auditIo.closeSync(auditIo.openSync(lock, "wx", 0o600));
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
if (e.code !== "EEXIST")
|
|
82
|
+
throw e;
|
|
83
|
+
// Un bloqueo de más de 30 s es de un proceso que murió a mitad: se recupera.
|
|
84
|
+
try {
|
|
85
|
+
if (Date.now() - auditIo.statSync(lock).mtimeMs > 30_000)
|
|
86
|
+
auditIo.rmSync(lock, { force: true });
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
if (Date.now() > deadline)
|
|
90
|
+
throw new Error("registro de auditoría bloqueado por otro proceso");
|
|
91
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 25);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
return fn();
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
auditIo.rmSync(lock, { force: true });
|
|
99
|
+
}
|
|
50
100
|
}
|
|
51
101
|
/**
|
|
52
102
|
* Añade una entrada. Lanza si no puede escribir: quien llama decide no
|
|
@@ -55,6 +105,12 @@ function lastEntry(path) {
|
|
|
55
105
|
export function appendAudit(input, dir = stateDir()) {
|
|
56
106
|
auditIo.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
57
107
|
const path = join(dir, AUDIT_FILE);
|
|
108
|
+
return withLock(dir, () => appendLocked(path, input));
|
|
109
|
+
}
|
|
110
|
+
function appendLocked(path, input) {
|
|
111
|
+
// La clave se obtiene (o se crea) ANTES de fechar la entrada: así la primera firmada nunca queda con una fecha
|
|
112
|
+
// anterior a la de la clave, y quitarle la firma no la hace pasar por una entrada antigua.
|
|
113
|
+
const key = auditKey(true);
|
|
58
114
|
const prev = lastEntry(path);
|
|
59
115
|
const base = {
|
|
60
116
|
seq: prev.seq + 1,
|
|
@@ -63,14 +119,19 @@ export function appendAudit(input, dir = stateDir()) {
|
|
|
63
119
|
args: auditArgs(input.args),
|
|
64
120
|
prev: prev.hash,
|
|
65
121
|
};
|
|
66
|
-
const
|
|
122
|
+
const hash = entryHash(base);
|
|
123
|
+
const entry = key ? { ...base, hash, mac: macOf(key.key, hash) } : { ...base, hash };
|
|
67
124
|
auditIo.appendFileSync(path, JSON.stringify(entry) + "\n", { mode: 0o600 });
|
|
68
125
|
return entry;
|
|
69
126
|
}
|
|
70
|
-
/**
|
|
71
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Recorre la cadena completa: secuencia, enlace con la anterior y hash propio. Con la clave, además, la firma de cada
|
|
129
|
+
* entrada; y toda entrada posterior a la creación de la clave TIENE que estar firmada (quitar firmas no sirve).
|
|
130
|
+
*/
|
|
131
|
+
export function verifyAudit(text, key) {
|
|
72
132
|
const lines = text.split("\n").filter(Boolean);
|
|
73
133
|
let prev = GENESIS;
|
|
134
|
+
let signed = 0;
|
|
74
135
|
for (let i = 0; i < lines.length; i++) {
|
|
75
136
|
let e;
|
|
76
137
|
try {
|
|
@@ -79,15 +140,23 @@ export function verifyAudit(text) {
|
|
|
79
140
|
catch {
|
|
80
141
|
return { ok: false, entries: lines.length, brokenAt: i + 1, reason: "línea que no es JSON" };
|
|
81
142
|
}
|
|
82
|
-
const { hash, ...rest } = e;
|
|
143
|
+
const { hash, mac, ...rest } = e;
|
|
83
144
|
if (e.seq !== i + 1)
|
|
84
145
|
return { ok: false, entries: lines.length, brokenAt: i + 1, reason: `secuencia ${e.seq}, se esperaba ${i + 1}` };
|
|
85
146
|
if (e.prev !== prev)
|
|
86
147
|
return { ok: false, entries: lines.length, brokenAt: i + 1, reason: "no enlaza con la entrada anterior" };
|
|
87
148
|
if (entryHash(rest) !== hash)
|
|
88
149
|
return { ok: false, entries: lines.length, brokenAt: i + 1, reason: "contenido alterado" };
|
|
150
|
+
if (key) {
|
|
151
|
+
if (mac !== undefined && mac !== macOf(key.key, hash))
|
|
152
|
+
return { ok: false, entries: lines.length, brokenAt: i + 1, reason: "firma HMAC inválida (cadena recalculada sin la clave)" };
|
|
153
|
+
if (mac === undefined && e.ts >= key.created)
|
|
154
|
+
return { ok: false, entries: lines.length, brokenAt: i + 1, reason: "entrada sin firma posterior a la creación de la clave (firma quitada)" };
|
|
155
|
+
if (mac !== undefined)
|
|
156
|
+
signed++;
|
|
157
|
+
}
|
|
89
158
|
prev = hash;
|
|
90
159
|
}
|
|
91
|
-
return { ok: true, entries: lines.length };
|
|
160
|
+
return key ? { ok: true, entries: lines.length, signed } : { ok: true, entries: lines.length };
|
|
92
161
|
}
|
|
93
162
|
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
3
|
+
/**
|
|
4
|
+
* Clave HMAC del registro de auditoría, guardada en el almacén de secretos del sistema operativo (llavero de macOS
|
|
5
|
+
* o Secret Service en Linux). Con ella, reescribir el registro exige además poder leer el llavero del usuario: un
|
|
6
|
+
* proceso sin acceso al llavero (u otro usuario del equipo) ya no puede recalcular la cadena y que pase por buena.
|
|
7
|
+
*
|
|
8
|
+
* Formato guardado: «v1:<fecha ISO de creación>:<64 hex>». La fecha permite detectar que se QUITARON firmas:
|
|
9
|
+
* toda entrada posterior a ella tiene que llevarla.
|
|
10
|
+
*
|
|
11
|
+
* ABAP_DZ_AUDIT_KEYSTORE: «os» (por defecto), «memory» (tests) o «none» (sin firma, se avisa).
|
|
12
|
+
*/
|
|
13
|
+
const SERVICE = "abap-adt-dozimple-audit";
|
|
14
|
+
const ACCOUNT = "hmac";
|
|
15
|
+
let cached;
|
|
16
|
+
let memory;
|
|
17
|
+
let warned = false;
|
|
18
|
+
const parse = (v) => {
|
|
19
|
+
const m = /^v1:([^:]+(?::[^:]+)*?):([0-9a-f]{64})$/.exec((v ?? "").trim());
|
|
20
|
+
return m ? { created: m[1], key: Buffer.from(m[2], "hex") } : null;
|
|
21
|
+
};
|
|
22
|
+
function readOs() {
|
|
23
|
+
try {
|
|
24
|
+
if (process.platform === "darwin") {
|
|
25
|
+
return execFileSync("security", ["find-generic-password", "-s", SERVICE, "-a", ACCOUNT, "-w"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
26
|
+
}
|
|
27
|
+
if (process.platform === "linux") {
|
|
28
|
+
return execFileSync("secret-tool", ["lookup", "service", SERVICE, "account", ACCOUNT], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* no existe todavía, o no hay almacén */
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
/** Guarda el valor sin pasarlo nunca como argumento de un proceso (visible en `ps`): siempre por stdin. */
|
|
37
|
+
function writeOs(value) {
|
|
38
|
+
try {
|
|
39
|
+
if (process.platform === "darwin") {
|
|
40
|
+
execFileSync("security", ["-i"], { input: `add-generic-password -U -s ${SERVICE} -a ${ACCOUNT} -w ${value}\n`, stdio: ["pipe", "ignore", "ignore"] });
|
|
41
|
+
return parse(readOs())?.key.toString("hex") === value.split(":").pop();
|
|
42
|
+
}
|
|
43
|
+
if (process.platform === "linux") {
|
|
44
|
+
execFileSync("secret-tool", ["store", `--label=${SERVICE}`, "service", SERVICE, "account", ACCOUNT], { input: value, stdio: ["pipe", "ignore", "ignore"] });
|
|
45
|
+
return !!parse(readOs());
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
/* sin almacén disponible */
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* La clave, creándola la primera vez si `create`. null = no hay almacén de secretos: el registro sigue encadenado
|
|
55
|
+
* pero sin firma (y se avisa una vez por stderr).
|
|
56
|
+
*/
|
|
57
|
+
export function auditKey(create = true) {
|
|
58
|
+
if (cached !== undefined && (cached || !create))
|
|
59
|
+
return cached;
|
|
60
|
+
const mode = process.env.ABAP_DZ_AUDIT_KEYSTORE ?? "os";
|
|
61
|
+
if (mode === "none")
|
|
62
|
+
return (cached = null);
|
|
63
|
+
const read = () => (mode === "memory" ? memory : readOs());
|
|
64
|
+
let k = parse(read());
|
|
65
|
+
if (!k && create) {
|
|
66
|
+
const value = `v1:${new Date().toISOString()}:${randomBytes(32).toString("hex")}`;
|
|
67
|
+
if (mode === "memory")
|
|
68
|
+
memory = value;
|
|
69
|
+
else if (!writeOs(value)) {
|
|
70
|
+
if (!warned)
|
|
71
|
+
console.error("[abap-adt-doZimple] ⚠ Sin almacén de secretos: el registro de auditoría queda encadenado pero sin firma HMAC.");
|
|
72
|
+
warned = true;
|
|
73
|
+
return (cached = null);
|
|
74
|
+
}
|
|
75
|
+
k = parse(read());
|
|
76
|
+
}
|
|
77
|
+
return (cached = k);
|
|
78
|
+
}
|
|
79
|
+
/** Solo tests: olvida la clave en memoria (como si fuera otro equipo o se hubiera borrado). */
|
|
80
|
+
export function resetAuditKeyForTests() {
|
|
81
|
+
cached = undefined;
|
|
82
|
+
memory = undefined;
|
|
83
|
+
}
|
|
84
|
+
export const macOf = (key, hash) => createHmac("sha256", key).update(hash).digest("hex");
|
|
85
|
+
//# sourceMappingURL=auditkey.js.map
|
package/dist/core/config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
1
2
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
3
|
import { homedir } from "node:os";
|
|
3
4
|
import { join, resolve } from "node:path";
|
|
@@ -34,6 +35,11 @@ const SystemSchema = z.object({
|
|
|
34
35
|
dataClass: z.enum(["test", "masked", "prod"]).optional(),
|
|
35
36
|
/** Tope de filas por consulta; por defecto test 5000, masked 1000, prod 200. */
|
|
36
37
|
maxRows: z.number().int().min(1).max(5000).optional(),
|
|
38
|
+
/**
|
|
39
|
+
* Columnas personales propias de este cliente (campos Z o alias de CDS) que se enmascaran igual que las estándar
|
|
40
|
+
* en sistemas masked/prod. El enmascarado es por nombre: lo que no esté aquí ni en la lista estándar sale en claro.
|
|
41
|
+
*/
|
|
42
|
+
piiColumns: z.array(z.string().regex(/^[A-Za-z0-9_/]+$/)).default([]),
|
|
37
43
|
password: z
|
|
38
44
|
.string()
|
|
39
45
|
.regex(/^(keychain|env:[A-Z0-9_]+)$/, 'password: "keychain" o "env:NOMBRE_VARIABLE"')
|
|
@@ -70,6 +76,12 @@ export function parseConfig(raw) {
|
|
|
70
76
|
throw new Error(`Sistema duplicado en la configuración: ${s.id}`);
|
|
71
77
|
ids.add(key);
|
|
72
78
|
}
|
|
79
|
+
for (const s of cfg.systems) {
|
|
80
|
+
// Sin verificar TLS, quien intercepte la red recibe usuario y contraseña (Basic): nunca en calidad ni productivo.
|
|
81
|
+
if (s.allowSelfSigned && s.role !== "DEV") {
|
|
82
|
+
throw new Error(`${s.id}: allowSelfSigned solo se admite en sistemas DEV. Para ${s.role} usa caFile con el certificado de su CA.`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
73
85
|
if (cfg.defaultSystem && !ids.has(cfg.defaultSystem.toUpperCase())) {
|
|
74
86
|
throw new Error(`defaultSystem "${cfg.defaultSystem}" no está en systems[]`);
|
|
75
87
|
}
|
|
@@ -88,8 +100,9 @@ export function loadConfig(path = configPath()) {
|
|
|
88
100
|
* servidor y si puede escribir en él: tiene que ser solo del usuario.
|
|
89
101
|
*/
|
|
90
102
|
export function assertPrivateFile(path, st = statSync(path)) {
|
|
103
|
+
// En Windows no hay bits de modo ni uid: se leen las ACL (assertPrivateFileWindows).
|
|
91
104
|
if (process.platform === "win32")
|
|
92
|
-
return;
|
|
105
|
+
return assertPrivateFileWindows(path);
|
|
93
106
|
if (st.mode & 0o022) {
|
|
94
107
|
throw new Error(`${path} lo pueden modificar otros usuarios (permisos ${(st.mode & 0o777).toString(8)}). Corrígelo con: chmod 600 "${path}"`);
|
|
95
108
|
}
|
|
@@ -98,6 +111,69 @@ export function assertPrivateFile(path, st = statSync(path)) {
|
|
|
98
111
|
throw new Error(`${path} pertenece a otro usuario (uid ${st.uid}). Debe ser tuyo y con permisos 600.`);
|
|
99
112
|
}
|
|
100
113
|
}
|
|
114
|
+
// ── Windows: ACL del archivo ────────────────────────────────────────────────
|
|
115
|
+
/** Quién puede escribir legítimamente, además del propio usuario: SYSTEM, Administradores, TrustedInstaller. */
|
|
116
|
+
const TRUSTED_SIDS = new Set(["S-1-5-18", "S-1-5-32-544", "S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464"]);
|
|
117
|
+
/** Derechos que permiten cambiar el contenido, los permisos o el propietario (nombres de .NET y genéricos numéricos). */
|
|
118
|
+
const WRITE_RIGHTS = /FullControl|Modify|Write|AppendData|CreateFiles|ChangePermissions|TakeOwnership|^(268435456|1073741824)$/;
|
|
119
|
+
/** Entradas que dan escritura a alguien que no es el usuario ni una identidad del sistema. */
|
|
120
|
+
export function aclViolations(entries, userSid) {
|
|
121
|
+
return entries.filter((e) => /allow/i.test(e.type) && e.sid !== userSid && !TRUSTED_SIDS.has(e.sid) && WRITE_RIGHTS.test(e.rights));
|
|
122
|
+
}
|
|
123
|
+
/** Salida del script de PowerShell: «USER|<sid>», «OWNER|<sid>» y «ACE|<sid>|<derechos>|<tipo>». */
|
|
124
|
+
export function parseAclOutput(out) {
|
|
125
|
+
let user = "";
|
|
126
|
+
let owner = "";
|
|
127
|
+
const entries = [];
|
|
128
|
+
for (const line of out.split(/\r?\n/)) {
|
|
129
|
+
const [kind, sid, rights, type] = line.trim().split("|");
|
|
130
|
+
if (kind === "USER")
|
|
131
|
+
user = sid;
|
|
132
|
+
else if (kind === "OWNER")
|
|
133
|
+
owner = sid;
|
|
134
|
+
else if (kind === "ACE" && sid)
|
|
135
|
+
entries.push({ sid, rights: rights ?? "", type: type ?? "" });
|
|
136
|
+
}
|
|
137
|
+
return { user, owner, entries };
|
|
138
|
+
}
|
|
139
|
+
// La ruta llega por variable de entorno, nunca dentro del comando: sin inyección posible.
|
|
140
|
+
const ACL_SCRIPT = [
|
|
141
|
+
"$ErrorActionPreference='Stop'",
|
|
142
|
+
"$a=Get-Acl -LiteralPath $env:ABAPDZ_ACL_PATH",
|
|
143
|
+
"'USER|'+[Security.Principal.WindowsIdentity]::GetCurrent().User.Value",
|
|
144
|
+
"'OWNER|'+(New-Object Security.Principal.NTAccount($a.Owner)).Translate([Security.Principal.SecurityIdentifier]).Value",
|
|
145
|
+
"foreach($r in $a.Access){ $s=try{$r.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value}catch{$r.IdentityReference.Value}; 'ACE|'+$s+'|'+$r.FileSystemRights+'|'+$r.AccessControlType }",
|
|
146
|
+
].join("; ");
|
|
147
|
+
/**
|
|
148
|
+
* systems.json define qué proceso lanza cada componente (sidecars.command): quien pueda escribirlo ejecuta código
|
|
149
|
+
* con tu identidad. En Windows se exige que solo tu usuario (y el sistema) pueda modificarlo. Si la comprobación no
|
|
150
|
+
* se puede hacer, no se arranca (fail-closed), salvo ABAP_DZ_SKIP_ACL_CHECK=1, que queda avisado al arrancar.
|
|
151
|
+
*/
|
|
152
|
+
export function assertPrivateFileWindows(path, run = () => execFileSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", ACL_SCRIPT], {
|
|
153
|
+
encoding: "utf8",
|
|
154
|
+
timeout: 15_000,
|
|
155
|
+
env: { ...process.env, ABAPDZ_ACL_PATH: path },
|
|
156
|
+
})) {
|
|
157
|
+
if (process.env.ABAP_DZ_SKIP_ACL_CHECK === "1")
|
|
158
|
+
return;
|
|
159
|
+
let acl;
|
|
160
|
+
try {
|
|
161
|
+
acl = parseAclOutput(run());
|
|
162
|
+
}
|
|
163
|
+
catch (e) {
|
|
164
|
+
throw new Error(`No se pudieron leer los permisos de ${path} (${e.message.split("\n")[0]}). Sin esa comprobación no se arranca; si lo asumes, define ABAP_DZ_SKIP_ACL_CHECK=1.`);
|
|
165
|
+
}
|
|
166
|
+
if (!acl.user)
|
|
167
|
+
throw new Error(`No se pudo determinar el usuario actual para comprobar ${path}.`);
|
|
168
|
+
if (acl.owner && acl.owner !== acl.user && !TRUSTED_SIDS.has(acl.owner)) {
|
|
169
|
+
throw new Error(`${path} pertenece a otra identidad (${acl.owner}). Debe ser tuyo.`);
|
|
170
|
+
}
|
|
171
|
+
const bad = aclViolations(acl.entries, acl.user);
|
|
172
|
+
if (bad.length) {
|
|
173
|
+
throw new Error(`${path} lo pueden modificar otras identidades (${bad.map((b) => `${b.sid}: ${b.rights}`).join("; ")}). ` +
|
|
174
|
+
`Déjalo solo para tu usuario: icacls "${path}" /inheritance:r /grant:r "%USERNAME%":F`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
101
177
|
/** Escritura efectiva: hace falta allowWrite y ser un sistema de desarrollo. */
|
|
102
178
|
export function canWrite(s) {
|
|
103
179
|
return s.allowWrite && s.role === "DEV";
|
|
@@ -133,4 +209,16 @@ export function resolveSystem(cfg, requested) {
|
|
|
133
209
|
return { system: cfg.systems[0], source: "único configurado" };
|
|
134
210
|
throw new Error(`Hay varios sistemas y ninguno por defecto: indica "system" (${cfg.systems.map((x) => x.id).join(", ")})`);
|
|
135
211
|
}
|
|
212
|
+
/** Avisos que se escriben por stderr en cada arranque: lo que la configuración deja más débil de lo normal. */
|
|
213
|
+
export function startupWarnings(cfg, platform = process.platform) {
|
|
214
|
+
const out = [];
|
|
215
|
+
for (const s of cfg.systems) {
|
|
216
|
+
if (s.allowSelfSigned && !s.caFile)
|
|
217
|
+
out.push(`${s.id}: TLS SIN VERIFICAR (allowSelfSigned). Configura caFile con el certificado de su CA.`);
|
|
218
|
+
}
|
|
219
|
+
if (platform === "win32" && process.env.ABAP_DZ_SKIP_ACL_CHECK === "1") {
|
|
220
|
+
out.push("Windows: ABAP_DZ_SKIP_ACL_CHECK=1 desactiva la comprobación de permisos de systems.json (que define los procesos que se lanzan). Quítala en cuanto puedas.");
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
136
224
|
//# sourceMappingURL=config.js.map
|
package/dist/core/confirm.js
CHANGED
|
@@ -22,22 +22,34 @@ function canonical(v) {
|
|
|
22
22
|
export function argsDigest(tool, system, args) {
|
|
23
23
|
return createHash("sha256").update(JSON.stringify([tool, system.toUpperCase(), canonical(args)])).digest("hex");
|
|
24
24
|
}
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* `state` es la huella de lo que el usuario vio en la vista previa (p. ej. el sha256 de la fuente actual en SAP):
|
|
27
|
+
* la tool la compara al escribir para no aplicar un diff sobre algo que cambió entretanto.
|
|
28
|
+
*/
|
|
29
|
+
export function issueToken(tool, system, args, now = Date.now(), state) {
|
|
26
30
|
for (const [t, p] of pending)
|
|
27
31
|
if (p.exp <= now)
|
|
28
32
|
pending.delete(t);
|
|
29
33
|
const token = randomBytes(16).toString("hex");
|
|
30
|
-
pending.set(token, { digest: argsDigest(tool, system, args), exp: now + CONFIRM_TTL_MS });
|
|
34
|
+
pending.set(token, { digest: argsDigest(tool, system, args), exp: now + CONFIRM_TTL_MS, state });
|
|
31
35
|
return token;
|
|
32
36
|
}
|
|
33
|
-
/**
|
|
34
|
-
export function
|
|
37
|
+
/** Como consumeToken, y además devuelve la huella del estado que se vio en la vista previa. */
|
|
38
|
+
export function consumeTokenWithState(token, tool, system, args, now = Date.now()) {
|
|
35
39
|
const p = pending.get(token);
|
|
36
40
|
if (!p)
|
|
37
|
-
return "unknown";
|
|
41
|
+
return { check: "unknown" };
|
|
38
42
|
pending.delete(token);
|
|
39
43
|
if (p.exp <= now)
|
|
40
|
-
return "expired";
|
|
41
|
-
return p.digest === argsDigest(tool, system, args) ? "ok" : "mismatch";
|
|
44
|
+
return { check: "expired" };
|
|
45
|
+
return p.digest === argsDigest(tool, system, args) ? { check: "ok", state: p.state } : { check: "mismatch" };
|
|
46
|
+
}
|
|
47
|
+
/** Consume el token: vale una sola vez, también cuando no coincide. */
|
|
48
|
+
export function consumeToken(token, tool, system, args, now = Date.now()) {
|
|
49
|
+
return consumeTokenWithState(token, tool, system, args, now).check;
|
|
50
|
+
}
|
|
51
|
+
/** Huella de un contenido tal como se mostró: sha256 con saltos de línea normalizados. */
|
|
52
|
+
export function stateOf(content) {
|
|
53
|
+
return createHash("sha256").update(content.replace(/\r\n?/g, "\n")).digest("hex");
|
|
42
54
|
}
|
|
43
55
|
//# sourceMappingURL=confirm.js.map
|
package/dist/core/connection.js
CHANGED
|
@@ -88,7 +88,11 @@ export class SapConnection {
|
|
|
88
88
|
if (!this.basisRelease) {
|
|
89
89
|
this.basisRelease = this.query("SELECT release FROM cvers WHERE component = 'SAP_BASIS'", 1)
|
|
90
90
|
.then((r) => r.values[0]?.RELEASE?.trim())
|
|
91
|
-
.catch(() =>
|
|
91
|
+
.catch(() => {
|
|
92
|
+
// Un fallo pasajero (VPN caída un instante) no deja el dato en blanco para siempre: se reintenta después.
|
|
93
|
+
this.basisRelease = undefined;
|
|
94
|
+
return undefined;
|
|
95
|
+
});
|
|
92
96
|
}
|
|
93
97
|
return this.basisRelease;
|
|
94
98
|
}
|
package/dist/core/credentials.js
CHANGED
|
@@ -4,6 +4,8 @@ import { ToolError } from "./errors.js";
|
|
|
4
4
|
const run = promisify(execFile);
|
|
5
5
|
/** Servicio del llavero de macOS bajo el que se guardan las contraseñas. */
|
|
6
6
|
export const KEYCHAIN_SERVICE = "abap-adt-dozimple";
|
|
7
|
+
/** Contraseñas en memoria con caducidad: sin caché el llavero se leería en cada petición; sin límite, vivirían siempre. */
|
|
8
|
+
const CACHE_TTL_MS = 15 * 60_000;
|
|
7
9
|
const cache = new Map();
|
|
8
10
|
/**
|
|
9
11
|
* Devuelve la contraseña del sistema. Nunca se escribe en logs ni en la
|
|
@@ -11,8 +13,9 @@ const cache = new Map();
|
|
|
11
13
|
*/
|
|
12
14
|
export async function getPassword(s) {
|
|
13
15
|
const hit = cache.get(s.id);
|
|
14
|
-
if (hit)
|
|
15
|
-
return hit;
|
|
16
|
+
if (hit && hit.exp > Date.now())
|
|
17
|
+
return hit.pwd;
|
|
18
|
+
cache.delete(s.id);
|
|
16
19
|
let pwd;
|
|
17
20
|
if (s.password.startsWith("env:")) {
|
|
18
21
|
pwd = process.env[s.password.slice(4)];
|
|
@@ -34,11 +37,15 @@ export async function getPassword(s) {
|
|
|
34
37
|
throw new ToolError("AUTH", `No hay contraseña para ${s.id} en el llavero. Guárdala con: scripts/set-password.sh ${s.id}`);
|
|
35
38
|
}
|
|
36
39
|
}
|
|
37
|
-
cache.set(s.id, pwd);
|
|
40
|
+
cache.set(s.id, { pwd, exp: Date.now() + CACHE_TTL_MS });
|
|
38
41
|
return pwd;
|
|
39
42
|
}
|
|
40
43
|
/** Tras un 401 conviene olvidar la contraseña cacheada por si se cambió. */
|
|
41
44
|
export function forgetPassword(id) {
|
|
42
45
|
cache.delete(id);
|
|
43
46
|
}
|
|
47
|
+
/** Al cerrar el servidor: ninguna contraseña queda en memoria más de lo necesario. */
|
|
48
|
+
export function clearPasswords() {
|
|
49
|
+
cache.clear();
|
|
50
|
+
}
|
|
44
51
|
//# sourceMappingURL=credentials.js.map
|