ensmcp 0.1.0__tar.gz

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.
Files changed (73) hide show
  1. ensmcp-0.1.0/.github/workflows/ci.yml +116 -0
  2. ensmcp-0.1.0/.github/workflows/release.yml +66 -0
  3. ensmcp-0.1.0/.gitignore +15 -0
  4. ensmcp-0.1.0/LICENSE +21 -0
  5. ensmcp-0.1.0/PKG-INFO +253 -0
  6. ensmcp-0.1.0/README.md +231 -0
  7. ensmcp-0.1.0/pylock.toml +1591 -0
  8. ensmcp-0.1.0/pyproject.toml +85 -0
  9. ensmcp-0.1.0/scripts/atomic_write.py +33 -0
  10. ensmcp-0.1.0/scripts/build_guia_808.py +546 -0
  11. ensmcp-0.1.0/scripts/build_snapshot.py +164 -0
  12. ensmcp-0.1.0/scripts/capture_dom.py +230 -0
  13. ensmcp-0.1.0/src/ensmcp/__init__.py +1 -0
  14. ensmcp-0.1.0/src/ensmcp/__main__.py +119 -0
  15. ensmcp-0.1.0/src/ensmcp/data/anexo_ii.json +6352 -0
  16. ensmcp-0.1.0/src/ensmcp/data/guia_808.json +853 -0
  17. ensmcp-0.1.0/src/ensmcp/domain/__init__.py +1 -0
  18. ensmcp-0.1.0/src/ensmcp/domain/applicability.py +80 -0
  19. ensmcp-0.1.0/src/ensmcp/domain/models.py +234 -0
  20. ensmcp-0.1.0/src/ensmcp/domain/queries.py +302 -0
  21. ensmcp-0.1.0/src/ensmcp/domain/repository.py +18 -0
  22. ensmcp-0.1.0/src/ensmcp/guia/__init__.py +1 -0
  23. ensmcp-0.1.0/src/ensmcp/guia/codec.py +135 -0
  24. ensmcp-0.1.0/src/ensmcp/guia/loader.py +48 -0
  25. ensmcp-0.1.0/src/ensmcp/json_codec.py +37 -0
  26. ensmcp-0.1.0/src/ensmcp/mcp_server/__init__.py +1 -0
  27. ensmcp-0.1.0/src/ensmcp/mcp_server/server.py +656 -0
  28. ensmcp-0.1.0/src/ensmcp/package_data.py +22 -0
  29. ensmcp-0.1.0/src/ensmcp/py.typed +0 -0
  30. ensmcp-0.1.0/src/ensmcp/scraping/__init__.py +1 -0
  31. ensmcp-0.1.0/src/ensmcp/scraping/errors.py +23 -0
  32. ensmcp-0.1.0/src/ensmcp/scraping/live_session.py +248 -0
  33. ensmcp-0.1.0/src/ensmcp/scraping/navegable_repository.py +280 -0
  34. ensmcp-0.1.0/src/ensmcp/scraping/norm_texts.py +224 -0
  35. ensmcp-0.1.0/src/ensmcp/scraping/parsers.py +117 -0
  36. ensmcp-0.1.0/src/ensmcp/scraping/persistent_context.py +114 -0
  37. ensmcp-0.1.0/src/ensmcp/scraping/requisitos.py +299 -0
  38. ensmcp-0.1.0/src/ensmcp/scraping/selectors.py +32 -0
  39. ensmcp-0.1.0/src/ensmcp/snapshot/__init__.py +1 -0
  40. ensmcp-0.1.0/src/ensmcp/snapshot/codec.py +271 -0
  41. ensmcp-0.1.0/src/ensmcp/snapshot/repository.py +356 -0
  42. ensmcp-0.1.0/tests/__init__.py +0 -0
  43. ensmcp-0.1.0/tests/conftest.py +50 -0
  44. ensmcp-0.1.0/tests/domain/__init__.py +0 -0
  45. ensmcp-0.1.0/tests/domain/test_declaracion_aplicabilidad.py +377 -0
  46. ensmcp-0.1.0/tests/domain/test_models.py +143 -0
  47. ensmcp-0.1.0/tests/domain/test_queries.py +371 -0
  48. ensmcp-0.1.0/tests/domain/test_repository.py +16 -0
  49. ensmcp-0.1.0/tests/guia/__init__.py +0 -0
  50. ensmcp-0.1.0/tests/guia/test_build_guia_808.py +996 -0
  51. ensmcp-0.1.0/tests/guia/test_guia_808.py +356 -0
  52. ensmcp-0.1.0/tests/mcp_server/__init__.py +0 -0
  53. ensmcp-0.1.0/tests/mcp_server/test_server_integration.py +813 -0
  54. ensmcp-0.1.0/tests/mcp_server/test_server_normalization.py +256 -0
  55. ensmcp-0.1.0/tests/scraping/__init__.py +0 -0
  56. ensmcp-0.1.0/tests/scraping/fixtures/ens.js +1 -0
  57. ensmcp-0.1.0/tests/scraping/fixtures/requisitos.js +1 -0
  58. ensmcp-0.1.0/tests/scraping/test_capture_dom.py +152 -0
  59. ensmcp-0.1.0/tests/scraping/test_live_session.py +296 -0
  60. ensmcp-0.1.0/tests/scraping/test_navegable_repository.py +664 -0
  61. ensmcp-0.1.0/tests/scraping/test_norm_texts.py +468 -0
  62. ensmcp-0.1.0/tests/scraping/test_parsers.py +384 -0
  63. ensmcp-0.1.0/tests/scraping/test_persistent_context.py +165 -0
  64. ensmcp-0.1.0/tests/scraping/test_requisitos.py +358 -0
  65. ensmcp-0.1.0/tests/snapshot/__init__.py +0 -0
  66. ensmcp-0.1.0/tests/snapshot/test_build_snapshot.py +171 -0
  67. ensmcp-0.1.0/tests/snapshot/test_codec.py +464 -0
  68. ensmcp-0.1.0/tests/snapshot/test_repository.py +647 -0
  69. ensmcp-0.1.0/tests/support.py +451 -0
  70. ensmcp-0.1.0/tests/test_architecture.py +190 -0
  71. ensmcp-0.1.0/tests/test_json_codec.py +64 -0
  72. ensmcp-0.1.0/tests/test_main.py +344 -0
  73. ensmcp-0.1.0/tests/test_packaging.py +163 -0
@@ -0,0 +1,116 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ # Least privilege by default. Neither job writes anything back to the repo,
10
+ # so a read-only GITHUB_TOKEN is enough. Without this block the token
11
+ # inherits the repository/organisation default, which on older repos is
12
+ # read/write - and both jobs run third-party code (pip install executes
13
+ # package build hooks, patchright downloads browser binaries), so a
14
+ # compromised dependency would otherwise get a token that can push.
15
+ permissions:
16
+ contents: read
17
+
18
+ jobs:
19
+ quality:
20
+ name: Quality & security gates
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ # Actions are pinned to an immutable commit SHA, not a tag: tags are
24
+ # mutable, so @v4 silently follows whatever the action owner (or anyone
25
+ # who compromises their account) later points it at. The trailing
26
+ # comment keeps the human-readable version visible.
27
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
28
+ with:
29
+ # Do not leave the token in .git/config for later steps to read.
30
+ persist-credentials: false
31
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
32
+ with:
33
+ python-version: "3.14"
34
+ cache: pip
35
+ # Install from the PEP 751 lock, not by re-resolving pyproject.toml: every
36
+ # package is pinned to an exact version and verified against the sha256 in
37
+ # pylock.toml, so a tampered or yanked-and-replaced release cannot slip
38
+ # into CI. The local package goes in separately with --no-deps so it does
39
+ # not drag an unlocked resolution back in.
40
+ - run: pip install -r pylock.toml
41
+ - run: pip install -e . --no-deps
42
+ - run: black --check .
43
+ - run: ruff check .
44
+ - run: mypy .
45
+ # bandit needs -c pyproject.toml to pick up [tool.bandit] (venv exclusion).
46
+ - run: bandit -c pyproject.toml -r .
47
+ - run: pip-audit
48
+
49
+ tests:
50
+ # The offline suite (pytest -m "not network") reaches 100% coverage on its
51
+ # own using bundled headless Chromium against local fixture pages, so it can
52
+ # run on every supported platform. The live-site suite stays in its own
53
+ # Linux-only job below: it needs headed Chrome plus xvfb, and hitting the
54
+ # real ENS site once per push is enough.
55
+ name: Tests (${{ matrix.os }})
56
+ runs-on: ${{ matrix.os }}
57
+ strategy:
58
+ fail-fast: false
59
+ matrix:
60
+ os: [ubuntu-latest, macos-latest, windows-latest, windows-11-arm]
61
+ steps:
62
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
63
+ with:
64
+ persist-credentials: false
65
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
66
+ with:
67
+ python-version: "3.14"
68
+ # cryptography (mcp -> pyjwt[crypto]) publishes no win_arm64 wheels,
69
+ # so an ARM-native Python cannot install the lock without a
70
+ # Rust+OpenSSL source build. x64 Python under Windows emulation is
71
+ # what a Windows-on-ARM user gets today, and it is what this job
72
+ # verifies. Empty string means "runner default" for the other OSes.
73
+ architecture: ${{ matrix.os == 'windows-11-arm' && 'x64' || '' }}
74
+ cache: pip
75
+ # Ubuntu 24.04 images restrict unprivileged user namespaces via
76
+ # AppArmor, which kills Chromium's sandbox at launch. Re-enabling them
77
+ # is the workaround Chromium documents for CI:
78
+ # https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md
79
+ - if: runner.os == 'Linux'
80
+ run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
81
+ - run: pip install -r pylock.toml
82
+ - run: pip install -e . --no-deps
83
+ # System libraries for Chromium; only Linux needs (or supports) this step.
84
+ - if: runner.os == 'Linux'
85
+ run: patchright install-deps chromium
86
+ - run: patchright install chromium
87
+ - run: pytest -m "not network"
88
+
89
+ tests-live:
90
+ name: Tests (100% coverage, live ENS site)
91
+ runs-on: ubuntu-latest
92
+ steps:
93
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
94
+ with:
95
+ persist-credentials: false
96
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
97
+ with:
98
+ python-version: "3.14"
99
+ cache: pip
100
+ # Real Google Chrome for the network tests, which use headed
101
+ # channel="chrome" to get past the ENS site's WAF (it blocks headless).
102
+ - uses: browser-actions/setup-chrome@c785b87e244131f27c9f19c1a33e2ead956ab7ce # v1
103
+ with:
104
+ chrome-version: stable
105
+ # Same AppArmor/user-namespace workaround as the matrix job above.
106
+ - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
107
+ # Same hash-verified install as the quality job (see the note there).
108
+ - run: pip install -r pylock.toml
109
+ - run: pip install -e . --no-deps
110
+ # Bundled Chromium (headless, channel=None) for the local_site tests.
111
+ - run: patchright install-deps chromium
112
+ - run: patchright install chromium
113
+ # xvfb gives headed Chrome a display; headless Chromium ignores it.
114
+ # The full suite enforces --cov-fail-under=100, so every test (including
115
+ # the network ones against gobernanza.ccn-cert.cni.es) must run here.
116
+ - run: xvfb-run pytest
@@ -0,0 +1,66 @@
1
+ name: Release
2
+
3
+ # Releases are cut by pushing a version tag: bump `version` in pyproject.toml,
4
+ # tag the commit `vX.Y.Z` and push the tag. No API tokens are stored anywhere:
5
+ # publishing uses PyPI trusted publishing (OIDC), so PyPI accepts the upload
6
+ # because it comes from this repo's `release.yml` running in the `pypi`
7
+ # environment, and from nowhere else.
8
+
9
+ on:
10
+ push:
11
+ tags: ["v*"]
12
+
13
+ permissions:
14
+ contents: read
15
+
16
+ jobs:
17
+ build:
18
+ name: Build distribution
19
+ runs-on: ubuntu-latest
20
+ steps:
21
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
22
+ with:
23
+ persist-credentials: false
24
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
25
+ with:
26
+ python-version: "3.14"
27
+ # Refuse to publish a tag that does not match the declared version:
28
+ # otherwise PyPI would either reject the upload later or, worse, accept
29
+ # an artifact whose version says something else than the tag.
30
+ - name: Check tag matches pyproject version
31
+ run: |
32
+ version="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')"
33
+ test "v${version}" = "${GITHUB_REF_NAME}" || {
34
+ echo "Tag ${GITHUB_REF_NAME} does not match pyproject version ${version}" >&2
35
+ exit 1
36
+ }
37
+ - run: pip install build
38
+ - run: python -m build
39
+ # Smoke-test the built wheel in a clean venv. CI installs the project in
40
+ # editable mode, so nothing else ever exercises the packaged artifact:
41
+ # this is the only check that the wheel actually ships the snapshot data.
42
+ - name: Smoke-test wheel
43
+ run: |
44
+ python -m venv /tmp/smoke
45
+ /tmp/smoke/bin/pip install dist/*.whl
46
+ /tmp/smoke/bin/python -c "from importlib.resources import files; import ensmcp; assert (files('ensmcp') / 'data' / 'anexo_ii.json').is_file()"
47
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
48
+ with:
49
+ name: dist
50
+ path: dist/
51
+
52
+ publish:
53
+ name: Publish to PyPI (OIDC)
54
+ needs: build
55
+ runs-on: ubuntu-latest
56
+ environment: pypi
57
+ permissions:
58
+ # The OIDC token is the credential: PyPI's trusted publisher for this
59
+ # project must point at this repo, this workflow and the environment.
60
+ id-token: write
61
+ steps:
62
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
63
+ with:
64
+ name: dist
65
+ path: dist/
66
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
@@ -0,0 +1,15 @@
1
+ venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ htmlcov/
8
+ .coverage
9
+ .coverage.*
10
+ *.egg-info/
11
+ build/
12
+ dist/
13
+ *.har
14
+ # Guías CCN-STIC: se commitea el dato extraído, nunca el documento.
15
+ *.pdf
ensmcp-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marc Rivero Lopez
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ensmcp-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,253 @@
1
+ Metadata-Version: 2.5
2
+ Name: ensmcp
3
+ Version: 0.1.0
4
+ Summary: Servidor MCP con las medidas de seguridad del ENS (Anexo II del RD 311/2022): Anexo II, Declaración de Aplicabilidad y alcance de auditoría, sin conexión
5
+ Author-email: "Marc Rivero Lopez | @seifreed" <mriverolopez@gmail.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: >=3.14
9
+ Requires-Dist: mcp>=2.0.0
10
+ Requires-Dist: patchright>=1.61.2
11
+ Provides-Extra: dev
12
+ Requires-Dist: bandit>=1.8; extra == 'dev'
13
+ Requires-Dist: black>=24.10; extra == 'dev'
14
+ Requires-Dist: mypy>=1.13; extra == 'dev'
15
+ Requires-Dist: packaging>=26.3; extra == 'dev'
16
+ Requires-Dist: pip-audit>=2.7; extra == 'dev'
17
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
18
+ Requires-Dist: pytest-cov>=6.0; extra == 'dev'
19
+ Requires-Dist: pytest>=8.3; extra == 'dev'
20
+ Requires-Dist: ruff>=0.7; extra == 'dev'
21
+ Description-Content-Type: text/markdown
22
+
23
+ <p align="center">
24
+ <img src="https://img.shields.io/badge/ensmcp-MCP%20Server%20ENS%20Navegable-blue?style=for-the-badge" alt="ensmcp">
25
+ </p>
26
+
27
+ <h1 align="center">ensmcp</h1>
28
+
29
+ <p align="center">
30
+ <strong>Servidor MCP con las medidas de seguridad del ENS (Anexo II del RD 311/2022): consulta el Anexo II, genera la Declaración de Aplicabilidad y el alcance de auditoría de un sistema, y funciona sin conexión</strong>
31
+ </p>
32
+
33
+ <p align="center">
34
+ <a href="https://www.python.org/downloads/"><img src="https://img.shields.io/badge/python-3.14-blue?style=flat-square&logo=python&logoColor=white" alt="Python Version"></a>
35
+ <a href="https://github.com/seifreed/ensmcp/actions"><img src="https://img.shields.io/github/actions/workflow/status/seifreed/ensmcp/ci.yml?style=flat-square&logo=github&label=CI" alt="CI Status"></a>
36
+ <a href="https://modelcontextprotocol.io/"><img src="https://img.shields.io/badge/MCP-2.0-black?style=flat-square" alt="MCP"></a>
37
+ </p>
38
+
39
+ <p align="center">
40
+ <a href="https://github.com/seifreed/ensmcp/stargazers"><img src="https://img.shields.io/github/stars/seifreed/ensmcp?style=flat-square" alt="GitHub Stars"></a>
41
+ <a href="https://github.com/seifreed/ensmcp/issues"><img src="https://img.shields.io/github/issues/seifreed/ensmcp?style=flat-square" alt="GitHub Issues"></a>
42
+ <a href="https://buymeacoffee.com/seifreed"><img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-support-yellow?style=flat-square&logo=buy-me-a-coffee&logoColor=white" alt="Buy Me a Coffee"></a>
43
+ </p>
44
+
45
+ ---
46
+
47
+ ## Qué es
48
+
49
+ **ensmcp** es un servidor [MCP](https://modelcontextprotocol.io/) (Model Context Protocol) que pone las medidas de seguridad del [ENS Navegable](https://gobernanza.ccn-cert.cni.es/ens-navegable) (Anexo II del RD 311/2022) al alcance de Claude Desktop, Claude Code y cualquier otro cliente MCP.
50
+
51
+ Los datos viajan incluidos en el paquete como un snapshot, así que el servidor responde al instante y funciona sin conexión. Al arrancar, comprueba en segundo plano el sitio oficial y, si el contenido ha cambiado, actualiza los datos automáticamente; si no hay red o no hay navegador disponible, sigue funcionando con el snapshot sin más.
52
+
53
+ Además del Anexo II, incorpora el cuestionario de verificación de la **guía CCN-STIC 808** (edición para el RD 311/2022): las preguntas de auditoría por medida, las comprobaciones sobre el articulado del RD y las evidencias documentales que puede pedir el auditor.
54
+
55
+ ### Características principales
56
+
57
+ | Característica | Descripción |
58
+ |----------------|-------------|
59
+ | **Servidor MCP sobre stdio** | Integrable en Claude Desktop, Claude Code y otros clientes MCP |
60
+ | **Funciona sin conexión** | El corpus completo viaja como snapshot en el paquete |
61
+ | **Se mantiene al día** | Una comprobación en segundo plano detecta cambios en el sitio oficial y actualiza los datos |
62
+ | **Nunca bloquea** | Sin red o sin navegador, se sigue respondiendo desde el snapshot |
63
+ | **Declaración de Aplicabilidad** | La DdA completa de un sistema, valorando cada dimensión por separado, en una llamada |
64
+ | **Auditoría CCN-STIC 808** | Temario de auditoría, requisitos esenciales, artículos del RD y evidencias documentales |
65
+
66
+ ## Tools disponibles
67
+
68
+ ### Consulta del Anexo II
69
+
70
+ | Tool | Args | Descripción |
71
+ |------|------|-------------|
72
+ | `list_categories` | — | Las categorías del Anexo II con su grupo (`org`, `op`, `mp`). |
73
+ | `list_measures` | `category_code?`, `dimension?`, `level?` | Medidas filtradas por categoría, dimensión de seguridad o nivel. |
74
+ | `get_measure` | `code` | Una medida por código exacto (p. ej. `"org.1"`), con su texto del RD, refuerzos y niveles. |
75
+ | `search_measures` | `query` | Búsqueda por texto en código, título, descripción y redacción del RD (ignora mayúsculas y tildes). |
76
+
77
+ ### Declaración de Aplicabilidad y auditoría
78
+
79
+ | Tool | Args | Descripción |
80
+ |------|------|-------------|
81
+ | `declaracion_aplicabilidad` | `confidencialidad?`, `integridad?`, `disponibilidad?`, `autenticidad?`, `trazabilidad?` | La DdA de un sistema en una llamada: se valora cada dimensión (`basico`/`medio`/`alto`, u omitida) y devuelve las medidas exigibles con sus refuerzos. |
82
+ | `alcance_auditoria` | mismas que la DdA | El temario de auditoría del sistema: las medidas aplicables con sus preguntas de verificación acumuladas y el nivel de madurez mínimo exigible. |
83
+ | `requisitos_auditoria` | `code?`, `level?` | El cuestionario CCN-STIC 808 en bruto, por medida o por tramo, marcando los requisitos esenciales. |
84
+ | `requisitos_articulos` | — | Las comprobaciones de auditoría sobre el articulado del RD (DdA formal, categorización, INES...). |
85
+ | `evidencias_auditoria` | `code?` | La documentación que puede pedir el auditor, por medida. |
86
+
87
+ ### Estado y actualización
88
+
89
+ | Tool | Args | Descripción |
90
+ |------|------|-------------|
91
+ | `refresh_live_page` | — | Comprueba ahora el sitio oficial y actualiza los datos si han cambiado. |
92
+ | `snapshot_status` | — | Origen y frescura de los datos que se están sirviendo. |
93
+
94
+ ## Qué devuelve una medida
95
+
96
+ Cada medida trae dos textos, y hacen falta los dos: `norm_text` es la redacción del RD 311/2022 (lo que la medida **exige**) y `description` es el cuestionario de la CCN-STIC 808 (lo que el auditor **pregunta**). Los refuerzos vienen emparejados con el nivel que los exige y con su redacción en el RD:
97
+
98
+ ```json
99
+ {
100
+ "code": "mp.s.4",
101
+ "title": "Protección frente a denegación de servicio",
102
+ "description": "Categoría Media 1.1 ¿Se ha planificado y dotado al sistema de capacidad suficiente ...?",
103
+ "norm_text": "Se establecerán medidas preventivas frente a ataques de denegación de servicio ...",
104
+ "category_code": "mp.s",
105
+ "dimensions": ["disponibilidad"],
106
+ "levels": ["medio", "alto"],
107
+ "reinforcements": [
108
+ { "code": "R1", "level": "alto", "alternative": false, "text": "R1-Detección y reacción. ..." }
109
+ ],
110
+ "raw_levels": { "basico": "n.a.", "medio": "aplica", "alto": "+ R1" }
111
+ }
112
+ ```
113
+
114
+ `alternative` distingue los refuerzos obligatorios (`+ R1 + R2`) de los alternativos (`+ [R1 o R2]`, donde basta uno cualquiera): confundirlos cambia lo que hay que implantar.
115
+
116
+ ## Declaración de Aplicabilidad
117
+
118
+ El ENS no aplica un nivel al sistema entero: aplica **uno por dimensión**. Se valora cada una y la tool devuelve lo que ese sistema debe cumplir:
119
+
120
+ ```json
121
+ {
122
+ "categoria_sistema": "alto",
123
+ "measures": [
124
+ { "code": "op.acc.5", "title": "Mecanismo de autenticación (usuarios externos)",
125
+ "required_level": "alto",
126
+ "required_reinforcements": [
127
+ { "code": "R2", "alternative": true, "text": "R2-..." },
128
+ { "code": "R5", "alternative": false, "text": "R5-..." }
129
+ ] }
130
+ ]
131
+ }
132
+ ```
133
+
134
+ La regla sale del RD 311/2022: la categoría del sistema es el mayor de los niveles valorados (Anexo I, ap. 4), las medidas marcadas «Categoría» se exigen según la categoría del sistema, las que protegen dimensiones según el nivel de esas dimensiones, y una dimensión sin valorar deja fuera las medidas que solo la protegen.
135
+
136
+ ## Alcance de auditoría
137
+
138
+ Si la DdA contesta *«¿qué tengo que implantar?»*, `alcance_auditoria` contesta *«¿qué me va a preguntar el auditor?»*. Devuelve, por cada medida aplicable, los requisitos de verificación **acumulados** hasta su nivel exigible y el nivel de madurez mínimo que exige la guía:
139
+
140
+ | Categoría | Nivel mínimo de madurez |
141
+ |---|---|
142
+ | BÁSICA | **L2** — Reproducible, pero intuitivo |
143
+ | MEDIA | **L3** — Proceso definido |
144
+ | ALTA | **L4** — Gestionado y medible |
145
+
146
+ El matiz importa: los tramos que el ENS Navegable etiqueta «Categoría Básica / Media / Alta» son acumulativos según la CCN-STIC 808 §5 — «Categoría Básica» significa *exigible a todas las categorías*, no *solo para sistemas básicos*. Un sistema de categoría media responde las preguntas de básica **y** las de media. Para un sistema C=alto, I=medio, D=bajo, A=medio, T=medio, el temario real son **382 preguntas** (136 esenciales, cuyo incumplimiento bloquea la certificación), no las 73 de su tramo.
147
+
148
+ `requisitos_articulos` y `evidencias_auditoria` cubren la otra mitad de la auditoría: las comprobaciones sobre el articulado del RD (si la DdA existe y está suscrita, si el sistema está categorizado formalmente, si se reporta a INES...) y las 365 evidencias documentales que la guía propone. Estos datos salen de la CCN-STIC 808; el ENS Navegable no los publica.
149
+
150
+ ## De dónde salen los datos
151
+
152
+ - **`src/ensmcp/data/anexo_ii.json`** — el corpus del ENS Navegable (medidas, textos del RD, cuestionario, aplicabilidad por niveles), capturado del sitio oficial. Cada consulta es un lookup en memoria.
153
+ - **`src/ensmcp/data/guia_808.json`** — el dato extraído de la guía CCN-STIC 808 con su atribución (la guía en sí no se redistribuye). `snapshot_status` indica de qué edición procede.
154
+
155
+ Para regenerar el snapshot:
156
+
157
+ ```bash
158
+ python scripts/build_snapshot.py
159
+ ```
160
+
161
+ Para que el servidor no abra Chrome nunca por su cuenta: `ENSMCP_LIVE_CHECK=0`.
162
+
163
+ ## Requisitos
164
+
165
+ - Python **3.14+**
166
+
167
+ Solo para **actualizar** el snapshot (`refresh_live_page`, la comprobación de arranque, `scripts/build_snapshot.py`) hacen falta además:
168
+
169
+ - Google Chrome instalado
170
+ - Un display (o `xvfb` en servidores sin él)
171
+
172
+ ## Instalación
173
+
174
+ ```bash
175
+ git clone https://github.com/seifreed/ensmcp.git
176
+ cd ensmcp
177
+ python3 -m venv venv
178
+ source venv/bin/activate # Windows: venv\Scripts\activate
179
+ pip install -r pylock.toml # versiones exactas, verificadas por hash
180
+ pip install -e . --no-deps # el propio paquete, sin re-resolver
181
+ patchright install chromium
182
+ ```
183
+
184
+ En Linux recién instalado puede hacer falta además:
185
+
186
+ ```bash
187
+ patchright install-deps chromium
188
+ ```
189
+
190
+ `pyproject.toml` es el único sitio donde se declaran las dependencias; `pylock.toml` es un artefacto generado a partir de él ([PEP 751](https://peps.python.org/pep-0751/)) que fija todas las dependencias —runtime y desarrollo juntas— a versión exacta y hash, con marcadores para Windows, Linux y macOS en x64 y ARM. Para actualizar dependencias, edita los rangos en `pyproject.toml` y regenera:
191
+
192
+ ```bash
193
+ uv pip compile pyproject.toml --all-extras --universal --python-version 3.14 \
194
+ --format pylock.toml -o pylock.toml
195
+ ```
196
+
197
+ ## Inicio rápido
198
+
199
+ ```bash
200
+ python -m ensmcp
201
+ ```
202
+
203
+ Configúralo en un cliente MCP (p. ej. Claude Desktop / Claude Code) apuntando al intérprete del entorno virtual:
204
+
205
+ ```json
206
+ {
207
+ "mcpServers": {
208
+ "ensmcp": {
209
+ "command": "/ruta/a/ensmcp/venv/bin/python",
210
+ "args": ["-m", "ensmcp"]
211
+ }
212
+ }
213
+ }
214
+ ```
215
+
216
+ No hace falta configurar nada más: las consultas se responden desde el snapshot del paquete.
217
+
218
+ Para inspeccionarlo manualmente:
219
+
220
+ ```bash
221
+ npx @modelcontextprotocol/inspector venv/bin/python -m ensmcp
222
+ ```
223
+
224
+ ## Contribuir
225
+
226
+ Las contribuciones son bienvenidas.
227
+
228
+ 1. Haz un fork del repositorio
229
+ 2. Crea tu rama de funcionalidad (`git checkout -b feature/nueva-funcionalidad`)
230
+ 3. Haz commit de tus cambios (`git commit -m 'Añade nueva funcionalidad'`)
231
+ 4. Sube la rama (`git push origin feature/nueva-funcionalidad`)
232
+ 5. Abre un Pull Request
233
+
234
+ Asegúrate de que todas las gates de calidad y seguridad pasen sin errores ni warnings antes de enviar el PR.
235
+
236
+ ## Apoya el proyecto
237
+
238
+ Si este proyecto te es útil, puedes apoyar su desarrollo:
239
+
240
+ <a href="https://buymeacoffee.com/seifreed" target="_blank">
241
+ <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" height="50">
242
+ </a>
243
+
244
+ ## Autor
245
+
246
+ - **Marc Rivero López** | [@seifreed](https://github.com/seifreed)
247
+ - Repositorio: [github.com/seifreed/ensmcp](https://github.com/seifreed/ensmcp)
248
+
249
+ ---
250
+
251
+ <p align="center">
252
+ <sub>Las medidas del ENS, accesibles por MCP</sub>
253
+ </p>