iso27001-mcp 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -33
- package/dist/index.js +116 -67
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# iso27001-mcp
|
|
2
2
|
|
|
3
|
-
[](https://socket.dev/npm/package/iso27001-mcp/overview/0.8.2)
|
|
4
4
|
[](https://npmjs.com/package/iso27001-mcp)
|
|
5
|
+
[](https://npmjs.com/package/iso27001-mcp)
|
|
5
6
|
[](https://sushegaad.github.io/MCP-Server-for-ISO27001/)
|
|
6
7
|
|
|
7
8
|
**[▶ Live Interactive Demo](https://sushegaad.github.io/MCP-Server-for-ISO27001/)**
|
|
@@ -31,12 +32,28 @@ Claude ──MCP──► iso27001-mcp ──► encrypted SQLite (isms.db)
|
|
|
31
32
|
- [Configuration](#configuration)
|
|
32
33
|
- [Connecting to Claude](#connecting-to-claude)
|
|
33
34
|
- [Tools Reference](#tools-reference)
|
|
35
|
+
- [Group 1 — Control Registry](#group-1--control-registry-minimum-role-viewer)
|
|
36
|
+
- [Group 2 — Gap Analysis](#group-2--gap-analysis-reads-viewer-writes-analyst)
|
|
37
|
+
- [Group 3 — Risk Management](#group-3--risk-management-reads-viewer-writes-analyst)
|
|
38
|
+
- [Group 4 — Policy Management](#group-4--policy-management-reads-viewer-create-analyst-update-admin)
|
|
39
|
+
- [Group 5 — Statement of Applicability](#group-5--statement-of-applicability-minimum-role-analyst)
|
|
40
|
+
- [Group 6 — Audit Management](#group-6--audit-management-reads-viewer-writes-admin)
|
|
41
|
+
- [Group 7 — Evidence Tracking](#group-7--evidence-tracking-reads-viewer-writes-analyst)
|
|
42
|
+
- [Group 8 — Server Info](#group-8--server-info-minimum-role-viewer)
|
|
43
|
+
- [Group 9 — Admin & Key Management](#group-9--admin--key-management-minimum-role-admin)
|
|
44
|
+
- [Group 10 — Organisation Profile](#group-10--organisation-profile-minimum-role-admin-for-writes-viewer-for-reads)
|
|
45
|
+
- [Group 11 — Procedure Management](#group-11--procedure-management-reads-viewer-createexport-analyst-update-admin)
|
|
46
|
+
- [Group 12 — Management Review](#group-12--management-review-reads-viewer-writes-admin--clause-93)
|
|
47
|
+
- [Group 13 — Improvement Plan](#group-13--improvement-plan-reads-viewer-writes-analyst--clause-101)
|
|
48
|
+
- [Group 14 — Evidence Templates](#group-14--evidence-templates-reads-viewer-generate-analyst)
|
|
34
49
|
- [MCP Resources](#mcp-resources)
|
|
35
50
|
- [Architecture](#architecture)
|
|
36
51
|
- [Modes](#modes)
|
|
37
52
|
- [Integrations](#integrations)
|
|
53
|
+
- [Sample Outputs](#sample-outputs)
|
|
38
54
|
- [Development](#development)
|
|
39
55
|
- [Security](#security)
|
|
56
|
+
- [Trust Center](https://github.com/Sushegaad/MCP-Server-for-ISO27001/tree/main/docs/security/) — threat model · hardening guide · data flow · supply chain · audit log integrity
|
|
40
57
|
|
|
41
58
|
---
|
|
42
59
|
|
|
@@ -90,7 +107,7 @@ iso27001-mcp keygen --label "Me" --role admin
|
|
|
90
107
|
|
|
91
108
|
The raw key (`iso27001_...`) is printed **once** and never stored in plaintext. Copy it immediately.
|
|
92
109
|
|
|
93
|
-
> Three roles are available: `viewer` (
|
|
110
|
+
> Three roles are available: `viewer` (31 read-only tools), `analyst` (49 tools), `admin` (all 63 tools). Use `admin` for your personal key.
|
|
94
111
|
|
|
95
112
|
### Step 4 — Add to Claude Desktop
|
|
96
113
|
|
|
@@ -121,7 +138,7 @@ Add the following block, substituting your values from Steps 2 and 3:
|
|
|
121
138
|
|
|
122
139
|
### Step 5 — Restart Claude Desktop and verify
|
|
123
140
|
|
|
124
|
-
Fully quit and reopen Claude Desktop. You should see
|
|
141
|
+
Fully quit and reopen Claude Desktop. You should see 63 tools in the MCP tools panel (hammer icon). Then ask Claude:
|
|
125
142
|
|
|
126
143
|
> *"Use get_server_info to check the server is running."*
|
|
127
144
|
|
|
@@ -287,11 +304,12 @@ Full variable reference:
|
|
|
287
304
|
| `HMAC_SECRET` | ✅ | — | 32-byte hex secret for HMAC-signing API keys |
|
|
288
305
|
| `DB_ENCRYPTION_KEY` | ✅ | — | 32-byte hex key for AES-256 SQLite encryption |
|
|
289
306
|
| `DB_PATH` | | `./isms.db` | Path to the encrypted database file |
|
|
290
|
-
| `AUDIT_LOG_PATH` | | `./audit.
|
|
307
|
+
| `AUDIT_LOG_PATH` | | `./audit.jsonl` | Path for the append-only JSON-L audit log (`.jsonl` or `.log` only) |
|
|
291
308
|
| `RATE_LIMIT_RPM` | | `500` | Tool calls per minute per API key |
|
|
292
309
|
| `SESSION_TTL_HOURS` | | `4` | SSE session TTL (hosted/team modes) |
|
|
293
310
|
| `SSE_PORT` | | `3000` | Port for the SSE server (hosted/team modes) |
|
|
294
311
|
| `BEHIND_TLS_PROXY` | | `false` | Set `true` when behind nginx/Caddy in production |
|
|
312
|
+
| `CORS_ORIGIN` | | `http://localhost` (dev) / `https://claude.ai` (prod) | Allowed CORS origin for the SSE server — never set to `*` |
|
|
295
313
|
| `JIRA_BASE_URL` | | — | e.g. `https://your-org.atlassian.net` |
|
|
296
314
|
| `JIRA_API_TOKEN` | | — | Jira API token for the integration |
|
|
297
315
|
| `JIRA_PROJECT_KEY` | | — | e.g. `SEC` |
|
|
@@ -307,10 +325,10 @@ The server requires an API key on every tool call. Generate one for yourself:
|
|
|
307
325
|
# Viewer — read-only access to 25 tools
|
|
308
326
|
iso27001-mcp keygen --label "Alice" --role viewer
|
|
309
327
|
|
|
310
|
-
# Analyst — read + write for gap/risk/policy/procedure/evidence tools (
|
|
328
|
+
# Analyst — read + write for gap/risk/policy/procedure/evidence tools (49 tools)
|
|
311
329
|
iso27001-mcp keygen --label "Bob" --role analyst --expires 90d
|
|
312
330
|
|
|
313
|
-
# Admin — all
|
|
331
|
+
# Admin — all 63 tools including audit log and key management
|
|
314
332
|
iso27001-mcp keygen --label "CISO" --role admin --expires 1y
|
|
315
333
|
```
|
|
316
334
|
|
|
@@ -375,7 +393,7 @@ export DB_PATH=$HOME/.iso27001/isms.db
|
|
|
375
393
|
|
|
376
394
|
## Tools Reference
|
|
377
395
|
|
|
378
|
-
The server exposes **
|
|
396
|
+
The server exposes **63 tools** across 14 groups. All tools require a valid API key. The minimum role required is noted per group; `✅` marks required parameters, `—` marks optional ones.
|
|
379
397
|
|
|
380
398
|
---
|
|
381
399
|
|
|
@@ -913,6 +931,147 @@ Export a procedure as Markdown or JSON.
|
|
|
913
931
|
|
|
914
932
|
---
|
|
915
933
|
|
|
934
|
+
### Group 12 — Management Review *(reads: viewer+, writes: admin)* — Clause 9.3
|
|
935
|
+
|
|
936
|
+
#### `create_management_review`
|
|
937
|
+
Schedule a management review meeting.
|
|
938
|
+
|
|
939
|
+
| Parameter | Req | Type | Values / Notes |
|
|
940
|
+
|-----------|-----|------|----------------|
|
|
941
|
+
| `title` | ✅ | string | Review title |
|
|
942
|
+
| `review_date` | ✅ | string | `YYYY-MM-DD` |
|
|
943
|
+
| `chair` | ✅ | string | Review chair / CISO name |
|
|
944
|
+
| `attendees` | — | array | List of attendee names |
|
|
945
|
+
| `agenda` | — | string | Meeting agenda |
|
|
946
|
+
|
|
947
|
+
#### `record_review_input`
|
|
948
|
+
Record an input item to a management review (e.g. audit results, risk summary, performance metrics).
|
|
949
|
+
|
|
950
|
+
| Parameter | Req | Type | Values / Notes |
|
|
951
|
+
|-----------|-----|------|----------------|
|
|
952
|
+
| `review_id` | ✅ | string (UUID) | |
|
|
953
|
+
| `input_type` | ✅ | enum | `audit_results` \| `risk_summary` \| `objective_performance` \| `nonconformities` \| `previous_actions` \| `changes` \| `resources` \| `stakeholder_feedback` \| `other` |
|
|
954
|
+
| `summary` | ✅ | string | |
|
|
955
|
+
| `detail` | — | string | Supporting detail |
|
|
956
|
+
|
|
957
|
+
#### `record_review_output`
|
|
958
|
+
Record a decision or action item from a management review.
|
|
959
|
+
|
|
960
|
+
| Parameter | Req | Type | Values / Notes |
|
|
961
|
+
|-----------|-----|------|----------------|
|
|
962
|
+
| `review_id` | ✅ | string (UUID) | |
|
|
963
|
+
| `output_type` | ✅ | enum | `improvement_opportunity` \| `resource_decision` \| `policy_change` \| `objective_change` \| `other` |
|
|
964
|
+
| `description` | ✅ | string | |
|
|
965
|
+
| `owner` | — | string | |
|
|
966
|
+
| `due_date` | — | string | `YYYY-MM-DD` |
|
|
967
|
+
|
|
968
|
+
#### `complete_management_review`
|
|
969
|
+
Mark a management review as complete and record the outcome.
|
|
970
|
+
|
|
971
|
+
| Parameter | Req | Type | Values / Notes |
|
|
972
|
+
|-----------|-----|------|----------------|
|
|
973
|
+
| `review_id` | ✅ | string (UUID) | |
|
|
974
|
+
| `outcome_summary` | ✅ | string | |
|
|
975
|
+
|
|
976
|
+
#### `get_management_review`
|
|
977
|
+
Fetch a management review with all inputs, outputs, and status.
|
|
978
|
+
|
|
979
|
+
| Parameter | Req | Type | Values / Notes |
|
|
980
|
+
|-----------|-----|------|----------------|
|
|
981
|
+
| `review_id` | ✅ | string (UUID) | |
|
|
982
|
+
|
|
983
|
+
#### `list_management_reviews`
|
|
984
|
+
List management reviews with optional status filter.
|
|
985
|
+
|
|
986
|
+
| Parameter | Req | Type | Values / Notes |
|
|
987
|
+
|-----------|-----|------|----------------|
|
|
988
|
+
| `status` | — | enum | `scheduled` \| `in_progress` \| `completed` |
|
|
989
|
+
| `limit` | — | integer | Default: `20`, max `100` |
|
|
990
|
+
| `offset` | — | integer | Default: `0` |
|
|
991
|
+
|
|
992
|
+
---
|
|
993
|
+
|
|
994
|
+
### Group 13 — Improvement Plan *(reads: viewer+, writes: analyst+)* — Clause 10.1
|
|
995
|
+
|
|
996
|
+
#### `create_improvement_opportunity`
|
|
997
|
+
Register an improvement opportunity, typically identified during a management review or audit.
|
|
998
|
+
|
|
999
|
+
| Parameter | Req | Type | Values / Notes |
|
|
1000
|
+
|-----------|-----|------|----------------|
|
|
1001
|
+
| `title` | ✅ | string | |
|
|
1002
|
+
| `description` | ✅ | string | |
|
|
1003
|
+
| `source` | ✅ | enum | `audit` \| `management_review` \| `incident` \| `risk_assessment` \| `self_assessment` \| `other` |
|
|
1004
|
+
| `priority` | — | enum | `low` \| `medium` \| `high` \| `critical` — default: `medium` |
|
|
1005
|
+
| `owner` | — | string | |
|
|
1006
|
+
| `due_date` | — | string | `YYYY-MM-DD` |
|
|
1007
|
+
| `related_controls` | — | array | Control IDs |
|
|
1008
|
+
| `review_id` | — | string (UUID) | Link to a management review output |
|
|
1009
|
+
|
|
1010
|
+
#### `update_improvement_opportunity`
|
|
1011
|
+
Update the status, owner, or due date of an improvement opportunity.
|
|
1012
|
+
|
|
1013
|
+
| Parameter | Req | Type | Values / Notes |
|
|
1014
|
+
|-----------|-----|------|----------------|
|
|
1015
|
+
| `opportunity_id` | ✅ | string (UUID) | |
|
|
1016
|
+
| `status` | — | enum | `open` \| `in_progress` \| `completed` \| `cancelled` |
|
|
1017
|
+
| `owner` | — | string | |
|
|
1018
|
+
| `due_date` | — | string | `YYYY-MM-DD` |
|
|
1019
|
+
| `resolution_notes` | — | string | Required when closing |
|
|
1020
|
+
|
|
1021
|
+
#### `get_improvement_opportunity`
|
|
1022
|
+
Fetch a single improvement opportunity by ID.
|
|
1023
|
+
|
|
1024
|
+
| Parameter | Req | Type | Values / Notes |
|
|
1025
|
+
|-----------|-----|------|----------------|
|
|
1026
|
+
| `opportunity_id` | ✅ | string (UUID) | |
|
|
1027
|
+
|
|
1028
|
+
#### `list_improvement_opportunities`
|
|
1029
|
+
List improvement opportunities with optional filters.
|
|
1030
|
+
|
|
1031
|
+
| Parameter | Req | Type | Values / Notes |
|
|
1032
|
+
|-----------|-----|------|----------------|
|
|
1033
|
+
| `status` | — | enum | `open` \| `in_progress` \| `completed` \| `cancelled` |
|
|
1034
|
+
| `priority` | — | enum | `low` \| `medium` \| `high` \| `critical` |
|
|
1035
|
+
| `source` | — | enum | Any source enum value above |
|
|
1036
|
+
| `limit` | — | integer | Default: `50`, max `100` |
|
|
1037
|
+
| `offset` | — | integer | Default: `0` |
|
|
1038
|
+
|
|
1039
|
+
---
|
|
1040
|
+
|
|
1041
|
+
### Group 14 — Evidence Templates *(reads: viewer+, generate: analyst+)*
|
|
1042
|
+
|
|
1043
|
+
#### `generate_evidence_document`
|
|
1044
|
+
Render a Mustache evidence template and store it. The document is dual-written to both the `evidence` table and the `generated_evidence` table for tracking and version history.
|
|
1045
|
+
|
|
1046
|
+
| Parameter | Req | Type | Values / Notes |
|
|
1047
|
+
|-----------|-----|------|----------------|
|
|
1048
|
+
| `template_type` | ✅ | enum | `access_review_attestation` \| `bcp_test_report` \| `incident_post_mortem` \| `risk_treatment_sign_off` \| `supplier_security_questionnaire` \| `training_acknowledgement` |
|
|
1049
|
+
| `title` | ✅ | string | Document title |
|
|
1050
|
+
| `generated_by` | ✅ | string | Author or system that generated the document |
|
|
1051
|
+
| `organisation_name` | — | string | Auto-injected from org profile if set |
|
|
1052
|
+
| `control_id` | — | string | Link to a specific control (default: `general`) |
|
|
1053
|
+
| `vars` | — | object | Additional Mustache template variables |
|
|
1054
|
+
|
|
1055
|
+
#### `get_evidence_document`
|
|
1056
|
+
Fetch a generated evidence document by ID, including rendered content and clause/control mappings.
|
|
1057
|
+
|
|
1058
|
+
| Parameter | Req | Type | Values / Notes |
|
|
1059
|
+
|-----------|-----|------|----------------|
|
|
1060
|
+
| `document_id` | ✅ | string (UUID) | |
|
|
1061
|
+
|
|
1062
|
+
#### `list_evidence_documents`
|
|
1063
|
+
List generated evidence documents with optional filters.
|
|
1064
|
+
|
|
1065
|
+
| Parameter | Req | Type | Values / Notes |
|
|
1066
|
+
|-----------|-----|------|----------------|
|
|
1067
|
+
| `template_type` | — | enum | Filter to a specific template type |
|
|
1068
|
+
| `generated_by` | — | string | Filter by author |
|
|
1069
|
+
| `control_id` | — | string | Filter by linked control |
|
|
1070
|
+
| `limit` | — | integer | Default: `20`, max `100` |
|
|
1071
|
+
| `offset` | — | integer | Default: `0` |
|
|
1072
|
+
|
|
1073
|
+
---
|
|
1074
|
+
|
|
916
1075
|
## MCP Resources
|
|
917
1076
|
|
|
918
1077
|
In addition to tools, the server exposes ISMS artefacts as browseable **MCP Resources** under the `iso27001://` URI scheme. Claude can reference these directly without a tool call — ideal for inline document review, cross-referencing controls, and long-context analysis.
|
|
@@ -967,34 +1126,37 @@ Resources are read-only. Write operations always go through tools (which enforce
|
|
|
967
1126
|
│ Claude (LLM) │
|
|
968
1127
|
└──────────┬───────────────────────────────┬──────────────┘
|
|
969
1128
|
│ MCP Tools (read/write) │ MCP Resources (read-only)
|
|
970
|
-
│
|
|
1129
|
+
│ 63 tools, RBAC enforced │ 12 iso27001:// URIs
|
|
971
1130
|
┌──────────▼───────────────────────────────▼──────────────┐
|
|
972
1131
|
│ iso27001-mcp server │
|
|
973
1132
|
│ │
|
|
974
1133
|
│ ┌─────────────────────────────────────────────────┐ │
|
|
975
|
-
│ │
|
|
1134
|
+
│ │ 7-Step Security Pipeline │ │
|
|
976
1135
|
│ │ │ │
|
|
977
|
-
│ │ 1. Extract
|
|
978
|
-
│ │ 2. validateKey()
|
|
1136
|
+
│ │ 1. Extract credential (_meta.apiKey / env) │ │
|
|
1137
|
+
│ │ 2. Auth — session token OR validateKey() │ │
|
|
1138
|
+
│ │ SSE sessions use opaque token (no raw key) │ │
|
|
979
1139
|
│ │ 3. checkRateLimit() sliding 60s window (RPM) │ │
|
|
980
|
-
│ │ 4.
|
|
981
|
-
│ │ 5.
|
|
982
|
-
│ │ 6.
|
|
983
|
-
│ │ 7.
|
|
984
|
-
│ │ 8. writeAuditEvent() tamper-evident row_hash │ │
|
|
985
|
-
│ │ 9. Return result or structured McpError │ │
|
|
1140
|
+
│ │ 4. assertPermission() RBAC check │ │
|
|
1141
|
+
│ │ 5. sanitiseParams() strip injection patterns │ │
|
|
1142
|
+
│ │ 6. Domain handler business logic │ │
|
|
1143
|
+
│ │ 7. writeAuditEvent() HMAC chain + row_hash │ │
|
|
986
1144
|
│ └─────────────────────────────────────────────────┘ │
|
|
987
1145
|
│ │
|
|
988
1146
|
│ ┌─────────────┐ ┌──────────┐ ┌────────────────────┐ │
|
|
989
1147
|
│ │ Controls │ │ Risks │ │ Policies & │ │
|
|
990
1148
|
│ │ Gap Assess │ │ Register │ │ Procedures │ │
|
|
991
|
-
│ │ SoA │ │ Treatmts │ │ (Mustache
|
|
1149
|
+
│ │ SoA │ │ Treatmts │ │ (Mustache+partls) │ │
|
|
992
1150
|
│ └─────────────┘ └──────────┘ └────────────────────┘ │
|
|
993
1151
|
│ ┌─────────────┐ ┌──────────┐ ┌────────────────────┐ │
|
|
994
|
-
│ │ Audits │ │ Evidence │ │
|
|
995
|
-
│ │ Findings │ │ Jira/GH │ │
|
|
996
|
-
│ │ CARs │ │
|
|
1152
|
+
│ │ Audits │ │ Evidence │ │ Mgmt Review & │ │
|
|
1153
|
+
│ │ Findings │ │ Jira/GH │ │ Improvement Plan │ │
|
|
1154
|
+
│ │ CARs │ │ Tmplts │ │ (Clauses 9.3/10.1)│ │
|
|
997
1155
|
│ └─────────────┘ └──────────┘ └────────────────────┘ │
|
|
1156
|
+
│ ┌─────────────────────────────────────────────────┐ │
|
|
1157
|
+
│ │ Org Profile · Audit Log (HMAC-SHA256 chain) │ │
|
|
1158
|
+
│ │ Session Token Store · API Key RBAC (63 tools) │ │
|
|
1159
|
+
│ └─────────────────────────────────────────────────┘ │
|
|
998
1160
|
│ │
|
|
999
1161
|
│ ┌─────────────────────────────────────────────────┐ │
|
|
1000
1162
|
│ │ AES-256 encrypted SQLite (isms.db) │ │
|
|
@@ -1006,11 +1168,14 @@ Resources are read-only. Write operations always go through tools (which enforce
|
|
|
1006
1168
|
|
|
1007
1169
|
### Database
|
|
1008
1170
|
|
|
1009
|
-
All data is stored in a single encrypted SQLite file (`isms.db`) using AES-256 via `better-sqlite3-multiple-ciphers`. The schema is managed by
|
|
1171
|
+
All data is stored in a single encrypted SQLite file (`isms.db`) using AES-256 via `better-sqlite3-multiple-ciphers`. The schema is managed by six SQL migrations applied automatically on first startup:
|
|
1010
1172
|
|
|
1011
1173
|
- `0001_initial.sql` — 17 tables covering every ISMS domain (controls, gap assessments, risks, policies, audits, evidence, API keys, audit log, and more)
|
|
1012
1174
|
- `0002_fts_index.sql` — FTS5 full-text search index on controls, plus 12 performance indexes
|
|
1013
1175
|
- `0003_org_profile_procedures.sql` — `organization_profile` singleton table, `procedures` table, and `procedure_versions` history table
|
|
1176
|
+
- `0004_management_review_improvement.sql` — `management_reviews`, `review_inputs`, `review_outputs`, and `improvement_opportunities` tables (Clauses 9.3 and 10.1)
|
|
1177
|
+
- `0005_evidence_documents.sql` — `generated_evidence` table for Mustache-rendered evidence documents with dual-write to `evidence`
|
|
1178
|
+
- `0006_audit_log_hmac.sql` — adds `prev_hash` column to `audit_log` for HMAC chain integrity
|
|
1014
1179
|
|
|
1015
1180
|
### Seed Data
|
|
1016
1181
|
|
|
@@ -1023,7 +1188,7 @@ On first startup, `seedAll()` inserts all ISO 27001 reference data and verifies
|
|
|
1023
1188
|
|
|
1024
1189
|
### Security Pipeline
|
|
1025
1190
|
|
|
1026
|
-
Every tool call passes through the same
|
|
1191
|
+
Every tool call passes through the same 7-step pipeline before any business logic runs. SSE sessions use an opaque session token so the raw API key is never retained in server memory after the initial `/sse` handshake. Audit events are always written — including on authentication failure and RBAC denial — so the log is a complete record of all attempts, not just successful ones.
|
|
1027
1192
|
|
|
1028
1193
|
### Business Rules Enforced
|
|
1029
1194
|
|
|
@@ -1044,9 +1209,9 @@ Three roles with strict hierarchy. A key can only call tools at or below its ass
|
|
|
1044
1209
|
|
|
1045
1210
|
| Role | Tools available | Typical user |
|
|
1046
1211
|
|------|----------------|--------------|
|
|
1047
|
-
| `viewer` |
|
|
1048
|
-
| `analyst` |
|
|
1049
|
-
| `admin` |
|
|
1212
|
+
| `viewer` | 31 (all read-only tools) | Auditor, stakeholder |
|
|
1213
|
+
| `analyst` | 49 (reads + gap/risk/policy/procedure/evidence/improvement writes) | ISMS practitioner, consultant |
|
|
1214
|
+
| `admin` | 63 (all tools, including org profile, audit management, audit log and key management) | CISO, ISMS owner |
|
|
1050
1215
|
|
|
1051
1216
|
---
|
|
1052
1217
|
|
|
@@ -1087,6 +1252,24 @@ Sessions expire after `SESSION_TTL_HOURS` hours of inactivity. In `NODE_ENV=prod
|
|
|
1087
1252
|
|
|
1088
1253
|
---
|
|
1089
1254
|
|
|
1255
|
+
## Sample Outputs
|
|
1256
|
+
|
|
1257
|
+
The [`samples/`](samples/) directory contains auditor-ready example outputs generated from a demo ISMS for a fictitious organisation ("Acme Financial Services Ltd" — a UK payments processor preparing for ISO 27001:2022 certification). Each file states which tool(s) produced it.
|
|
1258
|
+
|
|
1259
|
+
| Sample | Description |
|
|
1260
|
+
|--------|-------------|
|
|
1261
|
+
| [gap-assessment-summary.md](samples/gap-assessment-summary.md) | Complete gap assessment across all 93 controls |
|
|
1262
|
+
| [remediation-roadmap.md](samples/remediation-roadmap.md) | 26-week prioritised remediation plan with owners and effort estimates |
|
|
1263
|
+
| [risk-register.csv](samples/risk-register.csv) | Risk register with 10 risks, scores, and treatment plans |
|
|
1264
|
+
| [statement-of-applicability.csv](samples/statement-of-applicability.csv) | Full SoA — all 93 ISO 27001:2022 controls with applicability justifications |
|
|
1265
|
+
| [access-control-policy.md](samples/access-control-policy.md) | Generated access control policy (Annex A 5.15–5.18, 8.2–8.5) |
|
|
1266
|
+
| [incident-handling-procedure.md](samples/incident-handling-procedure.md) | Incident handling procedure with severity tiers and GDPR notification |
|
|
1267
|
+
| [internal-audit-report.md](samples/internal-audit-report.md) | Internal audit report — 3 major NCs, 4 minor NCs, 2 positive observations |
|
|
1268
|
+
| [corrective-action-record.md](samples/corrective-action-record.md) | Two corrective action records: one in progress, one closed and verified |
|
|
1269
|
+
| [evidence-package.md](samples/evidence-package.md) | 47-item evidence inventory with 28-control gap analysis |
|
|
1270
|
+
|
|
1271
|
+
---
|
|
1272
|
+
|
|
1090
1273
|
## Integrations
|
|
1091
1274
|
|
|
1092
1275
|
### Jira
|
|
@@ -1123,7 +1306,7 @@ npm run typecheck
|
|
|
1123
1306
|
# Build dist/
|
|
1124
1307
|
npm run build
|
|
1125
1308
|
|
|
1126
|
-
# Run all tests (
|
|
1309
|
+
# Run all tests (470 unit + integration tests)
|
|
1127
1310
|
npm test
|
|
1128
1311
|
|
|
1129
1312
|
# Watch mode
|
|
@@ -1147,12 +1330,13 @@ src/
|
|
|
1147
1330
|
├── server.ts McpServer factory — registers tools + resources
|
|
1148
1331
|
├── auth/
|
|
1149
1332
|
│ ├── api-key.ts Key generation, HMAC validation, expiry, revocation
|
|
1150
|
-
│
|
|
1333
|
+
│ ├── rbac.ts Permission matrix (63 tools × 3 roles)
|
|
1334
|
+
│ └── session-store.ts SSE session token store (opaque token → keyHash + role)
|
|
1151
1335
|
├── security/
|
|
1152
1336
|
│ ├── sanitise.ts Prompt-injection stripping for free-text fields
|
|
1153
1337
|
│ ├── rate-limiter.ts Sliding-window RPM counter per key hash
|
|
1154
1338
|
│ ├── secrets.ts Env var validation (fail-fast on startup)
|
|
1155
|
-
│ └── validate.ts Zod schemas for all
|
|
1339
|
+
│ └── validate.ts Zod schemas for all 63 tool inputs
|
|
1156
1340
|
├── audit/
|
|
1157
1341
|
│ └── logger.ts Tamper-evident audit event writer
|
|
1158
1342
|
├── db/
|
|
@@ -1166,7 +1350,9 @@ src/
|
|
|
1166
1350
|
│ ├── version-mapping.json 125 cross-version mappings
|
|
1167
1351
|
│ ├── clause-requirements.json 41 clause requirements (clauses 4–10)
|
|
1168
1352
|
│ ├── policy-templates/ 12 Mustache .md policy templates
|
|
1169
|
-
│
|
|
1353
|
+
│ ├── procedure-templates/ 12 Mustache .md procedure templates
|
|
1354
|
+
│ ├── evidence-templates/ 6 Mustache .md evidence document templates
|
|
1355
|
+
│ └── partials/ Shared Mustache partials (org_header, revision_block, approver_signature)
|
|
1170
1356
|
├── tools/
|
|
1171
1357
|
│ ├── index.ts Tool registry and security pipeline
|
|
1172
1358
|
│ ├── controls.ts Group 1: Control Registry (7 tools)
|
|
@@ -1179,7 +1365,10 @@ src/
|
|
|
1179
1365
|
│ ├── server-info.ts Group 8: Server Info (1 tool)
|
|
1180
1366
|
│ ├── org-profile.ts Group 10: Organisation Profile (2 tools) + loadOrgProfileDefaults helper
|
|
1181
1367
|
│ ├── procedures.ts Group 11: Procedure Management (5 tools)
|
|
1182
|
-
│
|
|
1368
|
+
│ ├── management-review.ts Group 12: Management Review — Clause 9.3 (6 tools)
|
|
1369
|
+
│ ├── improvement-plan.ts Group 13: Improvement Plan — Clause 10.1 (4 tools)
|
|
1370
|
+
│ ├── evidence-templates.ts Group 14: Evidence Templates (3 tools)
|
|
1371
|
+
│ └── template-utils.ts Shared loadTemplate / stripFrontmatter / loadPartials helpers
|
|
1183
1372
|
├── resources/
|
|
1184
1373
|
│ ├── index.ts Registers all 12 MCP Resources
|
|
1185
1374
|
│ ├── resource-auth.ts Slim auth helper for resource callbacks
|
|
@@ -1212,6 +1401,8 @@ tests/
|
|
|
1212
1401
|
|
|
1213
1402
|
## Security
|
|
1214
1403
|
|
|
1404
|
+
For a full security profile — threat model, hardening guide, data flow documentation, supply chain attestation, and audit log integrity verification — see the **[Trust Center](https://github.com/Sushegaad/MCP-Server-for-ISO27001/tree/main/docs/security/)**.
|
|
1405
|
+
|
|
1215
1406
|
### API Key Storage
|
|
1216
1407
|
|
|
1217
1408
|
API keys are never stored in plaintext. Only an HMAC-SHA256 hash is persisted in the database. The raw `iso27001_...` key is printed once to stdout at generation time — there is no way to retrieve it afterwards.
|
|
@@ -1225,10 +1416,11 @@ The SQLite database (`isms.db`) is encrypted at rest using AES-256 via `better-s
|
|
|
1225
1416
|
Every tool call writes a row to `audit_log` with a `row_hash` computed as:
|
|
1226
1417
|
|
|
1227
1418
|
```
|
|
1228
|
-
|
|
1419
|
+
HMAC-SHA256(HMAC_SECRET, id | timestamp | tool | key_hash | role |
|
|
1420
|
+
params_json | outcome | error_message | duration_ms | prev_hash)
|
|
1229
1421
|
```
|
|
1230
1422
|
|
|
1231
|
-
|
|
1423
|
+
The `prev_hash` field chains each row to its predecessor — insertion, deletion, or reordering of rows is detectable via `verifyRowHash()` and `verifyChain()`. The same events are appended in JSON-L format to `AUDIT_LOG_PATH` for off-database retention and SIEM ingestion. The log path is validated on write to reject paths inside system directories (`/etc`, `/proc`, `/sys`, `/dev`).
|
|
1232
1424
|
|
|
1233
1425
|
### Production Checklist
|
|
1234
1426
|
|
package/dist/index.js
CHANGED
|
@@ -4947,10 +4947,10 @@ var require_raw_body = __commonJS({
|
|
|
4947
4947
|
if (done) {
|
|
4948
4948
|
return readStream(stream, encoding, length, limit, wrap(done));
|
|
4949
4949
|
}
|
|
4950
|
-
return new Promise(function executor(
|
|
4950
|
+
return new Promise(function executor(resolve2, reject) {
|
|
4951
4951
|
readStream(stream, encoding, length, limit, function onRead(err, buf) {
|
|
4952
4952
|
if (err) return reject(err);
|
|
4953
|
-
|
|
4953
|
+
resolve2(buf);
|
|
4954
4954
|
});
|
|
4955
4955
|
});
|
|
4956
4956
|
}
|
|
@@ -14034,7 +14034,7 @@ var require_mime_types = __commonJS({
|
|
|
14034
14034
|
"node_modules/mime-types/index.js"(exports2) {
|
|
14035
14035
|
"use strict";
|
|
14036
14036
|
var db = require_mime_db();
|
|
14037
|
-
var
|
|
14037
|
+
var extname2 = require("path").extname;
|
|
14038
14038
|
var EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/;
|
|
14039
14039
|
var TEXT_TYPE_REGEXP = /^text\//i;
|
|
14040
14040
|
exports2.charset = charset;
|
|
@@ -14088,7 +14088,7 @@ var require_mime_types = __commonJS({
|
|
|
14088
14088
|
if (!path || typeof path !== "string") {
|
|
14089
14089
|
return false;
|
|
14090
14090
|
}
|
|
14091
|
-
var extension2 =
|
|
14091
|
+
var extension2 = extname2("x." + path).toLowerCase().substr(1);
|
|
14092
14092
|
if (!extension2) {
|
|
14093
14093
|
return false;
|
|
14094
14094
|
}
|
|
@@ -20181,14 +20181,14 @@ var require_view = __commonJS({
|
|
|
20181
20181
|
var fs = require("fs");
|
|
20182
20182
|
var dirname = path.dirname;
|
|
20183
20183
|
var basename = path.basename;
|
|
20184
|
-
var
|
|
20184
|
+
var extname2 = path.extname;
|
|
20185
20185
|
var join2 = path.join;
|
|
20186
|
-
var
|
|
20186
|
+
var resolve2 = path.resolve;
|
|
20187
20187
|
module2.exports = View;
|
|
20188
20188
|
function View(name, options) {
|
|
20189
20189
|
var opts = options || {};
|
|
20190
20190
|
this.defaultEngine = opts.defaultEngine;
|
|
20191
|
-
this.ext =
|
|
20191
|
+
this.ext = extname2(name);
|
|
20192
20192
|
this.name = name;
|
|
20193
20193
|
this.root = opts.root;
|
|
20194
20194
|
if (!this.ext && !this.defaultEngine) {
|
|
@@ -20217,7 +20217,7 @@ var require_view = __commonJS({
|
|
|
20217
20217
|
debug('lookup "%s"', name);
|
|
20218
20218
|
for (var i = 0; i < roots.length && !path2; i++) {
|
|
20219
20219
|
var root = roots[i];
|
|
20220
|
-
var loc =
|
|
20220
|
+
var loc = resolve2(root, name);
|
|
20221
20221
|
var dir = dirname(loc);
|
|
20222
20222
|
var file = basename(loc);
|
|
20223
20223
|
path2 = this.resolve(dir, file);
|
|
@@ -20228,7 +20228,7 @@ var require_view = __commonJS({
|
|
|
20228
20228
|
debug('render "%s"', this.path);
|
|
20229
20229
|
this.engine(this.path, options, callback);
|
|
20230
20230
|
};
|
|
20231
|
-
View.prototype.resolve = function
|
|
20231
|
+
View.prototype.resolve = function resolve3(dir, file) {
|
|
20232
20232
|
var ext = this.ext;
|
|
20233
20233
|
var path2 = join2(dir, file);
|
|
20234
20234
|
var stat = tryStat(path2);
|
|
@@ -21299,10 +21299,10 @@ var require_send = __commonJS({
|
|
|
21299
21299
|
var statuses = require_statuses();
|
|
21300
21300
|
var Stream = require("stream");
|
|
21301
21301
|
var util = require("util");
|
|
21302
|
-
var
|
|
21302
|
+
var extname2 = path.extname;
|
|
21303
21303
|
var join2 = path.join;
|
|
21304
21304
|
var normalize = path.normalize;
|
|
21305
|
-
var
|
|
21305
|
+
var resolve2 = path.resolve;
|
|
21306
21306
|
var sep = path.sep;
|
|
21307
21307
|
var BYTES_RANGE_REGEXP = /^ *bytes=/;
|
|
21308
21308
|
var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1e3;
|
|
@@ -21339,7 +21339,7 @@ var require_send = __commonJS({
|
|
|
21339
21339
|
this._maxage = opts.maxAge || opts.maxage;
|
|
21340
21340
|
this._maxage = typeof this._maxage === "string" ? ms(this._maxage) : Number(this._maxage);
|
|
21341
21341
|
this._maxage = !isNaN(this._maxage) ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE) : 0;
|
|
21342
|
-
this._root = opts.root ?
|
|
21342
|
+
this._root = opts.root ? resolve2(opts.root) : null;
|
|
21343
21343
|
if (!this._root && opts.from) {
|
|
21344
21344
|
this.from(opts.from);
|
|
21345
21345
|
}
|
|
@@ -21363,7 +21363,7 @@ var require_send = __commonJS({
|
|
|
21363
21363
|
return this;
|
|
21364
21364
|
}, "send.index: pass index as option");
|
|
21365
21365
|
SendStream.prototype.root = function root(path2) {
|
|
21366
|
-
this._root =
|
|
21366
|
+
this._root = resolve2(String(path2));
|
|
21367
21367
|
debug("root %s", this._root);
|
|
21368
21368
|
return this;
|
|
21369
21369
|
};
|
|
@@ -21527,7 +21527,7 @@ var require_send = __commonJS({
|
|
|
21527
21527
|
return res;
|
|
21528
21528
|
}
|
|
21529
21529
|
parts = normalize(path2).split(sep);
|
|
21530
|
-
path2 =
|
|
21530
|
+
path2 = resolve2(path2);
|
|
21531
21531
|
}
|
|
21532
21532
|
if (containsDotFile(parts)) {
|
|
21533
21533
|
var access = this._dotfiles;
|
|
@@ -21624,7 +21624,7 @@ var require_send = __commonJS({
|
|
|
21624
21624
|
var self = this;
|
|
21625
21625
|
debug('stat "%s"', path2);
|
|
21626
21626
|
fs.stat(path2, function onstat(err, stat) {
|
|
21627
|
-
if (err && err.code === "ENOENT" && !
|
|
21627
|
+
if (err && err.code === "ENOENT" && !extname2(path2) && path2[path2.length - 1] !== sep) {
|
|
21628
21628
|
return next(err);
|
|
21629
21629
|
}
|
|
21630
21630
|
if (err) return self.onStatError(err);
|
|
@@ -22807,7 +22807,7 @@ var require_application = __commonJS({
|
|
|
22807
22807
|
var deprecate = require_depd()("express");
|
|
22808
22808
|
var flatten = require_array_flatten();
|
|
22809
22809
|
var merge = require_utils_merge();
|
|
22810
|
-
var
|
|
22810
|
+
var resolve2 = require("path").resolve;
|
|
22811
22811
|
var setPrototypeOf = require_setprototypeof();
|
|
22812
22812
|
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
22813
22813
|
var slice = Array.prototype.slice;
|
|
@@ -22846,7 +22846,7 @@ var require_application = __commonJS({
|
|
|
22846
22846
|
this.mountpath = "/";
|
|
22847
22847
|
this.locals.settings = this.settings;
|
|
22848
22848
|
this.set("view", View);
|
|
22849
|
-
this.set("views",
|
|
22849
|
+
this.set("views", resolve2("views"));
|
|
22850
22850
|
this.set("jsonp callback name", "callback");
|
|
22851
22851
|
if (env === "production") {
|
|
22852
22852
|
this.enable("view cache");
|
|
@@ -24090,9 +24090,9 @@ var require_response = __commonJS({
|
|
|
24090
24090
|
var setCharset = require_utils3().setCharset;
|
|
24091
24091
|
var cookie = require_cookie();
|
|
24092
24092
|
var send = require_send();
|
|
24093
|
-
var
|
|
24093
|
+
var extname2 = path.extname;
|
|
24094
24094
|
var mime = send.mime;
|
|
24095
|
-
var
|
|
24095
|
+
var resolve2 = path.resolve;
|
|
24096
24096
|
var vary = require_vary();
|
|
24097
24097
|
var res = Object.create(http.ServerResponse.prototype);
|
|
24098
24098
|
module2.exports = res;
|
|
@@ -24351,7 +24351,7 @@ var require_response = __commonJS({
|
|
|
24351
24351
|
}
|
|
24352
24352
|
opts = Object.create(opts);
|
|
24353
24353
|
opts.headers = headers;
|
|
24354
|
-
var fullPath = !opts.root ?
|
|
24354
|
+
var fullPath = !opts.root ? resolve2(path2) : path2;
|
|
24355
24355
|
return this.sendFile(fullPath, opts, done);
|
|
24356
24356
|
};
|
|
24357
24357
|
res.contentType = res.type = function contentType(type) {
|
|
@@ -24382,7 +24382,7 @@ var require_response = __commonJS({
|
|
|
24382
24382
|
};
|
|
24383
24383
|
res.attachment = function attachment(filename) {
|
|
24384
24384
|
if (filename) {
|
|
24385
|
-
this.type(
|
|
24385
|
+
this.type(extname2(filename));
|
|
24386
24386
|
}
|
|
24387
24387
|
this.set("Content-Disposition", contentDisposition(filename));
|
|
24388
24388
|
return this;
|
|
@@ -24617,7 +24617,7 @@ var require_serve_static = __commonJS({
|
|
|
24617
24617
|
var encodeUrl = require_encodeurl();
|
|
24618
24618
|
var escapeHtml = require_escape_html();
|
|
24619
24619
|
var parseUrl = require_parseurl();
|
|
24620
|
-
var
|
|
24620
|
+
var resolve2 = require("path").resolve;
|
|
24621
24621
|
var send = require_send();
|
|
24622
24622
|
var url = require("url");
|
|
24623
24623
|
module2.exports = serveStatic;
|
|
@@ -24637,7 +24637,7 @@ var require_serve_static = __commonJS({
|
|
|
24637
24637
|
throw new TypeError("option setHeaders must be function");
|
|
24638
24638
|
}
|
|
24639
24639
|
opts.maxage = opts.maxage || opts.maxAge || 0;
|
|
24640
|
-
opts.root =
|
|
24640
|
+
opts.root = resolve2(root);
|
|
24641
24641
|
var onDirectory = redirect ? createRedirectDirectoryListener() : createNotFoundDirectoryListener();
|
|
24642
24642
|
return function serveStatic2(req, res, next) {
|
|
24643
24643
|
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
@@ -24797,7 +24797,7 @@ var require_package = __commonJS({
|
|
|
24797
24797
|
"package.json"(exports2, module2) {
|
|
24798
24798
|
module2.exports = {
|
|
24799
24799
|
name: "iso27001-mcp",
|
|
24800
|
-
version: "0.8.
|
|
24800
|
+
version: "0.8.2",
|
|
24801
24801
|
description: "Stateful ISO 27001:2022 ISMS management for Claude \u2014 gap analysis, risk register, policies, audits, and evidence tracking via the Model Context Protocol",
|
|
24802
24802
|
license: "MIT",
|
|
24803
24803
|
repository: {
|
|
@@ -24840,7 +24840,6 @@ var require_package = __commonJS({
|
|
|
24840
24840
|
postbuild: "rm -rf dist/seed && mkdir -p dist/seed && cp -r src/seed/policy-templates dist/seed/policy-templates && cp -r src/seed/procedure-templates dist/seed/procedure-templates && cp -r src/seed/evidence-templates dist/seed/evidence-templates && cp -r src/seed/partials dist/seed/partials",
|
|
24841
24841
|
prepack: "npm run build",
|
|
24842
24842
|
prepublishOnly: "npm run typecheck && npm test && npm run build",
|
|
24843
|
-
postinstall: `node -e "require('better-sqlite3-multiple-ciphers')" 2>/dev/null || echo "\\n\u26A0\uFE0F iso27001-mcp: Native SQLite module failed to load. You may need build tools installed.\\n macOS: xcode-select --install\\n Ubuntu/Debian: sudo apt-get install build-essential python3\\n Windows: https://visualstudio.microsoft.com/downloads/ \u2192 Build Tools for Visual Studio \u2192 Desktop development with C++\\n See: https://github.com/Sushegaad/MCP-Server-for-ISO27001#prerequisites\\n"`,
|
|
24844
24843
|
typecheck: "tsc --noEmit",
|
|
24845
24844
|
lint: "eslint src --ext .ts",
|
|
24846
24845
|
test: "vitest run --coverage",
|
|
@@ -25063,15 +25062,15 @@ var validations = {
|
|
|
25063
25062
|
/**
|
|
25064
25063
|
* Ensures a single store instance is not used with multiple express-rate-limit instances
|
|
25065
25064
|
*/
|
|
25066
|
-
unsharedStore(
|
|
25067
|
-
if (usedStores.has(
|
|
25068
|
-
const maybeUniquePrefix =
|
|
25065
|
+
unsharedStore(store2) {
|
|
25066
|
+
if (usedStores.has(store2)) {
|
|
25067
|
+
const maybeUniquePrefix = store2?.localKeys ? "" : " (with a unique prefix)";
|
|
25069
25068
|
throw new ValidationError(
|
|
25070
25069
|
"ERR_ERL_STORE_REUSE",
|
|
25071
|
-
`A Store instance must not be shared across multiple rate limiters. Create a new instance of ${
|
|
25070
|
+
`A Store instance must not be shared across multiple rate limiters. Create a new instance of ${store2.constructor.name}${maybeUniquePrefix} for each limiter instead.`
|
|
25072
25071
|
);
|
|
25073
25072
|
}
|
|
25074
|
-
usedStores.add(
|
|
25073
|
+
usedStores.add(store2);
|
|
25075
25074
|
},
|
|
25076
25075
|
/**
|
|
25077
25076
|
* Ensures a given key is incremented only once per request.
|
|
@@ -25082,19 +25081,19 @@ var validations = {
|
|
|
25082
25081
|
*
|
|
25083
25082
|
* @returns {void}
|
|
25084
25083
|
*/
|
|
25085
|
-
singleCount(request,
|
|
25084
|
+
singleCount(request, store2, key) {
|
|
25086
25085
|
let storeKeys = singleCountKeys.get(request);
|
|
25087
25086
|
if (!storeKeys) {
|
|
25088
25087
|
storeKeys = /* @__PURE__ */ new Map();
|
|
25089
25088
|
singleCountKeys.set(request, storeKeys);
|
|
25090
25089
|
}
|
|
25091
|
-
const storeKey =
|
|
25090
|
+
const storeKey = store2.localKeys ? store2 : store2.constructor.name;
|
|
25092
25091
|
let keys = storeKeys.get(storeKey);
|
|
25093
25092
|
if (!keys) {
|
|
25094
25093
|
keys = [];
|
|
25095
25094
|
storeKeys.set(storeKey, keys);
|
|
25096
25095
|
}
|
|
25097
|
-
const prefixedKey = `${
|
|
25096
|
+
const prefixedKey = `${store2.prefix ?? ""}${key}`;
|
|
25098
25097
|
if (keys.includes(prefixedKey)) {
|
|
25099
25098
|
throw new ValidationError(
|
|
25100
25099
|
"ERR_ERL_DOUBLE_COUNT",
|
|
@@ -25212,12 +25211,12 @@ var validations = {
|
|
|
25212
25211
|
* which would prevent it from working correctly, with the default memory
|
|
25213
25212
|
* store (or any other store with localKeys.)
|
|
25214
25213
|
*/
|
|
25215
|
-
creationStack(
|
|
25214
|
+
creationStack(store2) {
|
|
25216
25215
|
const { stack } = new Error(
|
|
25217
25216
|
"express-rate-limit validation check (set options.validate.creationStack=false to disable)"
|
|
25218
25217
|
);
|
|
25219
25218
|
if (stack?.includes("Layer.handle [as handle_request]")) {
|
|
25220
|
-
if (!
|
|
25219
|
+
if (!store2.localKeys) {
|
|
25221
25220
|
throw new ValidationError(
|
|
25222
25221
|
"ERR_ERL_CREATED_IN_REQUEST_HANDLER",
|
|
25223
25222
|
"express-rate-limit instance should *usually* be created at app initialization, not when responding to a request."
|
|
@@ -25403,10 +25402,10 @@ var MemoryStore = class {
|
|
|
25403
25402
|
this.current = /* @__PURE__ */ new Map();
|
|
25404
25403
|
}
|
|
25405
25404
|
};
|
|
25406
|
-
var isLegacyStore = (
|
|
25405
|
+
var isLegacyStore = (store2) => (
|
|
25407
25406
|
// Check that `incr` exists but `increment` does not - store authors might want
|
|
25408
25407
|
// to keep both around for backwards compatibility.
|
|
25409
|
-
typeof
|
|
25408
|
+
typeof store2.incr === "function" && typeof store2.increment !== "function"
|
|
25410
25409
|
);
|
|
25411
25410
|
var promisifyStore = (passedStore) => {
|
|
25412
25411
|
if (!isLegacyStore(passedStore)) {
|
|
@@ -25415,12 +25414,12 @@ var promisifyStore = (passedStore) => {
|
|
|
25415
25414
|
const legacyStore = passedStore;
|
|
25416
25415
|
class PromisifiedStore {
|
|
25417
25416
|
async increment(key) {
|
|
25418
|
-
return new Promise((
|
|
25417
|
+
return new Promise((resolve2, reject) => {
|
|
25419
25418
|
legacyStore.incr(
|
|
25420
25419
|
key,
|
|
25421
25420
|
(error, totalHits, resetTime) => {
|
|
25422
25421
|
if (error) reject(error);
|
|
25423
|
-
|
|
25422
|
+
resolve2({ totalHits, resetTime });
|
|
25424
25423
|
}
|
|
25425
25424
|
);
|
|
25426
25425
|
});
|
|
@@ -26605,6 +26604,25 @@ function parseExpiresFlag(value) {
|
|
|
26605
26604
|
);
|
|
26606
26605
|
}
|
|
26607
26606
|
|
|
26607
|
+
// src/auth/session-store.ts
|
|
26608
|
+
var import_node_crypto5 = require("crypto");
|
|
26609
|
+
var SESSION_TOKEN_PREFIX = "iso27001_sess_";
|
|
26610
|
+
var store = /* @__PURE__ */ new Map();
|
|
26611
|
+
function createSessionToken(keyHash, role) {
|
|
26612
|
+
const token = `${SESSION_TOKEN_PREFIX}${(0, import_node_crypto5.randomUUID)()}`;
|
|
26613
|
+
store.set(token, { keyHash, role });
|
|
26614
|
+
return token;
|
|
26615
|
+
}
|
|
26616
|
+
function lookupSessionToken(token) {
|
|
26617
|
+
return store.get(token);
|
|
26618
|
+
}
|
|
26619
|
+
function removeSessionToken(token) {
|
|
26620
|
+
store.delete(token);
|
|
26621
|
+
}
|
|
26622
|
+
function isSessionToken(value) {
|
|
26623
|
+
return value.startsWith(SESSION_TOKEN_PREFIX);
|
|
26624
|
+
}
|
|
26625
|
+
|
|
26608
26626
|
// src/transport/sse.ts
|
|
26609
26627
|
var sessions = /* @__PURE__ */ new Map();
|
|
26610
26628
|
var ttlMs = parseInt(process.env["SESSION_TTL_HOURS"] ?? "4") * 60 * 60 * 1e3;
|
|
@@ -26616,6 +26634,7 @@ setInterval(() => {
|
|
|
26616
26634
|
entry.transport.res?.end();
|
|
26617
26635
|
} catch {
|
|
26618
26636
|
}
|
|
26637
|
+
removeSessionToken(entry.sessionToken);
|
|
26619
26638
|
sessions.delete(sessionId);
|
|
26620
26639
|
console.error(`[iso27001-mcp] Session ${sessionId} expired and removed.`);
|
|
26621
26640
|
}
|
|
@@ -26631,7 +26650,7 @@ function startSseServer(server) {
|
|
|
26631
26650
|
}
|
|
26632
26651
|
const app = (0, import_express.default)();
|
|
26633
26652
|
app.use((req, res, next) => {
|
|
26634
|
-
const allowedOrigin = isProduction ? "https://claude.ai" : "
|
|
26653
|
+
const allowedOrigin = process.env["CORS_ORIGIN"] ?? (isProduction ? "https://claude.ai" : "http://localhost");
|
|
26635
26654
|
res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
|
|
26636
26655
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
26637
26656
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
@@ -26656,32 +26675,34 @@ function startSseServer(server) {
|
|
|
26656
26675
|
});
|
|
26657
26676
|
app.get("/sse", async (req, res) => {
|
|
26658
26677
|
const authHeader = req.headers["authorization"];
|
|
26659
|
-
const rawKey =
|
|
26678
|
+
const rawKey = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : "";
|
|
26660
26679
|
let keyHash;
|
|
26680
|
+
let role;
|
|
26661
26681
|
try {
|
|
26662
26682
|
keyHash = validateKey(rawKey);
|
|
26663
|
-
loadRole(keyHash);
|
|
26683
|
+
role = loadRole(keyHash);
|
|
26664
26684
|
} catch (err) {
|
|
26665
26685
|
const msg = err instanceof McpError ? err.message : "Invalid or missing API key";
|
|
26666
26686
|
res.status(401).json({
|
|
26667
26687
|
error: "Unauthorized",
|
|
26668
26688
|
message: msg,
|
|
26669
|
-
hint: "Pass Authorization: Bearer <iso27001_...> header at connect time."
|
|
26689
|
+
hint: "Pass 'Authorization: Bearer <iso27001_...>' header at /sse connect time. MCP_API_KEY env fallback is not accepted over SSE."
|
|
26670
26690
|
});
|
|
26671
26691
|
return;
|
|
26672
26692
|
}
|
|
26693
|
+
const sessionToken = createSessionToken(keyHash, role);
|
|
26673
26694
|
const sessionId = (0, import_crypto.randomUUID)();
|
|
26674
26695
|
const transport = new import_sse.SSEServerTransport("/messages", res);
|
|
26675
26696
|
sessions.set(sessionId, {
|
|
26676
26697
|
transport,
|
|
26677
26698
|
createdAt: Date.now(),
|
|
26678
26699
|
lastActivity: Date.now(),
|
|
26679
|
-
|
|
26680
|
-
rawKey
|
|
26700
|
+
sessionToken
|
|
26681
26701
|
});
|
|
26682
26702
|
res.on("close", () => {
|
|
26703
|
+
removeSessionToken(sessionToken);
|
|
26683
26704
|
sessions.delete(sessionId);
|
|
26684
|
-
console.error(`[iso27001-mcp] SSE
|
|
26705
|
+
console.error(`[iso27001-mcp] SSE session ${sessionId} closed.`);
|
|
26685
26706
|
});
|
|
26686
26707
|
await server.connect(transport);
|
|
26687
26708
|
res.write("data: " + JSON.stringify({ type: "session", sessionId }) + "\n\n");
|
|
@@ -26698,11 +26719,11 @@ function startSseServer(server) {
|
|
|
26698
26719
|
return;
|
|
26699
26720
|
}
|
|
26700
26721
|
entry.lastActivity = Date.now();
|
|
26701
|
-
if (
|
|
26722
|
+
if (req.body && typeof req.body === "object") {
|
|
26702
26723
|
const body = req.body;
|
|
26703
26724
|
if (body.params && typeof body.params === "object") {
|
|
26704
26725
|
const meta = body.params["_meta"] ?? {};
|
|
26705
|
-
meta["apiKey"] = entry.
|
|
26726
|
+
meta["apiKey"] = entry.sessionToken;
|
|
26706
26727
|
body.params["_meta"] = meta;
|
|
26707
26728
|
}
|
|
26708
26729
|
}
|
|
@@ -26714,7 +26735,7 @@ function startSseServer(server) {
|
|
|
26714
26735
|
}
|
|
26715
26736
|
|
|
26716
26737
|
// src/seed/seeder.ts
|
|
26717
|
-
var
|
|
26738
|
+
var import_node_crypto6 = require("crypto");
|
|
26718
26739
|
|
|
26719
26740
|
// src/seed/controls-2022.json
|
|
26720
26741
|
var controls_2022_default = [
|
|
@@ -33154,7 +33175,7 @@ var checksums_default = {
|
|
|
33154
33175
|
|
|
33155
33176
|
// src/seed/seeder.ts
|
|
33156
33177
|
function sha256(data) {
|
|
33157
|
-
return (0,
|
|
33178
|
+
return (0, import_node_crypto6.createHash)("sha256").update(JSON.stringify(data, null, 2)).digest("hex");
|
|
33158
33179
|
}
|
|
33159
33180
|
function verifyChecksums() {
|
|
33160
33181
|
const checks = [
|
|
@@ -33183,7 +33204,7 @@ Seed data may have been modified. Run "npm run generate-checksums" to update che
|
|
|
33183
33204
|
console.error("[seeder] Checksum verification passed.");
|
|
33184
33205
|
}
|
|
33185
33206
|
function stableId(key) {
|
|
33186
|
-
const h = (0,
|
|
33207
|
+
const h = (0, import_node_crypto6.createHash)("sha256").update(key).digest("hex");
|
|
33187
33208
|
return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
|
|
33188
33209
|
}
|
|
33189
33210
|
function seedAll(db) {
|
|
@@ -33542,11 +33563,30 @@ function sanitiseParams(params) {
|
|
|
33542
33563
|
}
|
|
33543
33564
|
|
|
33544
33565
|
// src/audit/logger.ts
|
|
33545
|
-
var
|
|
33566
|
+
var import_node_crypto7 = require("crypto");
|
|
33546
33567
|
var import_node_fs = require("fs");
|
|
33568
|
+
var import_node_path = require("path");
|
|
33569
|
+
function resolveAuditLogPath(raw) {
|
|
33570
|
+
const abs = (0, import_node_path.resolve)(raw);
|
|
33571
|
+
const FORBIDDEN_PREFIXES = ["/etc/", "/proc/", "/sys/", "/dev/"];
|
|
33572
|
+
for (const prefix of FORBIDDEN_PREFIXES) {
|
|
33573
|
+
if (abs.startsWith(prefix)) {
|
|
33574
|
+
throw new Error(
|
|
33575
|
+
`[audit] AUDIT_LOG_PATH "${abs}" points to a system directory. Use a writable application directory instead.`
|
|
33576
|
+
);
|
|
33577
|
+
}
|
|
33578
|
+
}
|
|
33579
|
+
const ext = (0, import_node_path.extname)(abs).toLowerCase();
|
|
33580
|
+
if (ext !== "" && ext !== ".jsonl" && ext !== ".log") {
|
|
33581
|
+
throw new Error(
|
|
33582
|
+
`[audit] AUDIT_LOG_PATH "${abs}" has an unexpected extension "${ext}". Only .jsonl or .log files are permitted.`
|
|
33583
|
+
);
|
|
33584
|
+
}
|
|
33585
|
+
return abs;
|
|
33586
|
+
}
|
|
33547
33587
|
function writeAuditEvent(event) {
|
|
33548
33588
|
const db = getDb();
|
|
33549
|
-
const id = (0,
|
|
33589
|
+
const id = (0, import_node_crypto7.randomUUID)();
|
|
33550
33590
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").split(".")[0] + "Z";
|
|
33551
33591
|
const prevRow = db.prepare(
|
|
33552
33592
|
"SELECT row_hash FROM audit_log ORDER BY rowid DESC LIMIT 1"
|
|
@@ -33565,7 +33605,7 @@ function writeAuditEvent(event) {
|
|
|
33565
33605
|
String(event.duration_ms),
|
|
33566
33606
|
prev_hash ?? "GENESIS"
|
|
33567
33607
|
].join("|");
|
|
33568
|
-
const row_hash = (0,
|
|
33608
|
+
const row_hash = (0, import_node_crypto7.createHmac)("sha256", hmacSecret).update(hashInput).digest("hex");
|
|
33569
33609
|
db.prepare(`
|
|
33570
33610
|
INSERT INTO audit_log
|
|
33571
33611
|
(id, timestamp, tool, key_hash, role, params_json,
|
|
@@ -33586,7 +33626,7 @@ function writeAuditEvent(event) {
|
|
|
33586
33626
|
);
|
|
33587
33627
|
const full = { id, timestamp, prev_hash, row_hash, ...event };
|
|
33588
33628
|
try {
|
|
33589
|
-
const logPath = getEnv("AUDIT_LOG_PATH", "./audit.jsonl");
|
|
33629
|
+
const logPath = resolveAuditLogPath(getEnv("AUDIT_LOG_PATH", "./audit.jsonl"));
|
|
33590
33630
|
(0, import_node_fs.appendFileSync)(logPath, JSON.stringify(full) + "\n", "utf8");
|
|
33591
33631
|
} catch (err) {
|
|
33592
33632
|
console.error("[audit] Warning: failed to write to AUDIT_LOG_PATH:", err);
|
|
@@ -34310,9 +34350,9 @@ function handleGetServerInfo() {
|
|
|
34310
34350
|
}
|
|
34311
34351
|
|
|
34312
34352
|
// src/db/dal.ts
|
|
34313
|
-
var
|
|
34353
|
+
var import_node_crypto8 = require("crypto");
|
|
34314
34354
|
function newId() {
|
|
34315
|
-
return (0,
|
|
34355
|
+
return (0, import_node_crypto8.randomUUID)();
|
|
34316
34356
|
}
|
|
34317
34357
|
function toJson(value) {
|
|
34318
34358
|
if (value === void 0 || value === null) return null;
|
|
@@ -35303,15 +35343,15 @@ var import_mustache = __toESM(require("mustache"));
|
|
|
35303
35343
|
|
|
35304
35344
|
// src/tools/template-utils.ts
|
|
35305
35345
|
var import_node_fs2 = require("fs");
|
|
35306
|
-
var
|
|
35346
|
+
var import_node_path2 = require("path");
|
|
35307
35347
|
function loadTemplate(type, dir) {
|
|
35308
35348
|
const candidates = [
|
|
35309
35349
|
// dist/tools → dist/seed/<dir> (compiled CJS bundle)
|
|
35310
|
-
(0,
|
|
35350
|
+
(0, import_node_path2.join)(__dirname, `../seed/${dir}`, `${type}.md`),
|
|
35311
35351
|
// Running tests directly from source
|
|
35312
|
-
(0,
|
|
35352
|
+
(0, import_node_path2.join)(process.cwd(), `src/seed/${dir}`, `${type}.md`),
|
|
35313
35353
|
// Running after npm run build
|
|
35314
|
-
(0,
|
|
35354
|
+
(0, import_node_path2.join)(process.cwd(), `dist/seed/${dir}`, `${type}.md`)
|
|
35315
35355
|
];
|
|
35316
35356
|
for (const candidate of candidates) {
|
|
35317
35357
|
try {
|
|
@@ -35329,9 +35369,9 @@ function loadPartials() {
|
|
|
35329
35369
|
const partials = {};
|
|
35330
35370
|
for (const name of PARTIAL_NAMES) {
|
|
35331
35371
|
const candidates = [
|
|
35332
|
-
(0,
|
|
35333
|
-
(0,
|
|
35334
|
-
(0,
|
|
35372
|
+
(0, import_node_path2.join)(__dirname, `../seed/partials`, `${name}.md`),
|
|
35373
|
+
(0, import_node_path2.join)(process.cwd(), `src/seed/partials`, `${name}.md`),
|
|
35374
|
+
(0, import_node_path2.join)(process.cwd(), `dist/seed/partials`, `${name}.md`)
|
|
35335
35375
|
];
|
|
35336
35376
|
for (const candidate of candidates) {
|
|
35337
35377
|
try {
|
|
@@ -37415,16 +37455,25 @@ function registerAllTools(server) {
|
|
|
37415
37455
|
const shape = extractShape(schema);
|
|
37416
37456
|
server.tool(toolName, description, shape, async (args2, extra) => {
|
|
37417
37457
|
const startMs = Date.now();
|
|
37418
|
-
const
|
|
37458
|
+
const credential = extra._meta?.apiKey ?? process.env["MCP_API_KEY"] ?? "";
|
|
37419
37459
|
let keyHash = "";
|
|
37420
37460
|
let role = "unknown";
|
|
37421
37461
|
let outcome = "error";
|
|
37422
37462
|
let errorMessage = null;
|
|
37423
37463
|
let result;
|
|
37424
37464
|
try {
|
|
37425
|
-
|
|
37465
|
+
if (isSessionToken(credential)) {
|
|
37466
|
+
const session = lookupSessionToken(credential);
|
|
37467
|
+
if (!session) {
|
|
37468
|
+
throw new McpError({ error_code: "AUTH_INVALID", message: "Session token is invalid or expired" });
|
|
37469
|
+
}
|
|
37470
|
+
keyHash = session.keyHash;
|
|
37471
|
+
role = session.role;
|
|
37472
|
+
} else {
|
|
37473
|
+
keyHash = validateKey(credential);
|
|
37474
|
+
role = loadRole(keyHash);
|
|
37475
|
+
}
|
|
37426
37476
|
checkRateLimit(keyHash);
|
|
37427
|
-
role = loadRole(keyHash);
|
|
37428
37477
|
assertPermission(role, toolName);
|
|
37429
37478
|
const { sanitisedFields } = sanitiseParams(args2);
|
|
37430
37479
|
result = await handler(args2);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iso27001-mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "Stateful ISO 27001:2022 ISMS management for Claude — gap analysis, risk register, policies, audits, and evidence tracking via the Model Context Protocol",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
"postbuild": "rm -rf dist/seed && mkdir -p dist/seed && cp -r src/seed/policy-templates dist/seed/policy-templates && cp -r src/seed/procedure-templates dist/seed/procedure-templates && cp -r src/seed/evidence-templates dist/seed/evidence-templates && cp -r src/seed/partials dist/seed/partials",
|
|
44
44
|
"prepack": "npm run build",
|
|
45
45
|
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
46
|
-
"postinstall": "node -e \"require('better-sqlite3-multiple-ciphers')\" 2>/dev/null || echo \"\\n⚠️ iso27001-mcp: Native SQLite module failed to load. You may need build tools installed.\\n macOS: xcode-select --install\\n Ubuntu/Debian: sudo apt-get install build-essential python3\\n Windows: https://visualstudio.microsoft.com/downloads/ → Build Tools for Visual Studio → Desktop development with C++\\n See: https://github.com/Sushegaad/MCP-Server-for-ISO27001#prerequisites\\n\"",
|
|
47
46
|
"typecheck": "tsc --noEmit",
|
|
48
47
|
"lint": "eslint src --ext .ts",
|
|
49
48
|
"test": "vitest run --coverage",
|