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.
Files changed (3) hide show
  1. package/README.md +201 -33
  2. package/dist/index.js +113 -64
  3. package/package.json +1 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # iso27001-mcp
2
2
 
3
- [![Socket Badge](https://badge.socket.dev/npm/package/iso27001-mcp/0.7.9)](https://socket.dev/npm/package/iso27001-mcp/overview/0.7.9)
3
+ [![Socket Badge](https://badge.socket.dev/npm/package/iso27001-mcp/0.8.1)](https://socket.dev/npm/package/iso27001-mcp/overview/0.8.1)
4
4
  [![npm version](https://img.shields.io/npm/v/iso27001-mcp.svg)](https://npmjs.com/package/iso27001-mcp)
5
5
  [![Live Demo](https://img.shields.io/badge/demo-live-blue)](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` (25 read-only tools), `analyst` (40 tools), `admin` (all 50 tools). Use `admin` for your personal key.
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 50 tools in the MCP tools panel (hammer icon). Then ask Claude:
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.log` | Path for the append-only JSON-L audit log |
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 (40 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 50 tools including audit log and key management
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 **50 tools** across 11 groups. All tools require a valid API key. The minimum role required is noted per group; `✅` marks required parameters, `—` marks optional ones.
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
- 50 tools, RBAC enforced │ 12 iso27001:// URIs
1125
+ 63 tools, RBAC enforced │ 12 iso27001:// URIs
971
1126
  ┌──────────▼───────────────────────────────▼──────────────┐
972
1127
  │ iso27001-mcp server │
973
1128
  │ │
974
1129
  │ ┌─────────────────────────────────────────────────┐ │
975
- │ │ 9-Step Security Pipeline │ │
1130
+ │ │ 7-Step Security Pipeline │ │
976
1131
  │ │ │ │
977
- │ │ 1. Extract API key (meta or MCP_API_KEY env) │ │
978
- │ │ 2. validateKey() HMAC-SHA256, timing-safe │ │
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. loadRole() viewer | analyst | admin │ │
981
- │ │ 5. assertPermission() RBAC check │ │
982
- │ │ 6. sanitiseParams() strip injection patterns │ │
983
- │ │ 7. Domain handler business logic │ │
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 tmpl) │ │
1145
+ │ │ SoA │ │ Treatmts │ │ (Mustache+partls) │ │
992
1146
  │ └─────────────┘ └──────────┘ └────────────────────┘ │
993
1147
  │ ┌─────────────┐ ┌──────────┐ ┌────────────────────┐ │
994
- │ │ Audits │ │ Evidence │ │ Org Profile & │ │
995
- │ │ Findings │ │ Jira/GH │ │ Audit Log │ │
996
- │ │ CARs │ │ Gaps │ │ (tamper-evident) │ │
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 three SQL migrations applied automatically on first startup:
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 9-step pipeline before any business logic runs. 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.
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` | 25 (all read-only tools) | Auditor, stakeholder |
1048
- | `analyst` | 40 (reads + gap/risk/policy/procedure/evidence writes) | ISMS practitioner, consultant |
1049
- | `admin` | 50 (all tools, including org profile, audit log and key management) | CISO, ISMS owner |
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 (404 unit + integration 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
- └── rbac.ts Permission matrix (50 tools × 3 roles)
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 50 tool inputs
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
- └── procedure-templates/ 12 Mustache .md procedure templates
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
- └── template-utils.ts Shared loadTemplate / stripFrontmatter helpers
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
- SHA-256(timestamp | tool | key_hash | outcome)
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
- Any modification to an audit row after insertion will cause `verifyRowHash()` to fail. The same events are also appended in JSON-L format to `AUDIT_LOG_PATH` for off-database retention and SIEM ingestion.
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(resolve, reject) {
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
- resolve(buf);
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 extname = require("path").extname;
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 = extname("x." + path).toLowerCase().substr(1);
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 extname = path.extname;
20184
+ var extname2 = path.extname;
20185
20185
  var join2 = path.join;
20186
- var resolve = path.resolve;
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 = extname(name);
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 = resolve(root, name);
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 resolve2(dir, file) {
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 extname = path.extname;
21302
+ var extname2 = path.extname;
21303
21303
  var join2 = path.join;
21304
21304
  var normalize = path.normalize;
21305
- var resolve = path.resolve;
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 ? resolve(opts.root) : null;
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 = resolve(String(path2));
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 = resolve(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" && !extname(path2) && path2[path2.length - 1] !== sep) {
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 resolve = require("path").resolve;
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", resolve("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 extname = path.extname;
24093
+ var extname2 = path.extname;
24094
24094
  var mime = send.mime;
24095
- var resolve = path.resolve;
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 ? resolve(path2) : path2;
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(extname(filename));
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 resolve = require("path").resolve;
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 = resolve(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.0",
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(store) {
25067
- if (usedStores.has(store)) {
25068
- const maybeUniquePrefix = store?.localKeys ? "" : " (with a unique prefix)";
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 ${store.constructor.name}${maybeUniquePrefix} for each limiter instead.`
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(store);
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, store, key) {
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 = store.localKeys ? store : store.constructor.name;
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 = `${store.prefix ?? ""}${key}`;
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(store) {
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 (!store.localKeys) {
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 = (store) => (
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 store.incr === "function" && typeof store.increment !== "function"
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((resolve, reject) => {
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
- resolve({ totalHits, resetTime });
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
- keyHash,
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 connection closed for session ${sessionId}.`);
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 (entry.rawKey && req.body && typeof req.body === "object") {
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.rawKey;
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 import_node_crypto5 = require("crypto");
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, import_node_crypto5.createHash)("sha256").update(JSON.stringify(data, null, 2)).digest("hex");
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, import_node_crypto5.createHash)("sha256").update(key).digest("hex");
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 import_node_crypto6 = require("crypto");
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, import_node_crypto6.randomUUID)();
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, import_node_crypto6.createHmac)("sha256", hmacSecret).update(hashInput).digest("hex");
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 import_node_crypto7 = require("crypto");
34353
+ var import_node_crypto8 = require("crypto");
34314
34354
  function newId() {
34315
- return (0, import_node_crypto7.randomUUID)();
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 import_node_path = require("path");
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, import_node_path.join)(__dirname, `../seed/${dir}`, `${type}.md`),
35350
+ (0, import_node_path2.join)(__dirname, `../seed/${dir}`, `${type}.md`),
35311
35351
  // Running tests directly from source
35312
- (0, import_node_path.join)(process.cwd(), `src/seed/${dir}`, `${type}.md`),
35352
+ (0, import_node_path2.join)(process.cwd(), `src/seed/${dir}`, `${type}.md`),
35313
35353
  // Running after npm run build
35314
- (0, import_node_path.join)(process.cwd(), `dist/seed/${dir}`, `${type}.md`)
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, import_node_path.join)(__dirname, `../seed/partials`, `${name}.md`),
35333
- (0, import_node_path.join)(process.cwd(), `src/seed/partials`, `${name}.md`),
35334
- (0, import_node_path.join)(process.cwd(), `dist/seed/partials`, `${name}.md`)
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 rawKey = extra._meta?.apiKey ?? process.env["MCP_API_KEY"] ?? "";
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
- keyHash = validateKey(rawKey);
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.0",
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",