iso27001-mcp 0.8.0 → 0.8.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/README.md +201 -33
- package/dist/index.js +113 -64
- package/package.json +1 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# iso27001-mcp
|
|
2
2
|
|
|
3
|
-
[](https://socket.dev/npm/package/iso27001-mcp/overview/0.8.1)
|
|
4
4
|
[](https://npmjs.com/package/iso27001-mcp)
|
|
5
5
|
[](https://sushegaad.github.io/MCP-Server-for-ISO27001/)
|
|
6
6
|
|
|
@@ -31,6 +31,20 @@ Claude ──MCP──► iso27001-mcp ──► encrypted SQLite (isms.db)
|
|
|
31
31
|
- [Configuration](#configuration)
|
|
32
32
|
- [Connecting to Claude](#connecting-to-claude)
|
|
33
33
|
- [Tools Reference](#tools-reference)
|
|
34
|
+
- [Group 1 — Control Registry](#group-1--control-registry-minimum-role-viewer)
|
|
35
|
+
- [Group 2 — Gap Analysis](#group-2--gap-analysis-reads-viewer-writes-analyst)
|
|
36
|
+
- [Group 3 — Risk Management](#group-3--risk-management-reads-viewer-writes-analyst)
|
|
37
|
+
- [Group 4 — Policy Management](#group-4--policy-management-reads-viewer-create-analyst-update-admin)
|
|
38
|
+
- [Group 5 — Statement of Applicability](#group-5--statement-of-applicability-minimum-role-analyst)
|
|
39
|
+
- [Group 6 — Audit Management](#group-6--audit-management-reads-viewer-writes-admin)
|
|
40
|
+
- [Group 7 — Evidence Tracking](#group-7--evidence-tracking-reads-viewer-writes-analyst)
|
|
41
|
+
- [Group 8 — Server Info](#group-8--server-info-minimum-role-viewer)
|
|
42
|
+
- [Group 9 — Admin & Key Management](#group-9--admin--key-management-minimum-role-admin)
|
|
43
|
+
- [Group 10 — Organisation Profile](#group-10--organisation-profile-minimum-role-admin-for-writes-viewer-for-reads)
|
|
44
|
+
- [Group 11 — Procedure Management](#group-11--procedure-management-reads-viewer-createexport-analyst-update-admin)
|
|
45
|
+
- [Group 12 — Management Review](#group-12--management-review-reads-viewer-writes-admin--clause-93)
|
|
46
|
+
- [Group 13 — Improvement Plan](#group-13--improvement-plan-reads-viewer-writes-analyst--clause-101)
|
|
47
|
+
- [Group 14 — Evidence Templates](#group-14--evidence-templates-reads-viewer-generate-analyst)
|
|
34
48
|
- [MCP Resources](#mcp-resources)
|
|
35
49
|
- [Architecture](#architecture)
|
|
36
50
|
- [Modes](#modes)
|
|
@@ -90,7 +104,7 @@ iso27001-mcp keygen --label "Me" --role admin
|
|
|
90
104
|
|
|
91
105
|
The raw key (`iso27001_...`) is printed **once** and never stored in plaintext. Copy it immediately.
|
|
92
106
|
|
|
93
|
-
> Three roles are available: `viewer` (
|
|
107
|
+
> Three roles are available: `viewer` (31 read-only tools), `analyst` (49 tools), `admin` (all 63 tools). Use `admin` for your personal key.
|
|
94
108
|
|
|
95
109
|
### Step 4 — Add to Claude Desktop
|
|
96
110
|
|
|
@@ -121,7 +135,7 @@ Add the following block, substituting your values from Steps 2 and 3:
|
|
|
121
135
|
|
|
122
136
|
### Step 5 — Restart Claude Desktop and verify
|
|
123
137
|
|
|
124
|
-
Fully quit and reopen Claude Desktop. You should see
|
|
138
|
+
Fully quit and reopen Claude Desktop. You should see 63 tools in the MCP tools panel (hammer icon). Then ask Claude:
|
|
125
139
|
|
|
126
140
|
> *"Use get_server_info to check the server is running."*
|
|
127
141
|
|
|
@@ -287,7 +301,7 @@ Full variable reference:
|
|
|
287
301
|
| `HMAC_SECRET` | ✅ | — | 32-byte hex secret for HMAC-signing API keys |
|
|
288
302
|
| `DB_ENCRYPTION_KEY` | ✅ | — | 32-byte hex key for AES-256 SQLite encryption |
|
|
289
303
|
| `DB_PATH` | | `./isms.db` | Path to the encrypted database file |
|
|
290
|
-
| `AUDIT_LOG_PATH` | | `./audit.
|
|
304
|
+
| `AUDIT_LOG_PATH` | | `./audit.jsonl` | Path for the append-only JSON-L audit log (`.jsonl` or `.log` only) |
|
|
291
305
|
| `RATE_LIMIT_RPM` | | `500` | Tool calls per minute per API key |
|
|
292
306
|
| `SESSION_TTL_HOURS` | | `4` | SSE session TTL (hosted/team modes) |
|
|
293
307
|
| `SSE_PORT` | | `3000` | Port for the SSE server (hosted/team modes) |
|
|
@@ -307,10 +321,10 @@ The server requires an API key on every tool call. Generate one for yourself:
|
|
|
307
321
|
# Viewer — read-only access to 25 tools
|
|
308
322
|
iso27001-mcp keygen --label "Alice" --role viewer
|
|
309
323
|
|
|
310
|
-
# Analyst — read + write for gap/risk/policy/procedure/evidence tools (
|
|
324
|
+
# Analyst — read + write for gap/risk/policy/procedure/evidence tools (49 tools)
|
|
311
325
|
iso27001-mcp keygen --label "Bob" --role analyst --expires 90d
|
|
312
326
|
|
|
313
|
-
# Admin — all
|
|
327
|
+
# Admin — all 63 tools including audit log and key management
|
|
314
328
|
iso27001-mcp keygen --label "CISO" --role admin --expires 1y
|
|
315
329
|
```
|
|
316
330
|
|
|
@@ -375,7 +389,7 @@ export DB_PATH=$HOME/.iso27001/isms.db
|
|
|
375
389
|
|
|
376
390
|
## Tools Reference
|
|
377
391
|
|
|
378
|
-
The server exposes **
|
|
392
|
+
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
393
|
|
|
380
394
|
---
|
|
381
395
|
|
|
@@ -913,6 +927,147 @@ Export a procedure as Markdown or JSON.
|
|
|
913
927
|
|
|
914
928
|
---
|
|
915
929
|
|
|
930
|
+
### Group 12 — Management Review *(reads: viewer+, writes: admin)* — Clause 9.3
|
|
931
|
+
|
|
932
|
+
#### `create_management_review`
|
|
933
|
+
Schedule a management review meeting.
|
|
934
|
+
|
|
935
|
+
| Parameter | Req | Type | Values / Notes |
|
|
936
|
+
|-----------|-----|------|----------------|
|
|
937
|
+
| `title` | ✅ | string | Review title |
|
|
938
|
+
| `review_date` | ✅ | string | `YYYY-MM-DD` |
|
|
939
|
+
| `chair` | ✅ | string | Review chair / CISO name |
|
|
940
|
+
| `attendees` | — | array | List of attendee names |
|
|
941
|
+
| `agenda` | — | string | Meeting agenda |
|
|
942
|
+
|
|
943
|
+
#### `record_review_input`
|
|
944
|
+
Record an input item to a management review (e.g. audit results, risk summary, performance metrics).
|
|
945
|
+
|
|
946
|
+
| Parameter | Req | Type | Values / Notes |
|
|
947
|
+
|-----------|-----|------|----------------|
|
|
948
|
+
| `review_id` | ✅ | string (UUID) | |
|
|
949
|
+
| `input_type` | ✅ | enum | `audit_results` \| `risk_summary` \| `objective_performance` \| `nonconformities` \| `previous_actions` \| `changes` \| `resources` \| `stakeholder_feedback` \| `other` |
|
|
950
|
+
| `summary` | ✅ | string | |
|
|
951
|
+
| `detail` | — | string | Supporting detail |
|
|
952
|
+
|
|
953
|
+
#### `record_review_output`
|
|
954
|
+
Record a decision or action item from a management review.
|
|
955
|
+
|
|
956
|
+
| Parameter | Req | Type | Values / Notes |
|
|
957
|
+
|-----------|-----|------|----------------|
|
|
958
|
+
| `review_id` | ✅ | string (UUID) | |
|
|
959
|
+
| `output_type` | ✅ | enum | `improvement_opportunity` \| `resource_decision` \| `policy_change` \| `objective_change` \| `other` |
|
|
960
|
+
| `description` | ✅ | string | |
|
|
961
|
+
| `owner` | — | string | |
|
|
962
|
+
| `due_date` | — | string | `YYYY-MM-DD` |
|
|
963
|
+
|
|
964
|
+
#### `complete_management_review`
|
|
965
|
+
Mark a management review as complete and record the outcome.
|
|
966
|
+
|
|
967
|
+
| Parameter | Req | Type | Values / Notes |
|
|
968
|
+
|-----------|-----|------|----------------|
|
|
969
|
+
| `review_id` | ✅ | string (UUID) | |
|
|
970
|
+
| `outcome_summary` | ✅ | string | |
|
|
971
|
+
|
|
972
|
+
#### `get_management_review`
|
|
973
|
+
Fetch a management review with all inputs, outputs, and status.
|
|
974
|
+
|
|
975
|
+
| Parameter | Req | Type | Values / Notes |
|
|
976
|
+
|-----------|-----|------|----------------|
|
|
977
|
+
| `review_id` | ✅ | string (UUID) | |
|
|
978
|
+
|
|
979
|
+
#### `list_management_reviews`
|
|
980
|
+
List management reviews with optional status filter.
|
|
981
|
+
|
|
982
|
+
| Parameter | Req | Type | Values / Notes |
|
|
983
|
+
|-----------|-----|------|----------------|
|
|
984
|
+
| `status` | — | enum | `scheduled` \| `in_progress` \| `completed` |
|
|
985
|
+
| `limit` | — | integer | Default: `20`, max `100` |
|
|
986
|
+
| `offset` | — | integer | Default: `0` |
|
|
987
|
+
|
|
988
|
+
---
|
|
989
|
+
|
|
990
|
+
### Group 13 — Improvement Plan *(reads: viewer+, writes: analyst+)* — Clause 10.1
|
|
991
|
+
|
|
992
|
+
#### `create_improvement_opportunity`
|
|
993
|
+
Register an improvement opportunity, typically identified during a management review or audit.
|
|
994
|
+
|
|
995
|
+
| Parameter | Req | Type | Values / Notes |
|
|
996
|
+
|-----------|-----|------|----------------|
|
|
997
|
+
| `title` | ✅ | string | |
|
|
998
|
+
| `description` | ✅ | string | |
|
|
999
|
+
| `source` | ✅ | enum | `audit` \| `management_review` \| `incident` \| `risk_assessment` \| `self_assessment` \| `other` |
|
|
1000
|
+
| `priority` | — | enum | `low` \| `medium` \| `high` \| `critical` — default: `medium` |
|
|
1001
|
+
| `owner` | — | string | |
|
|
1002
|
+
| `due_date` | — | string | `YYYY-MM-DD` |
|
|
1003
|
+
| `related_controls` | — | array | Control IDs |
|
|
1004
|
+
| `review_id` | — | string (UUID) | Link to a management review output |
|
|
1005
|
+
|
|
1006
|
+
#### `update_improvement_opportunity`
|
|
1007
|
+
Update the status, owner, or due date of an improvement opportunity.
|
|
1008
|
+
|
|
1009
|
+
| Parameter | Req | Type | Values / Notes |
|
|
1010
|
+
|-----------|-----|------|----------------|
|
|
1011
|
+
| `opportunity_id` | ✅ | string (UUID) | |
|
|
1012
|
+
| `status` | — | enum | `open` \| `in_progress` \| `completed` \| `cancelled` |
|
|
1013
|
+
| `owner` | — | string | |
|
|
1014
|
+
| `due_date` | — | string | `YYYY-MM-DD` |
|
|
1015
|
+
| `resolution_notes` | — | string | Required when closing |
|
|
1016
|
+
|
|
1017
|
+
#### `get_improvement_opportunity`
|
|
1018
|
+
Fetch a single improvement opportunity by ID.
|
|
1019
|
+
|
|
1020
|
+
| Parameter | Req | Type | Values / Notes |
|
|
1021
|
+
|-----------|-----|------|----------------|
|
|
1022
|
+
| `opportunity_id` | ✅ | string (UUID) | |
|
|
1023
|
+
|
|
1024
|
+
#### `list_improvement_opportunities`
|
|
1025
|
+
List improvement opportunities with optional filters.
|
|
1026
|
+
|
|
1027
|
+
| Parameter | Req | Type | Values / Notes |
|
|
1028
|
+
|-----------|-----|------|----------------|
|
|
1029
|
+
| `status` | — | enum | `open` \| `in_progress` \| `completed` \| `cancelled` |
|
|
1030
|
+
| `priority` | — | enum | `low` \| `medium` \| `high` \| `critical` |
|
|
1031
|
+
| `source` | — | enum | Any source enum value above |
|
|
1032
|
+
| `limit` | — | integer | Default: `50`, max `100` |
|
|
1033
|
+
| `offset` | — | integer | Default: `0` |
|
|
1034
|
+
|
|
1035
|
+
---
|
|
1036
|
+
|
|
1037
|
+
### Group 14 — Evidence Templates *(reads: viewer+, generate: analyst+)*
|
|
1038
|
+
|
|
1039
|
+
#### `generate_evidence_document`
|
|
1040
|
+
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.
|
|
1041
|
+
|
|
1042
|
+
| Parameter | Req | Type | Values / Notes |
|
|
1043
|
+
|-----------|-----|------|----------------|
|
|
1044
|
+
| `template_type` | ✅ | enum | `access_review_attestation` \| `bcp_test_report` \| `incident_post_mortem` \| `risk_treatment_sign_off` \| `supplier_security_questionnaire` \| `training_acknowledgement` |
|
|
1045
|
+
| `title` | ✅ | string | Document title |
|
|
1046
|
+
| `generated_by` | ✅ | string | Author or system that generated the document |
|
|
1047
|
+
| `organisation_name` | — | string | Auto-injected from org profile if set |
|
|
1048
|
+
| `control_id` | — | string | Link to a specific control (default: `general`) |
|
|
1049
|
+
| `vars` | — | object | Additional Mustache template variables |
|
|
1050
|
+
|
|
1051
|
+
#### `get_evidence_document`
|
|
1052
|
+
Fetch a generated evidence document by ID, including rendered content and clause/control mappings.
|
|
1053
|
+
|
|
1054
|
+
| Parameter | Req | Type | Values / Notes |
|
|
1055
|
+
|-----------|-----|------|----------------|
|
|
1056
|
+
| `document_id` | ✅ | string (UUID) | |
|
|
1057
|
+
|
|
1058
|
+
#### `list_evidence_documents`
|
|
1059
|
+
List generated evidence documents with optional filters.
|
|
1060
|
+
|
|
1061
|
+
| Parameter | Req | Type | Values / Notes |
|
|
1062
|
+
|-----------|-----|------|----------------|
|
|
1063
|
+
| `template_type` | — | enum | Filter to a specific template type |
|
|
1064
|
+
| `generated_by` | — | string | Filter by author |
|
|
1065
|
+
| `control_id` | — | string | Filter by linked control |
|
|
1066
|
+
| `limit` | — | integer | Default: `20`, max `100` |
|
|
1067
|
+
| `offset` | — | integer | Default: `0` |
|
|
1068
|
+
|
|
1069
|
+
---
|
|
1070
|
+
|
|
916
1071
|
## MCP Resources
|
|
917
1072
|
|
|
918
1073
|
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 +1122,37 @@ Resources are read-only. Write operations always go through tools (which enforce
|
|
|
967
1122
|
│ Claude (LLM) │
|
|
968
1123
|
└──────────┬───────────────────────────────┬──────────────┘
|
|
969
1124
|
│ MCP Tools (read/write) │ MCP Resources (read-only)
|
|
970
|
-
│
|
|
1125
|
+
│ 63 tools, RBAC enforced │ 12 iso27001:// URIs
|
|
971
1126
|
┌──────────▼───────────────────────────────▼──────────────┐
|
|
972
1127
|
│ iso27001-mcp server │
|
|
973
1128
|
│ │
|
|
974
1129
|
│ ┌─────────────────────────────────────────────────┐ │
|
|
975
|
-
│ │
|
|
1130
|
+
│ │ 7-Step Security Pipeline │ │
|
|
976
1131
|
│ │ │ │
|
|
977
|
-
│ │ 1. Extract
|
|
978
|
-
│ │ 2. validateKey()
|
|
1132
|
+
│ │ 1. Extract credential (_meta.apiKey / env) │ │
|
|
1133
|
+
│ │ 2. Auth — session token OR validateKey() │ │
|
|
1134
|
+
│ │ SSE sessions use opaque token (no raw key) │ │
|
|
979
1135
|
│ │ 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 │ │
|
|
1136
|
+
│ │ 4. assertPermission() RBAC check │ │
|
|
1137
|
+
│ │ 5. sanitiseParams() strip injection patterns │ │
|
|
1138
|
+
│ │ 6. Domain handler business logic │ │
|
|
1139
|
+
│ │ 7. writeAuditEvent() HMAC chain + row_hash │ │
|
|
986
1140
|
│ └─────────────────────────────────────────────────┘ │
|
|
987
1141
|
│ │
|
|
988
1142
|
│ ┌─────────────┐ ┌──────────┐ ┌────────────────────┐ │
|
|
989
1143
|
│ │ Controls │ │ Risks │ │ Policies & │ │
|
|
990
1144
|
│ │ Gap Assess │ │ Register │ │ Procedures │ │
|
|
991
|
-
│ │ SoA │ │ Treatmts │ │ (Mustache
|
|
1145
|
+
│ │ SoA │ │ Treatmts │ │ (Mustache+partls) │ │
|
|
992
1146
|
│ └─────────────┘ └──────────┘ └────────────────────┘ │
|
|
993
1147
|
│ ┌─────────────┐ ┌──────────┐ ┌────────────────────┐ │
|
|
994
|
-
│ │ Audits │ │ Evidence │ │
|
|
995
|
-
│ │ Findings │ │ Jira/GH │ │
|
|
996
|
-
│ │ CARs │ │
|
|
1148
|
+
│ │ Audits │ │ Evidence │ │ Mgmt Review & │ │
|
|
1149
|
+
│ │ Findings │ │ Jira/GH │ │ Improvement Plan │ │
|
|
1150
|
+
│ │ CARs │ │ Tmplts │ │ (Clauses 9.3/10.1)│ │
|
|
997
1151
|
│ └─────────────┘ └──────────┘ └────────────────────┘ │
|
|
1152
|
+
│ ┌─────────────────────────────────────────────────┐ │
|
|
1153
|
+
│ │ Org Profile · Audit Log (HMAC-SHA256 chain) │ │
|
|
1154
|
+
│ │ Session Token Store · API Key RBAC (63 tools) │ │
|
|
1155
|
+
│ └─────────────────────────────────────────────────┘ │
|
|
998
1156
|
│ │
|
|
999
1157
|
│ ┌─────────────────────────────────────────────────┐ │
|
|
1000
1158
|
│ │ AES-256 encrypted SQLite (isms.db) │ │
|
|
@@ -1006,11 +1164,14 @@ Resources are read-only. Write operations always go through tools (which enforce
|
|
|
1006
1164
|
|
|
1007
1165
|
### Database
|
|
1008
1166
|
|
|
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
|
|
1167
|
+
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
1168
|
|
|
1011
1169
|
- `0001_initial.sql` — 17 tables covering every ISMS domain (controls, gap assessments, risks, policies, audits, evidence, API keys, audit log, and more)
|
|
1012
1170
|
- `0002_fts_index.sql` — FTS5 full-text search index on controls, plus 12 performance indexes
|
|
1013
1171
|
- `0003_org_profile_procedures.sql` — `organization_profile` singleton table, `procedures` table, and `procedure_versions` history table
|
|
1172
|
+
- `0004_management_review_improvement.sql` — `management_reviews`, `review_inputs`, `review_outputs`, and `improvement_opportunities` tables (Clauses 9.3 and 10.1)
|
|
1173
|
+
- `0005_evidence_documents.sql` — `generated_evidence` table for Mustache-rendered evidence documents with dual-write to `evidence`
|
|
1174
|
+
- `0006_audit_log_hmac.sql` — adds `prev_hash` column to `audit_log` for HMAC chain integrity
|
|
1014
1175
|
|
|
1015
1176
|
### Seed Data
|
|
1016
1177
|
|
|
@@ -1023,7 +1184,7 @@ On first startup, `seedAll()` inserts all ISO 27001 reference data and verifies
|
|
|
1023
1184
|
|
|
1024
1185
|
### Security Pipeline
|
|
1025
1186
|
|
|
1026
|
-
Every tool call passes through the same
|
|
1187
|
+
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
1188
|
|
|
1028
1189
|
### Business Rules Enforced
|
|
1029
1190
|
|
|
@@ -1044,9 +1205,9 @@ Three roles with strict hierarchy. A key can only call tools at or below its ass
|
|
|
1044
1205
|
|
|
1045
1206
|
| Role | Tools available | Typical user |
|
|
1046
1207
|
|------|----------------|--------------|
|
|
1047
|
-
| `viewer` |
|
|
1048
|
-
| `analyst` |
|
|
1049
|
-
| `admin` |
|
|
1208
|
+
| `viewer` | 31 (all read-only tools) | Auditor, stakeholder |
|
|
1209
|
+
| `analyst` | 49 (reads + gap/risk/policy/procedure/evidence/improvement writes) | ISMS practitioner, consultant |
|
|
1210
|
+
| `admin` | 63 (all tools, including org profile, audit management, audit log and key management) | CISO, ISMS owner |
|
|
1050
1211
|
|
|
1051
1212
|
---
|
|
1052
1213
|
|
|
@@ -1123,7 +1284,7 @@ npm run typecheck
|
|
|
1123
1284
|
# Build dist/
|
|
1124
1285
|
npm run build
|
|
1125
1286
|
|
|
1126
|
-
# Run all tests (
|
|
1287
|
+
# Run all tests (470 unit + integration tests)
|
|
1127
1288
|
npm test
|
|
1128
1289
|
|
|
1129
1290
|
# Watch mode
|
|
@@ -1147,12 +1308,13 @@ src/
|
|
|
1147
1308
|
├── server.ts McpServer factory — registers tools + resources
|
|
1148
1309
|
├── auth/
|
|
1149
1310
|
│ ├── api-key.ts Key generation, HMAC validation, expiry, revocation
|
|
1150
|
-
│
|
|
1311
|
+
│ ├── rbac.ts Permission matrix (63 tools × 3 roles)
|
|
1312
|
+
│ └── session-store.ts SSE session token store (opaque token → keyHash + role)
|
|
1151
1313
|
├── security/
|
|
1152
1314
|
│ ├── sanitise.ts Prompt-injection stripping for free-text fields
|
|
1153
1315
|
│ ├── rate-limiter.ts Sliding-window RPM counter per key hash
|
|
1154
1316
|
│ ├── secrets.ts Env var validation (fail-fast on startup)
|
|
1155
|
-
│ └── validate.ts Zod schemas for all
|
|
1317
|
+
│ └── validate.ts Zod schemas for all 63 tool inputs
|
|
1156
1318
|
├── audit/
|
|
1157
1319
|
│ └── logger.ts Tamper-evident audit event writer
|
|
1158
1320
|
├── db/
|
|
@@ -1166,7 +1328,9 @@ src/
|
|
|
1166
1328
|
│ ├── version-mapping.json 125 cross-version mappings
|
|
1167
1329
|
│ ├── clause-requirements.json 41 clause requirements (clauses 4–10)
|
|
1168
1330
|
│ ├── policy-templates/ 12 Mustache .md policy templates
|
|
1169
|
-
│
|
|
1331
|
+
│ ├── procedure-templates/ 12 Mustache .md procedure templates
|
|
1332
|
+
│ ├── evidence-templates/ 6 Mustache .md evidence document templates
|
|
1333
|
+
│ └── partials/ Shared Mustache partials (org_header, revision_block, approver_signature)
|
|
1170
1334
|
├── tools/
|
|
1171
1335
|
│ ├── index.ts Tool registry and security pipeline
|
|
1172
1336
|
│ ├── controls.ts Group 1: Control Registry (7 tools)
|
|
@@ -1179,7 +1343,10 @@ src/
|
|
|
1179
1343
|
│ ├── server-info.ts Group 8: Server Info (1 tool)
|
|
1180
1344
|
│ ├── org-profile.ts Group 10: Organisation Profile (2 tools) + loadOrgProfileDefaults helper
|
|
1181
1345
|
│ ├── procedures.ts Group 11: Procedure Management (5 tools)
|
|
1182
|
-
│
|
|
1346
|
+
│ ├── management-review.ts Group 12: Management Review — Clause 9.3 (6 tools)
|
|
1347
|
+
│ ├── improvement-plan.ts Group 13: Improvement Plan — Clause 10.1 (4 tools)
|
|
1348
|
+
│ ├── evidence-templates.ts Group 14: Evidence Templates (3 tools)
|
|
1349
|
+
│ └── template-utils.ts Shared loadTemplate / stripFrontmatter / loadPartials helpers
|
|
1183
1350
|
├── resources/
|
|
1184
1351
|
│ ├── index.ts Registers all 12 MCP Resources
|
|
1185
1352
|
│ ├── resource-auth.ts Slim auth helper for resource callbacks
|
|
@@ -1225,10 +1392,11 @@ The SQLite database (`isms.db`) is encrypted at rest using AES-256 via `better-s
|
|
|
1225
1392
|
Every tool call writes a row to `audit_log` with a `row_hash` computed as:
|
|
1226
1393
|
|
|
1227
1394
|
```
|
|
1228
|
-
|
|
1395
|
+
HMAC-SHA256(HMAC_SECRET, id | timestamp | tool | key_hash | role |
|
|
1396
|
+
params_json | outcome | error_message | duration_ms | prev_hash)
|
|
1229
1397
|
```
|
|
1230
1398
|
|
|
1231
|
-
|
|
1399
|
+
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
1400
|
|
|
1233
1401
|
### Production Checklist
|
|
1234
1402
|
|
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.1",
|
|
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
|
}
|
|
@@ -26658,9 +26677,10 @@ function startSseServer(server) {
|
|
|
26658
26677
|
const authHeader = req.headers["authorization"];
|
|
26659
26678
|
const rawKey = (authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null) ?? process.env["MCP_API_KEY"] ?? "";
|
|
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({
|
|
@@ -26670,18 +26690,19 @@ function startSseServer(server) {
|
|
|
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.1",
|
|
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",
|