iso27001-mcp 0.7.9 → 0.8.0

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 (35) hide show
  1. package/dist/index.js +1955 -808
  2. package/dist/seed/evidence-templates/access_review_attestation.md +63 -0
  3. package/dist/seed/evidence-templates/bcp_test_report.md +139 -0
  4. package/dist/seed/evidence-templates/incident_post_mortem.md +142 -0
  5. package/dist/seed/evidence-templates/risk_treatment_sign_off.md +112 -0
  6. package/dist/seed/evidence-templates/supplier_security_questionnaire.md +146 -0
  7. package/dist/seed/evidence-templates/training_acknowledgement.md +75 -0
  8. package/dist/seed/partials/approver_signature.md +31 -0
  9. package/dist/seed/partials/org_header.md +14 -0
  10. package/dist/seed/partials/revision_block.md +7 -0
  11. package/dist/seed/policy-templates/acceptable_use.md +15 -11
  12. package/dist/seed/policy-templates/access_control.md +15 -11
  13. package/dist/seed/policy-templates/asset_management.md +16 -11
  14. package/dist/seed/policy-templates/business_continuity.md +14 -11
  15. package/dist/seed/policy-templates/cryptography.md +13 -11
  16. package/dist/seed/policy-templates/data_classification.md +12 -11
  17. package/dist/seed/policy-templates/incident_response.md +15 -11
  18. package/dist/seed/policy-templates/information_security.md +15 -11
  19. package/dist/seed/policy-templates/physical_security.md +16 -11
  20. package/dist/seed/policy-templates/risk_management.md +14 -11
  21. package/dist/seed/policy-templates/secure_development.md +14 -11
  22. package/dist/seed/policy-templates/supplier_security.md +15 -11
  23. package/dist/seed/procedure-templates/access_provisioning.md +17 -12
  24. package/dist/seed/procedure-templates/asset_onboarding_offboarding.md +16 -12
  25. package/dist/seed/procedure-templates/audit_log_review.md +18 -12
  26. package/dist/seed/procedure-templates/backup_restore.md +15 -12
  27. package/dist/seed/procedure-templates/bcp_testing.md +15 -12
  28. package/dist/seed/procedure-templates/change_management.md +16 -12
  29. package/dist/seed/procedure-templates/cryptographic_key_management.md +18 -12
  30. package/dist/seed/procedure-templates/data_classification_handling.md +17 -12
  31. package/dist/seed/procedure-templates/incident_handling.md +14 -12
  32. package/dist/seed/procedure-templates/secure_development_workflow.md +18 -12
  33. package/dist/seed/procedure-templates/supplier_onboarding.md +15 -12
  34. package/dist/seed/procedure-templates/vulnerability_management.md +18 -12
  35. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -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.7.9",
24800
+ version: "0.8.0",
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: {
@@ -24837,7 +24837,7 @@ var require_package = __commonJS({
24837
24837
  ],
24838
24838
  scripts: {
24839
24839
  build: "tsup",
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",
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
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"`,
@@ -25660,120 +25660,10 @@ var lib_default = rateLimit;
25660
25660
  // src/transport/sse.ts
25661
25661
  var import_crypto = require("crypto");
25662
25662
  var import_sse = require("@modelcontextprotocol/sdk/server/sse.js");
25663
- var sessions = /* @__PURE__ */ new Map();
25664
- var ttlMs = parseInt(process.env["SESSION_TTL_HOURS"] ?? "4") * 60 * 60 * 1e3;
25665
- setInterval(() => {
25666
- const now2 = Date.now();
25667
- for (const [sessionId, entry] of sessions) {
25668
- if (now2 - entry.lastActivity > ttlMs) {
25669
- try {
25670
- entry.transport.res?.end();
25671
- } catch {
25672
- }
25673
- sessions.delete(sessionId);
25674
- console.error(`[iso27001-mcp] Session ${sessionId} expired and removed.`);
25675
- }
25676
- }
25677
- }, 6e4);
25678
- function startSseServer(server) {
25679
- const isProduction = process.env["NODE_ENV"] === "production";
25680
- const port = parseInt(process.env["SSE_PORT"] ?? "3000", 10);
25681
- if (isProduction && process.env["BEHIND_TLS_PROXY"] !== "true") {
25682
- console.error(
25683
- "[SECURITY] Running in production without BEHIND_TLS_PROXY=true. Ensure TLS is terminated upstream."
25684
- );
25685
- }
25686
- const app = (0, import_express.default)();
25687
- app.use((req, res, next) => {
25688
- const allowedOrigin = isProduction ? "https://claude.ai" : "*";
25689
- res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
25690
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
25691
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
25692
- if (req.method === "OPTIONS") {
25693
- res.sendStatus(204);
25694
- return;
25695
- }
25696
- next();
25697
- });
25698
- app.use(import_express.default.json());
25699
- if (isProduction) {
25700
- const limiter = lib_default({
25701
- windowMs: 60 * 1e3,
25702
- max: 100,
25703
- standardHeaders: true,
25704
- legacyHeaders: false
25705
- });
25706
- app.use("/messages", limiter);
25707
- }
25708
- app.get("/health", (_req, res) => {
25709
- res.json({ status: "ok", uptime: process.uptime(), mode: "sse" });
25710
- });
25711
- app.get("/sse", async (_req, res) => {
25712
- const sessionId = (0, import_crypto.randomUUID)();
25713
- const transport = new import_sse.SSEServerTransport("/messages", res);
25714
- sessions.set(sessionId, {
25715
- transport,
25716
- createdAt: Date.now(),
25717
- lastActivity: Date.now()
25718
- });
25719
- res.on("close", () => {
25720
- sessions.delete(sessionId);
25721
- console.error(`[iso27001-mcp] SSE connection closed for session ${sessionId}.`);
25722
- });
25723
- await server.connect(transport);
25724
- res.write("data: " + JSON.stringify({ type: "session", sessionId }) + "\n\n");
25725
- });
25726
- app.post("/messages", async (req, res) => {
25727
- const sessionId = req.query["sessionId"];
25728
- if (!sessionId) {
25729
- res.status(400).json({ error: "Missing sessionId query parameter." });
25730
- return;
25731
- }
25732
- const entry = sessions.get(sessionId);
25733
- if (!entry) {
25734
- res.status(404).json({ error: `Session not found: ${sessionId}` });
25735
- return;
25736
- }
25737
- entry.lastActivity = Date.now();
25738
- await entry.transport.handlePostMessage(req, res);
25739
- });
25740
- app.listen(port, () => {
25741
- console.error(`[iso27001-mcp] SSE server listening on port ${port}.`);
25742
- });
25743
- }
25744
-
25745
- // src/security/secrets.ts
25746
- var REQUIRED_VARS = ["DB_ENCRYPTION_KEY", "HMAC_SECRET"];
25747
- function requireEnv(name) {
25748
- const value = process.env[name];
25749
- if (!value) {
25750
- throw new Error(
25751
- `Missing required environment variable: ${name}. Copy .env.example to .env and set the required variables.`
25752
- );
25753
- }
25754
- return value;
25755
- }
25756
- function getEnv(name, defaultValue) {
25757
- return process.env[name] ?? defaultValue;
25758
- }
25759
- function loadSecrets() {
25760
- const missing = [];
25761
- for (const name of REQUIRED_VARS) {
25762
- if (!process.env[name]) {
25763
- missing.push(name);
25764
- }
25765
- }
25766
- if (missing.length > 0) {
25767
- const list = missing.map((n) => ` \u2022 ${n}`).join("\n");
25768
- throw new Error(
25769
- `[secrets] Server cannot start \u2014 missing required environment variables:
25770
- ${list}
25771
25663
 
25772
- Copy .env.example to .env and set all required variables.`
25773
- );
25774
- }
25775
- console.error("[secrets] Required environment variables verified.");
25776
- }
25664
+ // src/auth/api-key.ts
25665
+ var import_node_crypto3 = require("crypto");
25666
+ var import_node_crypto4 = require("crypto");
25777
25667
 
25778
25668
  // src/db/connection.ts
25779
25669
  var import_node_crypto2 = require("crypto");
@@ -26219,12 +26109,189 @@ CREATE INDEX IF NOT EXISTS idx_procedures_review
26219
26109
  CREATE INDEX IF NOT EXISTS idx_procedures_type
26220
26110
  ON procedures(procedure_type, status);
26221
26111
  `;
26112
+ var MIGRATION_0004 = `-- ============================================================
26113
+ -- iso27001-mcp Migration 0004 \u2014 Management Review & Improvement Plan
26114
+ -- Clause 9.3 (Management Review) and Clause 10.1 (Improvement Opportunities)
26115
+ -- ============================================================
26116
+
26117
+ -- \u2500\u2500 Management Reviews (ISO 27001:2022 Clause 9.3) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
26118
+
26119
+ CREATE TABLE IF NOT EXISTS management_reviews (
26120
+ id TEXT PRIMARY KEY NOT NULL,
26121
+ title TEXT NOT NULL,
26122
+ review_date TEXT NOT NULL,
26123
+ reviewers TEXT NOT NULL,
26124
+ scope_notes TEXT,
26125
+ status TEXT NOT NULL DEFAULT 'planned' CHECK (status IN ('planned','in_progress','completed')),
26126
+ completed_at TEXT,
26127
+ completed_by TEXT,
26128
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
26129
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
26130
+ );
26131
+
26132
+ -- \u2500\u2500 Review Inputs (ISO 27001:2022 Clause 9.3.2) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
26133
+
26134
+ CREATE TABLE IF NOT EXISTS review_inputs (
26135
+ id TEXT PRIMARY KEY NOT NULL,
26136
+ review_id TEXT NOT NULL REFERENCES management_reviews(id) ON DELETE CASCADE,
26137
+ input_category TEXT NOT NULL CHECK (input_category IN (
26138
+ 'previous_action_status',
26139
+ 'external_internal_issues',
26140
+ 'interested_party_needs',
26141
+ 'isms_performance',
26142
+ 'interested_party_feedback',
26143
+ 'risk_assessment_results',
26144
+ 'improvement_opportunities'
26145
+ )),
26146
+ summary TEXT NOT NULL,
26147
+ details TEXT,
26148
+ trend TEXT CHECK (trend IN ('improving','stable','declining','insufficient_data')),
26149
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
26150
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
26151
+ UNIQUE(review_id, input_category)
26152
+ );
26153
+
26154
+ -- \u2500\u2500 Review Outputs (ISO 27001:2022 Clause 9.3.3) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
26155
+
26156
+ CREATE TABLE IF NOT EXISTS review_outputs (
26157
+ id TEXT PRIMARY KEY NOT NULL,
26158
+ review_id TEXT NOT NULL REFERENCES management_reviews(id) ON DELETE CASCADE,
26159
+ output_type TEXT NOT NULL CHECK (output_type IN (
26160
+ 'improvement_decision',
26161
+ 'isms_change_decision'
26162
+ )),
26163
+ decision TEXT NOT NULL,
26164
+ owner TEXT,
26165
+ due_date TEXT,
26166
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
26167
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
26168
+ );
26169
+
26170
+ -- \u2500\u2500 Improvement Opportunities (ISO 27001:2022 Clause 10.1) \u2500\u2500\u2500
26171
+
26172
+ CREATE TABLE IF NOT EXISTS improvement_opportunities (
26173
+ id TEXT PRIMARY KEY NOT NULL,
26174
+ title TEXT NOT NULL,
26175
+ description TEXT NOT NULL,
26176
+ source TEXT NOT NULL CHECK (source IN (
26177
+ 'management_review','risk_assessment','audit','monitoring','other'
26178
+ )),
26179
+ priority TEXT NOT NULL DEFAULT 'medium' CHECK (priority IN ('low','medium','high','critical')),
26180
+ owner TEXT,
26181
+ target_date TEXT,
26182
+ status TEXT NOT NULL DEFAULT 'open' CHECK (status IN (
26183
+ 'open','in_progress','implemented','closed'
26184
+ )),
26185
+ review_id TEXT REFERENCES management_reviews(id) ON DELETE SET NULL,
26186
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
26187
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
26188
+ );
26189
+
26190
+ -- \u2500\u2500 Performance Indexes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
26191
+
26192
+ CREATE INDEX IF NOT EXISTS idx_management_reviews_status
26193
+ ON management_reviews(status, review_date DESC);
26194
+
26195
+ CREATE INDEX IF NOT EXISTS idx_review_inputs_review
26196
+ ON review_inputs(review_id, input_category);
26197
+
26198
+ CREATE INDEX IF NOT EXISTS idx_review_outputs_review
26199
+ ON review_outputs(review_id, output_type);
26200
+
26201
+ CREATE INDEX IF NOT EXISTS idx_improvement_opps_status
26202
+ ON improvement_opportunities(status, priority);
26203
+
26204
+ CREATE INDEX IF NOT EXISTS idx_improvement_opps_review
26205
+ ON improvement_opportunities(review_id);
26206
+ `;
26207
+ var MIGRATION_0005 = `-- ============================================================
26208
+ -- iso27001-mcp Migration 0005 \u2014 Generated Evidence Documents
26209
+ -- Supports the generate_evidence_document tool (Group 14).
26210
+ -- ============================================================
26211
+
26212
+ CREATE TABLE IF NOT EXISTS generated_evidence (
26213
+ id TEXT PRIMARY KEY NOT NULL,
26214
+ template_type TEXT NOT NULL CHECK (template_type IN (
26215
+ 'access_review_attestation',
26216
+ 'training_acknowledgement',
26217
+ 'supplier_security_questionnaire',
26218
+ 'incident_post_mortem',
26219
+ 'bcp_test_report',
26220
+ 'risk_treatment_sign_off'
26221
+ )),
26222
+ title TEXT NOT NULL,
26223
+ content TEXT NOT NULL,
26224
+ organisation_name TEXT NOT NULL,
26225
+ generated_by TEXT NOT NULL,
26226
+ clause_mappings TEXT,
26227
+ control_mappings TEXT,
26228
+ template_vars TEXT NOT NULL DEFAULT '{}',
26229
+ evidence_id TEXT REFERENCES evidence(id) ON DELETE SET NULL,
26230
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
26231
+ );
26232
+
26233
+ CREATE INDEX IF NOT EXISTS idx_generated_evidence_type
26234
+ ON generated_evidence(template_type, created_at DESC);
26235
+
26236
+ CREATE INDEX IF NOT EXISTS idx_generated_evidence_generated_by
26237
+ ON generated_evidence(generated_by, created_at DESC);
26238
+ `;
26239
+ var MIGRATION_0006 = `-- ============================================================
26240
+ -- iso27001-mcp Migration 0006 \u2014 Audit Log HMAC Hardening
26241
+ -- Adds prev_hash column to audit_log for hash-chain integrity.
26242
+ -- After this migration, row_hash is computed as HMAC-SHA256 over
26243
+ -- ALL fields (id, timestamp, tool, key_hash, role, params_json,
26244
+ -- outcome, error_message, duration_ms, prev_hash), replacing the
26245
+ -- former SHA-256 over 4 fields only.
26246
+ -- !! Never edit this file after it has been applied !!
26247
+ -- ============================================================
26248
+
26249
+ -- SQLite does not support ALTER TABLE ADD COLUMN with NOT NULL
26250
+ -- without a default; prev_hash is nullable (NULL = first in chain).
26251
+ ALTER TABLE audit_log ADD COLUMN prev_hash TEXT;
26252
+ `;
26222
26253
  var MIGRATIONS = [
26223
26254
  { filename: "0001_initial.sql", sql: MIGRATION_0001 },
26224
26255
  { filename: "0002_fts_index.sql", sql: MIGRATION_0002 },
26225
- { filename: "0003_org_profile_procedures.sql", sql: MIGRATION_0003 }
26256
+ { filename: "0003_org_profile_procedures.sql", sql: MIGRATION_0003 },
26257
+ { filename: "0004_management_review_improvement.sql", sql: MIGRATION_0004 },
26258
+ { filename: "0005_evidence_documents.sql", sql: MIGRATION_0005 },
26259
+ { filename: "0006_audit_log_hmac.sql", sql: MIGRATION_0006 }
26226
26260
  ];
26227
26261
 
26262
+ // src/security/secrets.ts
26263
+ var REQUIRED_VARS = ["DB_ENCRYPTION_KEY", "HMAC_SECRET"];
26264
+ function requireEnv(name) {
26265
+ const value = process.env[name];
26266
+ if (!value) {
26267
+ throw new Error(
26268
+ `Missing required environment variable: ${name}. Copy .env.example to .env and set the required variables.`
26269
+ );
26270
+ }
26271
+ return value;
26272
+ }
26273
+ function getEnv(name, defaultValue) {
26274
+ return process.env[name] ?? defaultValue;
26275
+ }
26276
+ function loadSecrets() {
26277
+ const missing = [];
26278
+ for (const name of REQUIRED_VARS) {
26279
+ if (!process.env[name]) {
26280
+ missing.push(name);
26281
+ }
26282
+ }
26283
+ if (missing.length > 0) {
26284
+ const list = missing.map((n) => ` \u2022 ${n}`).join("\n");
26285
+ throw new Error(
26286
+ `[secrets] Server cannot start \u2014 missing required environment variables:
26287
+ ${list}
26288
+
26289
+ Copy .env.example to .env and set all required variables.`
26290
+ );
26291
+ }
26292
+ console.error("[secrets] Required environment variables verified.");
26293
+ }
26294
+
26228
26295
  // src/db/connection.ts
26229
26296
  var Database = require("better-sqlite3-multiple-ciphers");
26230
26297
  var _db = null;
@@ -26292,82 +26359,436 @@ function runMigrations(db) {
26292
26359
  }
26293
26360
  var requireEnv2 = requireEnv;
26294
26361
 
26295
- // src/seed/seeder.ts
26296
- var import_node_crypto3 = require("crypto");
26297
-
26298
- // src/seed/controls-2022.json
26299
- var controls_2022_default = [
26300
- {
26301
- control_id: "5.1",
26302
- version: "2022",
26303
- name: "Policies for information security",
26304
- theme: "Organizational",
26305
- description: "Information security policy and topic-specific policies shall be defined, approved by management, published, communicated to and acknowledged by relevant personnel and relevant interested parties, and reviewed at planned intervals or if significant changes occur.",
26306
- guidance: "Policies should address business requirements, relevant legislation and regulations, current and anticipated information security threats, and the specific policy areas defined in Annex A. Policies shall be reviewed at planned intervals or if significant changes occur.",
26307
- control_type: [
26308
- "Preventive"
26309
- ],
26310
- attributes: {
26311
- information_security_properties: [
26312
- "Confidentiality",
26313
- "Integrity",
26314
- "Availability"
26315
- ],
26316
- cybersecurity_concepts: [
26317
- "Identify",
26318
- "Protect"
26319
- ],
26320
- operational_capabilities: [
26321
- "Governance"
26322
- ],
26323
- security_domains: [
26324
- "Governance_and_ecosystem"
26325
- ]
26326
- },
26327
- related_controls: [
26328
- "5.2",
26329
- "5.4",
26330
- "5.36",
26331
- "5.37"
26332
- ],
26333
- new_in_2022: false,
26334
- iso_clause_refs: [
26335
- "5",
26336
- "6.2"
26337
- ]
26338
- },
26339
- {
26340
- control_id: "5.2",
26341
- version: "2022",
26342
- name: "Information security roles and responsibilities",
26343
- theme: "Organizational",
26344
- description: "Information security roles and responsibilities shall be defined and allocated according to the organisation's needs.",
26345
- guidance: "Roles and responsibilities should be defined for all personnel involved in information security. Responsibilities for protecting specific assets and carrying out specific information security processes shall be clearly assigned.",
26346
- control_type: [
26347
- "Preventive"
26348
- ],
26349
- attributes: {
26350
- information_security_properties: [
26351
- "Confidentiality",
26352
- "Integrity",
26353
- "Availability"
26354
- ],
26355
- cybersecurity_concepts: [
26356
- "Identify",
26357
- "Protect"
26358
- ],
26359
- operational_capabilities: [
26360
- "Governance"
26361
- ],
26362
- security_domains: [
26363
- "Governance_and_ecosystem"
26364
- ]
26365
- },
26366
- related_controls: [
26367
- "5.1",
26368
- "5.3",
26369
- "5.4",
26370
- "6.2"
26362
+ // src/types/errors.ts
26363
+ var HTTP_STATUS = {
26364
+ AUTH_MISSING: 401,
26365
+ AUTH_INVALID: 401,
26366
+ AUTH_EXPIRED: 401,
26367
+ AUTH_REVOKED: 401,
26368
+ RBAC_DENIED: 403,
26369
+ RATE_LIMITED: 429,
26370
+ VALIDATION_ERROR: 400,
26371
+ NOT_FOUND: 404,
26372
+ BUSINESS_RULE: 422,
26373
+ INTERNAL_ERROR: 500,
26374
+ INTEGRATION_ERROR: 502
26375
+ };
26376
+ var McpError = class extends Error {
26377
+ error_code;
26378
+ http_status;
26379
+ field;
26380
+ hint;
26381
+ docs_ref;
26382
+ constructor(opts) {
26383
+ super(opts.message);
26384
+ this.name = "McpError";
26385
+ this.error_code = opts.error_code;
26386
+ this.http_status = HTTP_STATUS[opts.error_code];
26387
+ this.field = opts.field;
26388
+ this.hint = opts.hint;
26389
+ this.docs_ref = opts.docs_ref;
26390
+ }
26391
+ /**
26392
+ * Serialise to the structured tool result format expected by the MCP SDK.
26393
+ * Returns a single content block with JSON text so Claude can parse it.
26394
+ */
26395
+ toToolResult() {
26396
+ const body = {
26397
+ error_code: this.error_code,
26398
+ http_status: this.http_status,
26399
+ message: this.message
26400
+ };
26401
+ if (this.field) body["field"] = this.field;
26402
+ if (this.hint) body["hint"] = this.hint;
26403
+ if (this.docs_ref) body["docs_ref"] = this.docs_ref;
26404
+ return {
26405
+ content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
26406
+ isError: true
26407
+ };
26408
+ }
26409
+ };
26410
+ function authMissing() {
26411
+ return new McpError({
26412
+ error_code: "AUTH_MISSING",
26413
+ message: "No API key provided. Pass your key via MCP_API_KEY env var or _meta.apiKey.",
26414
+ hint: "Set MCP_API_KEY env var or include apiKey in _meta"
26415
+ });
26416
+ }
26417
+ function authInvalid() {
26418
+ return new McpError({
26419
+ error_code: "AUTH_INVALID",
26420
+ message: "HMAC validation failed. The provided API key is not recognised.",
26421
+ hint: "Verify your API key \u2014 run: iso27001-mcp keygen"
26422
+ });
26423
+ }
26424
+ function authExpired() {
26425
+ return new McpError({
26426
+ error_code: "AUTH_EXPIRED",
26427
+ message: "API key has expired.",
26428
+ hint: "Generate a new key: iso27001-mcp keygen"
26429
+ });
26430
+ }
26431
+ function authRevoked() {
26432
+ return new McpError({
26433
+ error_code: "AUTH_REVOKED",
26434
+ message: "API key has been revoked.",
26435
+ hint: "Generate a new key: iso27001-mcp keygen --role [role]"
26436
+ });
26437
+ }
26438
+ function rbacDenied(toolName, requiredRole) {
26439
+ return new McpError({
26440
+ error_code: "RBAC_DENIED",
26441
+ message: `Your role does not have permission to call '${toolName}'. Requires: ${requiredRole}.`,
26442
+ hint: "Your role cannot call this tool \u2014 contact your admin to get a key with a higher role"
26443
+ });
26444
+ }
26445
+ function rateLimited() {
26446
+ return new McpError({
26447
+ error_code: "RATE_LIMITED",
26448
+ message: "Too many requests. Exceeded RATE_LIMIT_RPM.",
26449
+ hint: "Slow down or raise RATE_LIMIT_RPM in your .env"
26450
+ });
26451
+ }
26452
+ function notFound(entity, id) {
26453
+ return new McpError({
26454
+ error_code: "NOT_FOUND",
26455
+ message: `${entity} not found: ${id}`
26456
+ });
26457
+ }
26458
+ function businessRule(message, hint, docsRef) {
26459
+ return new McpError({
26460
+ error_code: "BUSINESS_RULE",
26461
+ message,
26462
+ hint,
26463
+ docs_ref: docsRef
26464
+ });
26465
+ }
26466
+ function integrationError(service, message, hint) {
26467
+ return new McpError({
26468
+ error_code: "INTEGRATION_ERROR",
26469
+ message: `${service} integration error: ${message}`,
26470
+ hint
26471
+ });
26472
+ }
26473
+
26474
+ // src/auth/api-key.ts
26475
+ function hmacSha256(secret, data) {
26476
+ return (0, import_node_crypto3.createHmac)("sha256", secret).update(data).digest("hex");
26477
+ }
26478
+ function generateKey(label, role, expiresAt) {
26479
+ const db = getDb();
26480
+ const rawKey = "iso27001_" + (0, import_node_crypto3.randomBytes)(24).toString("base64url");
26481
+ const keyHash = hmacSha256(requireEnv("HMAC_SECRET"), rawKey);
26482
+ db.prepare(`
26483
+ INSERT INTO api_keys (id, key_hash, label, role, expires_at, created_at)
26484
+ VALUES (?, ?, ?, ?, ?, datetime('now'))
26485
+ `).run(
26486
+ (0, import_node_crypto4.randomUUID)(),
26487
+ keyHash,
26488
+ label,
26489
+ role,
26490
+ expiresAt ?? null
26491
+ );
26492
+ console.log("=".repeat(60));
26493
+ console.log("API Key generated (save now \u2014 NOT stored in plaintext):");
26494
+ console.log("");
26495
+ console.log(" " + rawKey);
26496
+ console.log("");
26497
+ console.log(` Label: ${label}`);
26498
+ console.log(` Role: ${role}`);
26499
+ console.log(` Expires: ${expiresAt ?? "never"}`);
26500
+ console.log("=".repeat(60));
26501
+ return rawKey;
26502
+ }
26503
+ function validateKey(rawKey) {
26504
+ if (!rawKey) throw authMissing();
26505
+ const secret = requireEnv("HMAC_SECRET");
26506
+ const keyHash = hmacSha256(secret, rawKey);
26507
+ const db = getDb();
26508
+ const row = db.prepare(
26509
+ "SELECT key_hash FROM api_keys WHERE key_hash = ? LIMIT 1"
26510
+ ).get(keyHash);
26511
+ if (!row) {
26512
+ const dummyA = Buffer.alloc(32, 0);
26513
+ const dummyB = Buffer.alloc(32, 1);
26514
+ (0, import_node_crypto3.timingSafeEqual)(dummyA, dummyB);
26515
+ throw authInvalid();
26516
+ }
26517
+ const storedBuf = Buffer.from(row.key_hash, "hex");
26518
+ const computedBuf = Buffer.from(keyHash, "hex");
26519
+ if (storedBuf.length !== computedBuf.length || !(0, import_node_crypto3.timingSafeEqual)(storedBuf, computedBuf)) {
26520
+ throw authInvalid();
26521
+ }
26522
+ return keyHash;
26523
+ }
26524
+ function loadRole(keyHash) {
26525
+ const db = getDb();
26526
+ const row = db.prepare(`
26527
+ SELECT id, role, expires_at, revoked_at FROM api_keys WHERE key_hash = ?
26528
+ `).get(keyHash);
26529
+ if (!row) throw authInvalid();
26530
+ if (row.revoked_at) throw authRevoked();
26531
+ if (row.expires_at) {
26532
+ const expiry = new Date(row.expires_at);
26533
+ if (expiry < /* @__PURE__ */ new Date()) throw authExpired();
26534
+ }
26535
+ try {
26536
+ db.prepare("UPDATE api_keys SET last_used_at = datetime('now') WHERE id = ?").run(row.id);
26537
+ } catch {
26538
+ }
26539
+ return row.role;
26540
+ }
26541
+ function listKeys() {
26542
+ const db = getDb();
26543
+ const rows = db.prepare(`
26544
+ SELECT id, label, role, created_at, expires_at, revoked_at, last_used_at
26545
+ FROM api_keys
26546
+ ORDER BY created_at DESC
26547
+ `).all();
26548
+ const now2 = /* @__PURE__ */ new Date();
26549
+ return rows.map((r) => {
26550
+ let status = "active";
26551
+ if (r.revoked_at) {
26552
+ status = "revoked";
26553
+ } else if (r.expires_at && new Date(r.expires_at) < now2) {
26554
+ status = "expired";
26555
+ }
26556
+ return {
26557
+ id: r.id,
26558
+ label: r.label,
26559
+ role: r.role,
26560
+ created_at: r.created_at,
26561
+ expires_at: r.expires_at,
26562
+ last_used_at: r.last_used_at,
26563
+ status
26564
+ };
26565
+ });
26566
+ }
26567
+ function revokeKey(label) {
26568
+ const db = getDb();
26569
+ const row = db.prepare("SELECT id FROM api_keys WHERE label = ?").get(label);
26570
+ if (!row) {
26571
+ throw new Error(`API key with label '${label}' not found.`);
26572
+ }
26573
+ db.prepare("UPDATE api_keys SET revoked_at = datetime('now') WHERE id = ?").run(row.id);
26574
+ console.log(`[auth] Key '${label}' revoked.`);
26575
+ }
26576
+ function warnAdminExpiry() {
26577
+ const db = getDb();
26578
+ const rows = db.prepare(`
26579
+ SELECT label FROM api_keys
26580
+ WHERE role = 'admin' AND expires_at IS NULL AND revoked_at IS NULL
26581
+ `).all();
26582
+ if (rows.length > 0) {
26583
+ const labels = rows.map((r) => r.label).join(", ");
26584
+ console.warn(
26585
+ `[SECURITY] Admin keys without expiry: ${labels}. Consider setting --expires to limit blast radius.`
26586
+ );
26587
+ }
26588
+ }
26589
+ function parseExpiresFlag(value) {
26590
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
26591
+ const days = /^(\d+)d$/.exec(value);
26592
+ if (days) {
26593
+ const d = /* @__PURE__ */ new Date();
26594
+ d.setDate(d.getDate() + parseInt(days[1], 10));
26595
+ return d.toISOString().split("T")[0];
26596
+ }
26597
+ const years = /^(\d+)y$/.exec(value);
26598
+ if (years) {
26599
+ const d = /* @__PURE__ */ new Date();
26600
+ d.setFullYear(d.getFullYear() + parseInt(years[1], 10));
26601
+ return d.toISOString().split("T")[0];
26602
+ }
26603
+ throw new Error(
26604
+ `Invalid --expires value: '${value}'. Use '90d', '1y', or 'YYYY-MM-DD'.`
26605
+ );
26606
+ }
26607
+
26608
+ // src/transport/sse.ts
26609
+ var sessions = /* @__PURE__ */ new Map();
26610
+ var ttlMs = parseInt(process.env["SESSION_TTL_HOURS"] ?? "4") * 60 * 60 * 1e3;
26611
+ setInterval(() => {
26612
+ const now2 = Date.now();
26613
+ for (const [sessionId, entry] of sessions) {
26614
+ if (now2 - entry.lastActivity > ttlMs) {
26615
+ try {
26616
+ entry.transport.res?.end();
26617
+ } catch {
26618
+ }
26619
+ sessions.delete(sessionId);
26620
+ console.error(`[iso27001-mcp] Session ${sessionId} expired and removed.`);
26621
+ }
26622
+ }
26623
+ }, 6e4);
26624
+ function startSseServer(server) {
26625
+ const isProduction = process.env["NODE_ENV"] === "production";
26626
+ const port = parseInt(process.env["SSE_PORT"] ?? "3000", 10);
26627
+ if (isProduction && process.env["BEHIND_TLS_PROXY"] !== "true") {
26628
+ console.error(
26629
+ "[SECURITY] Running in production without BEHIND_TLS_PROXY=true. Ensure TLS is terminated upstream."
26630
+ );
26631
+ }
26632
+ const app = (0, import_express.default)();
26633
+ app.use((req, res, next) => {
26634
+ const allowedOrigin = isProduction ? "https://claude.ai" : "*";
26635
+ res.setHeader("Access-Control-Allow-Origin", allowedOrigin);
26636
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
26637
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
26638
+ if (req.method === "OPTIONS") {
26639
+ res.sendStatus(204);
26640
+ return;
26641
+ }
26642
+ next();
26643
+ });
26644
+ app.use(import_express.default.json());
26645
+ if (isProduction) {
26646
+ const limiter = lib_default({
26647
+ windowMs: 60 * 1e3,
26648
+ max: 100,
26649
+ standardHeaders: true,
26650
+ legacyHeaders: false
26651
+ });
26652
+ app.use("/messages", limiter);
26653
+ }
26654
+ app.get("/health", (_req, res) => {
26655
+ res.json({ status: "ok", uptime: process.uptime(), mode: "sse" });
26656
+ });
26657
+ app.get("/sse", async (req, res) => {
26658
+ const authHeader = req.headers["authorization"];
26659
+ const rawKey = (authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null) ?? process.env["MCP_API_KEY"] ?? "";
26660
+ let keyHash;
26661
+ try {
26662
+ keyHash = validateKey(rawKey);
26663
+ loadRole(keyHash);
26664
+ } catch (err) {
26665
+ const msg = err instanceof McpError ? err.message : "Invalid or missing API key";
26666
+ res.status(401).json({
26667
+ error: "Unauthorized",
26668
+ message: msg,
26669
+ hint: "Pass Authorization: Bearer <iso27001_...> header at connect time."
26670
+ });
26671
+ return;
26672
+ }
26673
+ const sessionId = (0, import_crypto.randomUUID)();
26674
+ const transport = new import_sse.SSEServerTransport("/messages", res);
26675
+ sessions.set(sessionId, {
26676
+ transport,
26677
+ createdAt: Date.now(),
26678
+ lastActivity: Date.now(),
26679
+ keyHash,
26680
+ rawKey
26681
+ });
26682
+ res.on("close", () => {
26683
+ sessions.delete(sessionId);
26684
+ console.error(`[iso27001-mcp] SSE connection closed for session ${sessionId}.`);
26685
+ });
26686
+ await server.connect(transport);
26687
+ res.write("data: " + JSON.stringify({ type: "session", sessionId }) + "\n\n");
26688
+ });
26689
+ app.post("/messages", async (req, res) => {
26690
+ const sessionId = req.query["sessionId"];
26691
+ if (!sessionId) {
26692
+ res.status(400).json({ error: "Missing sessionId query parameter." });
26693
+ return;
26694
+ }
26695
+ const entry = sessions.get(sessionId);
26696
+ if (!entry) {
26697
+ res.status(404).json({ error: `Session not found: ${sessionId}` });
26698
+ return;
26699
+ }
26700
+ entry.lastActivity = Date.now();
26701
+ if (entry.rawKey && req.body && typeof req.body === "object") {
26702
+ const body = req.body;
26703
+ if (body.params && typeof body.params === "object") {
26704
+ const meta = body.params["_meta"] ?? {};
26705
+ meta["apiKey"] = entry.rawKey;
26706
+ body.params["_meta"] = meta;
26707
+ }
26708
+ }
26709
+ await entry.transport.handlePostMessage(req, res);
26710
+ });
26711
+ app.listen(port, () => {
26712
+ console.error(`[iso27001-mcp] SSE server listening on port ${port}.`);
26713
+ });
26714
+ }
26715
+
26716
+ // src/seed/seeder.ts
26717
+ var import_node_crypto5 = require("crypto");
26718
+
26719
+ // src/seed/controls-2022.json
26720
+ var controls_2022_default = [
26721
+ {
26722
+ control_id: "5.1",
26723
+ version: "2022",
26724
+ name: "Policies for information security",
26725
+ theme: "Organizational",
26726
+ description: "Information security policy and topic-specific policies shall be defined, approved by management, published, communicated to and acknowledged by relevant personnel and relevant interested parties, and reviewed at planned intervals or if significant changes occur.",
26727
+ guidance: "Policies should address business requirements, relevant legislation and regulations, current and anticipated information security threats, and the specific policy areas defined in Annex A. Policies shall be reviewed at planned intervals or if significant changes occur.",
26728
+ control_type: [
26729
+ "Preventive"
26730
+ ],
26731
+ attributes: {
26732
+ information_security_properties: [
26733
+ "Confidentiality",
26734
+ "Integrity",
26735
+ "Availability"
26736
+ ],
26737
+ cybersecurity_concepts: [
26738
+ "Identify",
26739
+ "Protect"
26740
+ ],
26741
+ operational_capabilities: [
26742
+ "Governance"
26743
+ ],
26744
+ security_domains: [
26745
+ "Governance_and_ecosystem"
26746
+ ]
26747
+ },
26748
+ related_controls: [
26749
+ "5.2",
26750
+ "5.4",
26751
+ "5.36",
26752
+ "5.37"
26753
+ ],
26754
+ new_in_2022: false,
26755
+ iso_clause_refs: [
26756
+ "5",
26757
+ "6.2"
26758
+ ]
26759
+ },
26760
+ {
26761
+ control_id: "5.2",
26762
+ version: "2022",
26763
+ name: "Information security roles and responsibilities",
26764
+ theme: "Organizational",
26765
+ description: "Information security roles and responsibilities shall be defined and allocated according to the organisation's needs.",
26766
+ guidance: "Roles and responsibilities should be defined for all personnel involved in information security. Responsibilities for protecting specific assets and carrying out specific information security processes shall be clearly assigned.",
26767
+ control_type: [
26768
+ "Preventive"
26769
+ ],
26770
+ attributes: {
26771
+ information_security_properties: [
26772
+ "Confidentiality",
26773
+ "Integrity",
26774
+ "Availability"
26775
+ ],
26776
+ cybersecurity_concepts: [
26777
+ "Identify",
26778
+ "Protect"
26779
+ ],
26780
+ operational_capabilities: [
26781
+ "Governance"
26782
+ ],
26783
+ security_domains: [
26784
+ "Governance_and_ecosystem"
26785
+ ]
26786
+ },
26787
+ related_controls: [
26788
+ "5.1",
26789
+ "5.3",
26790
+ "5.4",
26791
+ "6.2"
26371
26792
  ],
26372
26793
  new_in_2022: false,
26373
26794
  iso_clause_refs: [
@@ -32667,502 +33088,252 @@ var clause_requirements_default = [
32667
33088
  "5.4"
32668
33089
  ]
32669
33090
  },
32670
- {
32671
- clause_id: "9.3.2",
32672
- parent_id: "9.3",
32673
- title: "Management review inputs",
32674
- requirement_text: "The management review shall include consideration of: status of actions from previous reviews; changes in external/internal issues; feedback on information security performance; feedback from interested parties; results of risk assessment and risk treatment plan status; opportunities for continual improvement.",
32675
- implementation_notes: "Prepare management review inputs in advance. Include trend data where available. Present metrics and KPIs. Summarise key risks and treatment status. Include audit findings and corrective action status.",
32676
- related_controls: [
32677
- "5.35",
32678
- "5.36"
32679
- ]
32680
- },
32681
- {
32682
- clause_id: "9.3.3",
32683
- parent_id: "9.3",
32684
- title: "Management review results",
32685
- requirement_text: "The outputs of the management review shall include decisions related to continual improvement opportunities and any need for changes to the information security management system.",
32686
- implementation_notes: "Document all decisions with clear action items, owners, and due dates. Communicate decisions to relevant stakeholders. Track action items to completion. Use management review outputs to drive ISMS improvement.",
32687
- related_controls: [
32688
- "5.1"
32689
- ]
32690
- },
32691
- {
32692
- clause_id: "10",
32693
- parent_id: null,
32694
- title: "Improvement",
32695
- requirement_text: "The organisation shall continually improve the suitability, adequacy and effectiveness of the information security management system.",
32696
- implementation_notes: "Improvement activities include corrective actions, preventive actions, and continual improvement initiatives. Track and trend nonconformities. Use improvement inputs from audits, incidents, management reviews, and performance metrics.",
32697
- related_controls: [
32698
- "5.27",
32699
- "5.35",
32700
- "5.36"
32701
- ]
32702
- },
32703
- {
32704
- clause_id: "10.1",
32705
- parent_id: "10",
32706
- title: "Continual improvement",
32707
- requirement_text: "The organisation shall continually improve the suitability, adequacy and effectiveness of the information security management system.",
32708
- implementation_notes: "Establish a formal process for identifying and implementing improvements. Use Lessons Learned from incidents, audit findings, and management reviews as inputs. Track improvement initiatives to completion. Measure the effectiveness of improvements.",
32709
- related_controls: [
32710
- "5.27"
32711
- ]
32712
- },
32713
- {
32714
- clause_id: "10.2",
32715
- parent_id: "10",
32716
- title: "Nonconformity and corrective action",
32717
- requirement_text: "When a nonconformity occurs, the organisation shall: a) react to the nonconformity and, as applicable: 1) take action to control and correct it; 2) deal with the consequences; b) evaluate the need for action to eliminate the causes of the nonconformity, in order that it does not recur or occur elsewhere, by: 1) reviewing the nonconformity; 2) determining the causes of the nonconformity; 3) determining if similar nonconformities exist, or could potentially occur; c) implement any action needed; d) review the effectiveness of any corrective action taken; e) make changes to the information security management system, if necessary. Corrective actions shall be appropriate to the effects of the nonconformities encountered. The organisation shall retain documented information as evidence of: a) the nature of the nonconformities and any subsequent actions taken; b) the results of any corrective action.",
32718
- implementation_notes: "Establish a formal nonconformity and corrective action procedure. Use root cause analysis techniques (5 Whys, fishbone diagram). Assign corrective actions to owners with due dates. Verify effectiveness before closing. Retain records of nonconformities and corrective actions.",
32719
- related_controls: [
32720
- "5.27",
32721
- "5.35"
32722
- ]
32723
- }
32724
- ];
32725
-
32726
- // src/seed/checksums.json
32727
- var checksums_default = {
32728
- "controls-2022.json": "f6ccfc42c3ebe39695f8d5c75face62077bffceb28a1786207969c75d5b78cba",
32729
- "controls-2013.json": "653ecfb8e22e134d836d3ee38f48839152dd4a2c382a38e1f2052d9a4dbe8c39",
32730
- "version-mapping.json": "458e2acf023bcd4636113df5b5fc1252c82db62e382a8887fcdfcb53ed62687a",
32731
- "clause-requirements.json": "ef72f98436a1faba117fe333c096b365180aa7a7d5eeebb9cd2f70cbd542592c"
32732
- };
32733
-
32734
- // src/seed/seeder.ts
32735
- function sha256(data) {
32736
- return (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(data, null, 2)).digest("hex");
32737
- }
32738
- function verifyChecksums() {
32739
- const checks = [
32740
- { key: "controls-2022.json", data: controls_2022_default },
32741
- { key: "controls-2013.json", data: controls_2013_default },
32742
- { key: "version-mapping.json", data: version_mapping_default },
32743
- { key: "clause-requirements.json", data: clause_requirements_default }
32744
- ];
32745
- for (const { key, data } of checks) {
32746
- const actual = sha256(data);
32747
- const expected = checksums_default[key];
32748
- if (!expected) {
32749
- throw new Error(
32750
- `[seeder] No checksum entry found for "${key}". Run "npm run generate-checksums" to regenerate checksums.json.`
32751
- );
32752
- }
32753
- if (actual !== expected) {
32754
- throw new Error(
32755
- `[seeder] Checksum mismatch for "${key}".
32756
- expected: ${expected}
32757
- actual: ${actual}
32758
- Seed data may have been modified. Run "npm run generate-checksums" to update checksums.json.`
32759
- );
32760
- }
32761
- }
32762
- console.error("[seeder] Checksum verification passed.");
32763
- }
32764
- function stableId(key) {
32765
- const h = (0, import_node_crypto3.createHash)("sha256").update(key).digest("hex");
32766
- return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
32767
- }
32768
- function seedAll(db) {
32769
- verifyChecksums();
32770
- const existing = db.prepare("SELECT count(*) AS n FROM controls").get();
32771
- if (existing.n > 0) {
32772
- console.error(`[seeder] Seed data already present (${existing.n} controls). Skipping.`);
32773
- return;
32774
- }
32775
- console.error("[seeder] Starting seed run\u2026");
32776
- const t0 = Date.now();
32777
- const seed = db.transaction(() => {
32778
- _seedControls2022(db);
32779
- _seedControls2013(db);
32780
- _seedVersionMappings(db);
32781
- _seedClauseRequirements(db);
32782
- _rebuildFts(db);
32783
- });
32784
- seed();
32785
- const elapsed = Date.now() - t0;
32786
- const totals = db.prepare(`
32787
- SELECT
32788
- (SELECT count(*) FROM controls WHERE version='2022') AS c2022,
32789
- (SELECT count(*) FROM controls WHERE version='2013') AS c2013,
32790
- (SELECT count(*) FROM controls WHERE new_in_2022=1) AS new22,
32791
- (SELECT count(*) FROM control_version_mapping) AS mappings,
32792
- (SELECT count(*) FROM clause_requirements) AS clauses
32793
- `).get();
32794
- console.error(
32795
- `[seeder] Done in ${elapsed}ms \u2014 controls-2022: ${totals.c2022}, controls-2013: ${totals.c2013}, new-in-2022: ${totals.new22}, mappings: ${totals.mappings}, clauses: ${totals.clauses}`
32796
- );
32797
- }
32798
- function _seedControls2022(db) {
32799
- const stmt = db.prepare(`
32800
- INSERT OR IGNORE INTO controls
32801
- (id, control_id, version, name, theme, description, guidance,
32802
- control_type, attributes, related_controls, new_in_2022, iso_clause_refs)
32803
- VALUES
32804
- (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
32805
- `);
32806
- for (const row of controls_2022_default) {
32807
- stmt.run(
32808
- stableId(`2022:${row.control_id}`),
32809
- row.control_id,
32810
- "2022",
32811
- row.name,
32812
- row.theme,
32813
- row.description,
32814
- row.guidance ?? null,
32815
- JSON.stringify(row.control_type),
32816
- row.attributes ? JSON.stringify(row.attributes) : null,
32817
- JSON.stringify(row.related_controls ?? []),
32818
- row.new_in_2022 ? 1 : 0,
32819
- JSON.stringify(row.iso_clause_refs ?? [])
32820
- );
32821
- }
32822
- console.error(`[seeder] Inserted ${controls_2022_default.length} 2022 controls.`);
32823
- }
32824
- function _seedControls2013(db) {
32825
- const stmt = db.prepare(`
32826
- INSERT OR IGNORE INTO controls
32827
- (id, control_id, version, name, theme, description, guidance,
32828
- control_type, attributes, related_controls, new_in_2022, iso_clause_refs)
32829
- VALUES
32830
- (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
32831
- `);
32832
- for (const row of controls_2013_default) {
32833
- stmt.run(
32834
- stableId(`2013:${row.control_id}`),
32835
- row.control_id,
32836
- "2013",
32837
- row.name,
32838
- row.theme,
32839
- row.description,
32840
- row.guidance ?? null,
32841
- JSON.stringify(row.control_type),
32842
- null,
32843
- // 2013 controls have no 2022-style attributes
32844
- JSON.stringify(row.related_controls ?? []),
32845
- 0,
32846
- // new_in_2022 is always false for 2013 controls
32847
- JSON.stringify(row.iso_clause_refs ?? [])
32848
- );
32849
- }
32850
- console.error(`[seeder] Inserted ${controls_2013_default.length} 2013 controls.`);
32851
- }
32852
- function _seedVersionMappings(db) {
32853
- const stmt = db.prepare(`
32854
- INSERT OR IGNORE INTO control_version_mapping
32855
- (id, v2013_id, v2022_id, mapping_type, change_summary, migration_notes)
32856
- VALUES
32857
- (?, ?, ?, ?, ?, ?)
32858
- `);
32859
- for (const row of version_mapping_default) {
32860
- const naturalKey = `mapping:${row.v2013_id ?? "null"}:${row.v2022_id ?? "null"}:${row.mapping_type}`;
32861
- stmt.run(
32862
- stableId(naturalKey),
32863
- row.v2013_id ?? null,
32864
- row.v2022_id ?? null,
32865
- row.mapping_type,
32866
- row.change_summary,
32867
- row.migration_notes ?? null
32868
- );
32869
- }
32870
- console.error(`[seeder] Inserted ${version_mapping_default.length} version mappings.`);
32871
- }
32872
- function _seedClauseRequirements(db) {
32873
- const stmt = db.prepare(`
32874
- INSERT OR IGNORE INTO clause_requirements
32875
- (id, clause_id, parent_id, title, requirement_text,
32876
- implementation_notes, related_controls)
32877
- VALUES
32878
- (?, ?, ?, ?, ?, ?, ?)
32879
- `);
32880
- const clauses = clause_requirements_default;
32881
- const idMap = /* @__PURE__ */ new Map();
32882
- for (const row of clauses) {
32883
- idMap.set(row.clause_id, stableId(`clause:${row.clause_id}`));
32884
- }
32885
- const sorted = [...clauses].sort((a, b) => {
32886
- if (a.parent_id === null && b.parent_id !== null) return -1;
32887
- if (a.parent_id !== null && b.parent_id === null) return 1;
32888
- return a.clause_id.localeCompare(b.clause_id);
32889
- });
32890
- for (const row of sorted) {
32891
- const rowId = idMap.get(row.clause_id);
32892
- const parentDbId = row.parent_id ? idMap.get(row.parent_id) ?? null : null;
32893
- stmt.run(
32894
- rowId,
32895
- row.clause_id,
32896
- parentDbId,
32897
- row.title,
32898
- row.requirement_text,
32899
- row.implementation_notes ?? null,
32900
- JSON.stringify(row.related_controls ?? [])
32901
- );
33091
+ {
33092
+ clause_id: "9.3.2",
33093
+ parent_id: "9.3",
33094
+ title: "Management review inputs",
33095
+ requirement_text: "The management review shall include consideration of: status of actions from previous reviews; changes in external/internal issues; feedback on information security performance; feedback from interested parties; results of risk assessment and risk treatment plan status; opportunities for continual improvement.",
33096
+ implementation_notes: "Prepare management review inputs in advance. Include trend data where available. Present metrics and KPIs. Summarise key risks and treatment status. Include audit findings and corrective action status.",
33097
+ related_controls: [
33098
+ "5.35",
33099
+ "5.36"
33100
+ ]
33101
+ },
33102
+ {
33103
+ clause_id: "9.3.3",
33104
+ parent_id: "9.3",
33105
+ title: "Management review results",
33106
+ requirement_text: "The outputs of the management review shall include decisions related to continual improvement opportunities and any need for changes to the information security management system.",
33107
+ implementation_notes: "Document all decisions with clear action items, owners, and due dates. Communicate decisions to relevant stakeholders. Track action items to completion. Use management review outputs to drive ISMS improvement.",
33108
+ related_controls: [
33109
+ "5.1"
33110
+ ]
33111
+ },
33112
+ {
33113
+ clause_id: "10",
33114
+ parent_id: null,
33115
+ title: "Improvement",
33116
+ requirement_text: "The organisation shall continually improve the suitability, adequacy and effectiveness of the information security management system.",
33117
+ implementation_notes: "Improvement activities include corrective actions, preventive actions, and continual improvement initiatives. Track and trend nonconformities. Use improvement inputs from audits, incidents, management reviews, and performance metrics.",
33118
+ related_controls: [
33119
+ "5.27",
33120
+ "5.35",
33121
+ "5.36"
33122
+ ]
33123
+ },
33124
+ {
33125
+ clause_id: "10.1",
33126
+ parent_id: "10",
33127
+ title: "Continual improvement",
33128
+ requirement_text: "The organisation shall continually improve the suitability, adequacy and effectiveness of the information security management system.",
33129
+ implementation_notes: "Establish a formal process for identifying and implementing improvements. Use Lessons Learned from incidents, audit findings, and management reviews as inputs. Track improvement initiatives to completion. Measure the effectiveness of improvements.",
33130
+ related_controls: [
33131
+ "5.27"
33132
+ ]
33133
+ },
33134
+ {
33135
+ clause_id: "10.2",
33136
+ parent_id: "10",
33137
+ title: "Nonconformity and corrective action",
33138
+ requirement_text: "When a nonconformity occurs, the organisation shall: a) react to the nonconformity and, as applicable: 1) take action to control and correct it; 2) deal with the consequences; b) evaluate the need for action to eliminate the causes of the nonconformity, in order that it does not recur or occur elsewhere, by: 1) reviewing the nonconformity; 2) determining the causes of the nonconformity; 3) determining if similar nonconformities exist, or could potentially occur; c) implement any action needed; d) review the effectiveness of any corrective action taken; e) make changes to the information security management system, if necessary. Corrective actions shall be appropriate to the effects of the nonconformities encountered. The organisation shall retain documented information as evidence of: a) the nature of the nonconformities and any subsequent actions taken; b) the results of any corrective action.",
33139
+ implementation_notes: "Establish a formal nonconformity and corrective action procedure. Use root cause analysis techniques (5 Whys, fishbone diagram). Assign corrective actions to owners with due dates. Verify effectiveness before closing. Retain records of nonconformities and corrective actions.",
33140
+ related_controls: [
33141
+ "5.27",
33142
+ "5.35"
33143
+ ]
32902
33144
  }
32903
- console.error(`[seeder] Inserted ${clauses.length} clause requirements.`);
32904
- }
32905
- function _rebuildFts(db) {
32906
- db.prepare("INSERT INTO controls_fts(controls_fts) VALUES('rebuild')").run();
32907
- console.error("[seeder] FTS5 index rebuilt.");
32908
- }
32909
-
32910
- // src/server.ts
32911
- var import_mcp6 = require("@modelcontextprotocol/sdk/server/mcp.js");
32912
-
32913
- // src/tools/index.ts
32914
- var import_zod2 = require("zod");
32915
-
32916
- // src/auth/api-key.ts
32917
- var import_node_crypto4 = require("crypto");
32918
- var import_node_crypto5 = require("crypto");
33145
+ ];
32919
33146
 
32920
- // src/types/errors.ts
32921
- var HTTP_STATUS = {
32922
- AUTH_MISSING: 401,
32923
- AUTH_INVALID: 401,
32924
- AUTH_EXPIRED: 401,
32925
- AUTH_REVOKED: 401,
32926
- RBAC_DENIED: 403,
32927
- RATE_LIMITED: 429,
32928
- VALIDATION_ERROR: 400,
32929
- NOT_FOUND: 404,
32930
- BUSINESS_RULE: 422,
32931
- INTERNAL_ERROR: 500,
32932
- INTEGRATION_ERROR: 502
32933
- };
32934
- var McpError = class extends Error {
32935
- error_code;
32936
- http_status;
32937
- field;
32938
- hint;
32939
- docs_ref;
32940
- constructor(opts) {
32941
- super(opts.message);
32942
- this.name = "McpError";
32943
- this.error_code = opts.error_code;
32944
- this.http_status = HTTP_STATUS[opts.error_code];
32945
- this.field = opts.field;
32946
- this.hint = opts.hint;
32947
- this.docs_ref = opts.docs_ref;
32948
- }
32949
- /**
32950
- * Serialise to the structured tool result format expected by the MCP SDK.
32951
- * Returns a single content block with JSON text so Claude can parse it.
32952
- */
32953
- toToolResult() {
32954
- const body = {
32955
- error_code: this.error_code,
32956
- http_status: this.http_status,
32957
- message: this.message
32958
- };
32959
- if (this.field) body["field"] = this.field;
32960
- if (this.hint) body["hint"] = this.hint;
32961
- if (this.docs_ref) body["docs_ref"] = this.docs_ref;
32962
- return {
32963
- content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
32964
- isError: true
32965
- };
32966
- }
33147
+ // src/seed/checksums.json
33148
+ var checksums_default = {
33149
+ "controls-2022.json": "f6ccfc42c3ebe39695f8d5c75face62077bffceb28a1786207969c75d5b78cba",
33150
+ "controls-2013.json": "653ecfb8e22e134d836d3ee38f48839152dd4a2c382a38e1f2052d9a4dbe8c39",
33151
+ "version-mapping.json": "458e2acf023bcd4636113df5b5fc1252c82db62e382a8887fcdfcb53ed62687a",
33152
+ "clause-requirements.json": "ef72f98436a1faba117fe333c096b365180aa7a7d5eeebb9cd2f70cbd542592c"
32967
33153
  };
32968
- function authMissing() {
32969
- return new McpError({
32970
- error_code: "AUTH_MISSING",
32971
- message: "No API key provided. Pass your key via MCP_API_KEY env var or _meta.apiKey.",
32972
- hint: "Set MCP_API_KEY env var or include apiKey in _meta"
32973
- });
32974
- }
32975
- function authInvalid() {
32976
- return new McpError({
32977
- error_code: "AUTH_INVALID",
32978
- message: "HMAC validation failed. The provided API key is not recognised.",
32979
- hint: "Verify your API key \u2014 run: iso27001-mcp keygen"
32980
- });
32981
- }
32982
- function authExpired() {
32983
- return new McpError({
32984
- error_code: "AUTH_EXPIRED",
32985
- message: "API key has expired.",
32986
- hint: "Generate a new key: iso27001-mcp keygen"
32987
- });
32988
- }
32989
- function authRevoked() {
32990
- return new McpError({
32991
- error_code: "AUTH_REVOKED",
32992
- message: "API key has been revoked.",
32993
- hint: "Generate a new key: iso27001-mcp keygen --role [role]"
32994
- });
32995
- }
32996
- function rbacDenied(toolName, requiredRole) {
32997
- return new McpError({
32998
- error_code: "RBAC_DENIED",
32999
- message: `Your role does not have permission to call '${toolName}'. Requires: ${requiredRole}.`,
33000
- hint: "Your role cannot call this tool \u2014 contact your admin to get a key with a higher role"
33001
- });
33002
- }
33003
- function rateLimited() {
33004
- return new McpError({
33005
- error_code: "RATE_LIMITED",
33006
- message: "Too many requests. Exceeded RATE_LIMIT_RPM.",
33007
- hint: "Slow down or raise RATE_LIMIT_RPM in your .env"
33008
- });
33009
- }
33010
- function notFound(entity, id) {
33011
- return new McpError({
33012
- error_code: "NOT_FOUND",
33013
- message: `${entity} not found: ${id}`
33014
- });
33015
- }
33016
- function businessRule(message, hint, docsRef) {
33017
- return new McpError({
33018
- error_code: "BUSINESS_RULE",
33019
- message,
33020
- hint,
33021
- docs_ref: docsRef
33022
- });
33023
- }
33024
- function integrationError(service, message, hint) {
33025
- return new McpError({
33026
- error_code: "INTEGRATION_ERROR",
33027
- message: `${service} integration error: ${message}`,
33028
- hint
33029
- });
33030
- }
33031
33154
 
33032
- // src/auth/api-key.ts
33033
- function hmacSha256(secret, data) {
33034
- return (0, import_node_crypto4.createHmac)("sha256", secret).update(data).digest("hex");
33035
- }
33036
- function generateKey(label, role, expiresAt) {
33037
- const db = getDb();
33038
- const rawKey = "iso27001_" + (0, import_node_crypto4.randomBytes)(24).toString("base64url");
33039
- const keyHash = hmacSha256(requireEnv("HMAC_SECRET"), rawKey);
33040
- db.prepare(`
33041
- INSERT INTO api_keys (id, key_hash, label, role, expires_at, created_at)
33042
- VALUES (?, ?, ?, ?, ?, datetime('now'))
33043
- `).run(
33044
- (0, import_node_crypto5.randomUUID)(),
33045
- keyHash,
33046
- label,
33047
- role,
33048
- expiresAt ?? null
33049
- );
33050
- console.log("=".repeat(60));
33051
- console.log("API Key generated (save now \u2014 NOT stored in plaintext):");
33052
- console.log("");
33053
- console.log(" " + rawKey);
33054
- console.log("");
33055
- console.log(` Label: ${label}`);
33056
- console.log(` Role: ${role}`);
33057
- console.log(` Expires: ${expiresAt ?? "never"}`);
33058
- console.log("=".repeat(60));
33059
- return rawKey;
33060
- }
33061
- function validateKey(rawKey) {
33062
- if (!rawKey) throw authMissing();
33063
- const secret = requireEnv("HMAC_SECRET");
33064
- const keyHash = hmacSha256(secret, rawKey);
33065
- const db = getDb();
33066
- const row = db.prepare(
33067
- "SELECT key_hash FROM api_keys WHERE key_hash = ? LIMIT 1"
33068
- ).get(keyHash);
33069
- if (!row) {
33070
- const dummyA = Buffer.alloc(32, 0);
33071
- const dummyB = Buffer.alloc(32, 1);
33072
- (0, import_node_crypto4.timingSafeEqual)(dummyA, dummyB);
33073
- throw authInvalid();
33074
- }
33075
- const storedBuf = Buffer.from(row.key_hash, "hex");
33076
- const computedBuf = Buffer.from(keyHash, "hex");
33077
- if (storedBuf.length !== computedBuf.length || !(0, import_node_crypto4.timingSafeEqual)(storedBuf, computedBuf)) {
33078
- throw authInvalid();
33079
- }
33080
- return keyHash;
33155
+ // src/seed/seeder.ts
33156
+ function sha256(data) {
33157
+ return (0, import_node_crypto5.createHash)("sha256").update(JSON.stringify(data, null, 2)).digest("hex");
33081
33158
  }
33082
- function loadRole(keyHash) {
33083
- const db = getDb();
33084
- const row = db.prepare(`
33085
- SELECT id, role, expires_at, revoked_at FROM api_keys WHERE key_hash = ?
33086
- `).get(keyHash);
33087
- if (!row) throw authInvalid();
33088
- if (row.revoked_at) throw authRevoked();
33089
- if (row.expires_at) {
33090
- const expiry = new Date(row.expires_at);
33091
- if (expiry < /* @__PURE__ */ new Date()) throw authExpired();
33092
- }
33093
- try {
33094
- db.prepare("UPDATE api_keys SET last_used_at = datetime('now') WHERE id = ?").run(row.id);
33095
- } catch {
33159
+ function verifyChecksums() {
33160
+ const checks = [
33161
+ { key: "controls-2022.json", data: controls_2022_default },
33162
+ { key: "controls-2013.json", data: controls_2013_default },
33163
+ { key: "version-mapping.json", data: version_mapping_default },
33164
+ { key: "clause-requirements.json", data: clause_requirements_default }
33165
+ ];
33166
+ for (const { key, data } of checks) {
33167
+ const actual = sha256(data);
33168
+ const expected = checksums_default[key];
33169
+ if (!expected) {
33170
+ throw new Error(
33171
+ `[seeder] No checksum entry found for "${key}". Run "npm run generate-checksums" to regenerate checksums.json.`
33172
+ );
33173
+ }
33174
+ if (actual !== expected) {
33175
+ throw new Error(
33176
+ `[seeder] Checksum mismatch for "${key}".
33177
+ expected: ${expected}
33178
+ actual: ${actual}
33179
+ Seed data may have been modified. Run "npm run generate-checksums" to update checksums.json.`
33180
+ );
33181
+ }
33096
33182
  }
33097
- return row.role;
33183
+ console.error("[seeder] Checksum verification passed.");
33098
33184
  }
33099
- function listKeys() {
33100
- const db = getDb();
33101
- const rows = db.prepare(`
33102
- SELECT id, label, role, created_at, expires_at, revoked_at, last_used_at
33103
- FROM api_keys
33104
- ORDER BY created_at DESC
33105
- `).all();
33106
- const now2 = /* @__PURE__ */ new Date();
33107
- return rows.map((r) => {
33108
- let status = "active";
33109
- if (r.revoked_at) {
33110
- status = "revoked";
33111
- } else if (r.expires_at && new Date(r.expires_at) < now2) {
33112
- status = "expired";
33113
- }
33114
- return {
33115
- id: r.id,
33116
- label: r.label,
33117
- role: r.role,
33118
- created_at: r.created_at,
33119
- expires_at: r.expires_at,
33120
- last_used_at: r.last_used_at,
33121
- status
33122
- };
33185
+ function stableId(key) {
33186
+ const h = (0, import_node_crypto5.createHash)("sha256").update(key).digest("hex");
33187
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-4${h.slice(13, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
33188
+ }
33189
+ function seedAll(db) {
33190
+ verifyChecksums();
33191
+ const existing = db.prepare("SELECT count(*) AS n FROM controls").get();
33192
+ if (existing.n > 0) {
33193
+ console.error(`[seeder] Seed data already present (${existing.n} controls). Skipping.`);
33194
+ return;
33195
+ }
33196
+ console.error("[seeder] Starting seed run\u2026");
33197
+ const t0 = Date.now();
33198
+ const seed = db.transaction(() => {
33199
+ _seedControls2022(db);
33200
+ _seedControls2013(db);
33201
+ _seedVersionMappings(db);
33202
+ _seedClauseRequirements(db);
33203
+ _rebuildFts(db);
33123
33204
  });
33205
+ seed();
33206
+ const elapsed = Date.now() - t0;
33207
+ const totals = db.prepare(`
33208
+ SELECT
33209
+ (SELECT count(*) FROM controls WHERE version='2022') AS c2022,
33210
+ (SELECT count(*) FROM controls WHERE version='2013') AS c2013,
33211
+ (SELECT count(*) FROM controls WHERE new_in_2022=1) AS new22,
33212
+ (SELECT count(*) FROM control_version_mapping) AS mappings,
33213
+ (SELECT count(*) FROM clause_requirements) AS clauses
33214
+ `).get();
33215
+ console.error(
33216
+ `[seeder] Done in ${elapsed}ms \u2014 controls-2022: ${totals.c2022}, controls-2013: ${totals.c2013}, new-in-2022: ${totals.new22}, mappings: ${totals.mappings}, clauses: ${totals.clauses}`
33217
+ );
33124
33218
  }
33125
- function revokeKey(label) {
33126
- const db = getDb();
33127
- const row = db.prepare("SELECT id FROM api_keys WHERE label = ?").get(label);
33128
- if (!row) {
33129
- throw new Error(`API key with label '${label}' not found.`);
33219
+ function _seedControls2022(db) {
33220
+ const stmt = db.prepare(`
33221
+ INSERT OR IGNORE INTO controls
33222
+ (id, control_id, version, name, theme, description, guidance,
33223
+ control_type, attributes, related_controls, new_in_2022, iso_clause_refs)
33224
+ VALUES
33225
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
33226
+ `);
33227
+ for (const row of controls_2022_default) {
33228
+ stmt.run(
33229
+ stableId(`2022:${row.control_id}`),
33230
+ row.control_id,
33231
+ "2022",
33232
+ row.name,
33233
+ row.theme,
33234
+ row.description,
33235
+ row.guidance ?? null,
33236
+ JSON.stringify(row.control_type),
33237
+ row.attributes ? JSON.stringify(row.attributes) : null,
33238
+ JSON.stringify(row.related_controls ?? []),
33239
+ row.new_in_2022 ? 1 : 0,
33240
+ JSON.stringify(row.iso_clause_refs ?? [])
33241
+ );
33130
33242
  }
33131
- db.prepare("UPDATE api_keys SET revoked_at = datetime('now') WHERE id = ?").run(row.id);
33132
- console.log(`[auth] Key '${label}' revoked.`);
33243
+ console.error(`[seeder] Inserted ${controls_2022_default.length} 2022 controls.`);
33133
33244
  }
33134
- function warnAdminExpiry() {
33135
- const db = getDb();
33136
- const rows = db.prepare(`
33137
- SELECT label FROM api_keys
33138
- WHERE role = 'admin' AND expires_at IS NULL AND revoked_at IS NULL
33139
- `).all();
33140
- if (rows.length > 0) {
33141
- const labels = rows.map((r) => r.label).join(", ");
33142
- console.warn(
33143
- `[SECURITY] Admin keys without expiry: ${labels}. Consider setting --expires to limit blast radius.`
33245
+ function _seedControls2013(db) {
33246
+ const stmt = db.prepare(`
33247
+ INSERT OR IGNORE INTO controls
33248
+ (id, control_id, version, name, theme, description, guidance,
33249
+ control_type, attributes, related_controls, new_in_2022, iso_clause_refs)
33250
+ VALUES
33251
+ (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
33252
+ `);
33253
+ for (const row of controls_2013_default) {
33254
+ stmt.run(
33255
+ stableId(`2013:${row.control_id}`),
33256
+ row.control_id,
33257
+ "2013",
33258
+ row.name,
33259
+ row.theme,
33260
+ row.description,
33261
+ row.guidance ?? null,
33262
+ JSON.stringify(row.control_type),
33263
+ null,
33264
+ // 2013 controls have no 2022-style attributes
33265
+ JSON.stringify(row.related_controls ?? []),
33266
+ 0,
33267
+ // new_in_2022 is always false for 2013 controls
33268
+ JSON.stringify(row.iso_clause_refs ?? [])
33144
33269
  );
33145
33270
  }
33271
+ console.error(`[seeder] Inserted ${controls_2013_default.length} 2013 controls.`);
33146
33272
  }
33147
- function parseExpiresFlag(value) {
33148
- if (/^\d{4}-\d{2}-\d{2}$/.test(value)) return value;
33149
- const days = /^(\d+)d$/.exec(value);
33150
- if (days) {
33151
- const d = /* @__PURE__ */ new Date();
33152
- d.setDate(d.getDate() + parseInt(days[1], 10));
33153
- return d.toISOString().split("T")[0];
33273
+ function _seedVersionMappings(db) {
33274
+ const stmt = db.prepare(`
33275
+ INSERT OR IGNORE INTO control_version_mapping
33276
+ (id, v2013_id, v2022_id, mapping_type, change_summary, migration_notes)
33277
+ VALUES
33278
+ (?, ?, ?, ?, ?, ?)
33279
+ `);
33280
+ for (const row of version_mapping_default) {
33281
+ const naturalKey = `mapping:${row.v2013_id ?? "null"}:${row.v2022_id ?? "null"}:${row.mapping_type}`;
33282
+ stmt.run(
33283
+ stableId(naturalKey),
33284
+ row.v2013_id ?? null,
33285
+ row.v2022_id ?? null,
33286
+ row.mapping_type,
33287
+ row.change_summary,
33288
+ row.migration_notes ?? null
33289
+ );
33154
33290
  }
33155
- const years = /^(\d+)y$/.exec(value);
33156
- if (years) {
33157
- const d = /* @__PURE__ */ new Date();
33158
- d.setFullYear(d.getFullYear() + parseInt(years[1], 10));
33159
- return d.toISOString().split("T")[0];
33291
+ console.error(`[seeder] Inserted ${version_mapping_default.length} version mappings.`);
33292
+ }
33293
+ function _seedClauseRequirements(db) {
33294
+ const stmt = db.prepare(`
33295
+ INSERT OR IGNORE INTO clause_requirements
33296
+ (id, clause_id, parent_id, title, requirement_text,
33297
+ implementation_notes, related_controls)
33298
+ VALUES
33299
+ (?, ?, ?, ?, ?, ?, ?)
33300
+ `);
33301
+ const clauses = clause_requirements_default;
33302
+ const idMap = /* @__PURE__ */ new Map();
33303
+ for (const row of clauses) {
33304
+ idMap.set(row.clause_id, stableId(`clause:${row.clause_id}`));
33160
33305
  }
33161
- throw new Error(
33162
- `Invalid --expires value: '${value}'. Use '90d', '1y', or 'YYYY-MM-DD'.`
33163
- );
33306
+ const sorted = [...clauses].sort((a, b) => {
33307
+ if (a.parent_id === null && b.parent_id !== null) return -1;
33308
+ if (a.parent_id !== null && b.parent_id === null) return 1;
33309
+ return a.clause_id.localeCompare(b.clause_id);
33310
+ });
33311
+ for (const row of sorted) {
33312
+ const rowId = idMap.get(row.clause_id);
33313
+ const parentDbId = row.parent_id ? idMap.get(row.parent_id) ?? null : null;
33314
+ stmt.run(
33315
+ rowId,
33316
+ row.clause_id,
33317
+ parentDbId,
33318
+ row.title,
33319
+ row.requirement_text,
33320
+ row.implementation_notes ?? null,
33321
+ JSON.stringify(row.related_controls ?? [])
33322
+ );
33323
+ }
33324
+ console.error(`[seeder] Inserted ${clauses.length} clause requirements.`);
33325
+ }
33326
+ function _rebuildFts(db) {
33327
+ db.prepare("INSERT INTO controls_fts(controls_fts) VALUES('rebuild')").run();
33328
+ console.error("[seeder] FTS5 index rebuilt.");
33164
33329
  }
33165
33330
 
33331
+ // src/server.ts
33332
+ var import_mcp8 = require("@modelcontextprotocol/sdk/server/mcp.js");
33333
+
33334
+ // src/tools/index.ts
33335
+ var import_zod2 = require("zod");
33336
+
33166
33337
  // src/auth/rbac.ts
33167
33338
  var ROLE_LEVEL = {
33168
33339
  viewer: 0,
@@ -33237,7 +33408,26 @@ var TOOL_MIN_ROLE = {
33237
33408
  get_procedure: "viewer",
33238
33409
  update_procedure: "admin",
33239
33410
  list_procedures: "viewer",
33240
- export_procedure: "analyst"
33411
+ export_procedure: "analyst",
33412
+ // ── Group 12: Management Review (Clause 9.3) ─────────────
33413
+ // Schedule/record: admin; read: viewer
33414
+ create_management_review: "admin",
33415
+ record_review_input: "admin",
33416
+ record_review_output: "admin",
33417
+ complete_management_review: "admin",
33418
+ get_management_review: "viewer",
33419
+ list_management_reviews: "viewer",
33420
+ // ── Group 13: Improvement Plan (Clause 10.1) ─────────────
33421
+ // Create/update: analyst; read: viewer
33422
+ create_improvement_opportunity: "analyst",
33423
+ update_improvement_opportunity: "analyst",
33424
+ get_improvement_opportunity: "viewer",
33425
+ list_improvement_opportunities: "viewer",
33426
+ // ── Group 14: Evidence Templates ──────────────────────────
33427
+ // Generate (writes two tables): analyst; read: viewer
33428
+ generate_evidence_document: "analyst",
33429
+ get_evidence_document: "viewer",
33430
+ list_evidence_documents: "viewer"
33241
33431
  };
33242
33432
  function checkPermission(role, toolName) {
33243
33433
  const minRole = TOOL_MIN_ROLE[toolName] ?? "admin";
@@ -33358,12 +33548,29 @@ function writeAuditEvent(event) {
33358
33548
  const db = getDb();
33359
33549
  const id = (0, import_node_crypto6.randomUUID)();
33360
33550
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace("T", " ").split(".")[0] + "Z";
33361
- const row_hash = (0, import_node_crypto6.createHash)("sha256").update(`${timestamp}|${event.tool}|${event.key_hash}|${event.outcome}`).digest("hex");
33551
+ const prevRow = db.prepare(
33552
+ "SELECT row_hash FROM audit_log ORDER BY rowid DESC LIMIT 1"
33553
+ ).get();
33554
+ const prev_hash = prevRow?.row_hash ?? null;
33555
+ const hmacSecret = requireEnv("HMAC_SECRET");
33556
+ const hashInput = [
33557
+ id,
33558
+ timestamp,
33559
+ event.tool,
33560
+ event.key_hash,
33561
+ event.role,
33562
+ event.params_json,
33563
+ event.outcome,
33564
+ event.error_message ?? "",
33565
+ String(event.duration_ms),
33566
+ prev_hash ?? "GENESIS"
33567
+ ].join("|");
33568
+ const row_hash = (0, import_node_crypto6.createHmac)("sha256", hmacSecret).update(hashInput).digest("hex");
33362
33569
  db.prepare(`
33363
33570
  INSERT INTO audit_log
33364
33571
  (id, timestamp, tool, key_hash, role, params_json,
33365
- outcome, error_message, duration_ms, row_hash)
33366
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
33572
+ outcome, error_message, duration_ms, prev_hash, row_hash)
33573
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
33367
33574
  `).run(
33368
33575
  id,
33369
33576
  timestamp,
@@ -33374,9 +33581,10 @@ function writeAuditEvent(event) {
33374
33581
  event.outcome,
33375
33582
  event.error_message,
33376
33583
  event.duration_ms,
33584
+ prev_hash,
33377
33585
  row_hash
33378
33586
  );
33379
- const full = { id, timestamp, row_hash, ...event };
33587
+ const full = { id, timestamp, prev_hash, row_hash, ...event };
33380
33588
  try {
33381
33589
  const logPath = getEnv("AUDIT_LOG_PATH", "./audit.jsonl");
33382
33590
  (0, import_node_fs.appendFileSync)(logPath, JSON.stringify(full) + "\n", "utf8");
@@ -33834,6 +34042,132 @@ var ExportProcedureSchema = import_zod.z.object({
33834
34042
  procedure_id: uuid,
33835
34043
  format: formatMarkdownJson
33836
34044
  });
34045
+ var reviewStatusEnum = import_zod.z.enum(["planned", "in_progress", "completed"]);
34046
+ var reviewInputCategoryEnum = import_zod.z.enum([
34047
+ "previous_action_status",
34048
+ "external_internal_issues",
34049
+ "interested_party_needs",
34050
+ "isms_performance",
34051
+ "interested_party_feedback",
34052
+ "risk_assessment_results",
34053
+ "improvement_opportunities"
34054
+ ]);
34055
+ var reviewOutputTypeEnum = import_zod.z.enum([
34056
+ "improvement_decision",
34057
+ "isms_change_decision"
34058
+ ]);
34059
+ var reviewTrendEnum = import_zod.z.enum([
34060
+ "improving",
34061
+ "stable",
34062
+ "declining",
34063
+ "insufficient_data"
34064
+ ]);
34065
+ var CreateManagementReviewSchema = import_zod.z.object({
34066
+ title: shortText(200),
34067
+ review_date: date,
34068
+ reviewers: import_zod.z.array(shortText(200)).min(1, "At least one reviewer required"),
34069
+ scope_notes: freeText(2e3).optional()
34070
+ });
34071
+ var RecordReviewInputSchema = import_zod.z.object({
34072
+ review_id: uuid,
34073
+ input_category: reviewInputCategoryEnum,
34074
+ summary: freeText(2e3),
34075
+ details: freeText(4e3).optional(),
34076
+ trend: reviewTrendEnum.optional()
34077
+ });
34078
+ var RecordReviewOutputSchema = import_zod.z.object({
34079
+ review_id: uuid,
34080
+ output_type: reviewOutputTypeEnum,
34081
+ decision: freeText(2e3),
34082
+ owner: shortText(200).optional(),
34083
+ due_date: date.optional()
34084
+ });
34085
+ var CompleteManagementReviewSchema = import_zod.z.object({
34086
+ review_id: uuid,
34087
+ completed_by: shortText(200)
34088
+ });
34089
+ var GetManagementReviewSchema = import_zod.z.object({
34090
+ review_id: uuid
34091
+ });
34092
+ var ListManagementReviewsSchema = import_zod.z.object({
34093
+ status: reviewStatusEnum.optional(),
34094
+ limit: paginationLimit,
34095
+ offset: paginationOffset
34096
+ });
34097
+ var improvementStatusEnum = import_zod.z.enum([
34098
+ "open",
34099
+ "in_progress",
34100
+ "implemented",
34101
+ "closed"
34102
+ ]);
34103
+ var improvementSourceEnum = import_zod.z.enum([
34104
+ "management_review",
34105
+ "risk_assessment",
34106
+ "audit",
34107
+ "monitoring",
34108
+ "other"
34109
+ ]);
34110
+ var improvementPriorityEnum = import_zod.z.enum([
34111
+ "low",
34112
+ "medium",
34113
+ "high",
34114
+ "critical"
34115
+ ]);
34116
+ var CreateImprovementOpportunitySchema = import_zod.z.object({
34117
+ title: shortText(200),
34118
+ description: freeText(2e3),
34119
+ source: improvementSourceEnum,
34120
+ priority: improvementPriorityEnum.optional().default("medium"),
34121
+ owner: shortText(200).optional(),
34122
+ target_date: date.optional(),
34123
+ review_id: uuid.optional()
34124
+ });
34125
+ var UpdateImprovementOpportunitySchema = import_zod.z.object({
34126
+ opportunity_id: uuid,
34127
+ status: improvementStatusEnum.optional(),
34128
+ owner: shortText(200).optional(),
34129
+ target_date: date.optional(),
34130
+ priority: improvementPriorityEnum.optional(),
34131
+ description: freeText(2e3).optional()
34132
+ });
34133
+ var GetImprovementOpportunitySchema = import_zod.z.object({
34134
+ opportunity_id: uuid
34135
+ });
34136
+ var ListImprovementOpportunitiesSchema = import_zod.z.object({
34137
+ status: improvementStatusEnum.optional(),
34138
+ source: improvementSourceEnum.optional(),
34139
+ priority: improvementPriorityEnum.optional(),
34140
+ review_id: uuid.optional(),
34141
+ limit: paginationLimit,
34142
+ offset: paginationOffset
34143
+ });
34144
+ var evidenceTemplateTypeEnum = import_zod.z.enum([
34145
+ "access_review_attestation",
34146
+ "training_acknowledgement",
34147
+ "supplier_security_questionnaire",
34148
+ "incident_post_mortem",
34149
+ "bcp_test_report",
34150
+ "risk_treatment_sign_off"
34151
+ ]);
34152
+ var GenerateEvidenceDocumentSchema = import_zod.z.object({
34153
+ template_type: evidenceTemplateTypeEnum,
34154
+ title: shortText(200),
34155
+ generated_by: shortText(200),
34156
+ organisation_name: shortText(200).optional(),
34157
+ // falls back to org profile
34158
+ control_id: import_zod.z.string().min(1).max(20).optional(),
34159
+ vars: import_zod.z.record(import_zod.z.string()).optional().default({})
34160
+ });
34161
+ var GetEvidenceDocumentSchema = import_zod.z.object({
34162
+ document_id: uuid
34163
+ });
34164
+ var ListEvidenceDocumentsSchema = import_zod.z.object({
34165
+ template_type: evidenceTemplateTypeEnum.optional(),
34166
+ generated_by: shortText(200).optional(),
34167
+ control_id: import_zod.z.string().min(1).max(20).optional(),
34168
+ limit: paginationLimit,
34169
+ offset: paginationOffset
34170
+ });
33837
34171
  var TOOL_SCHEMAS = {
33838
34172
  // Group 1
33839
34173
  get_control: GetControlSchema,
@@ -33895,7 +34229,23 @@ var TOOL_SCHEMAS = {
33895
34229
  get_procedure: GetProcedureSchema,
33896
34230
  update_procedure: UpdateProcedureSchema,
33897
34231
  list_procedures: ListProceduresSchema,
33898
- export_procedure: ExportProcedureSchema
34232
+ export_procedure: ExportProcedureSchema,
34233
+ // Group 12
34234
+ create_management_review: CreateManagementReviewSchema,
34235
+ record_review_input: RecordReviewInputSchema,
34236
+ record_review_output: RecordReviewOutputSchema,
34237
+ complete_management_review: CompleteManagementReviewSchema,
34238
+ get_management_review: GetManagementReviewSchema,
34239
+ list_management_reviews: ListManagementReviewsSchema,
34240
+ // Group 13
34241
+ create_improvement_opportunity: CreateImprovementOpportunitySchema,
34242
+ update_improvement_opportunity: UpdateImprovementOpportunitySchema,
34243
+ get_improvement_opportunity: GetImprovementOpportunitySchema,
34244
+ list_improvement_opportunities: ListImprovementOpportunitiesSchema,
34245
+ // Group 14
34246
+ generate_evidence_document: GenerateEvidenceDocumentSchema,
34247
+ get_evidence_document: GetEvidenceDocumentSchema,
34248
+ list_evidence_documents: ListEvidenceDocumentsSchema
33899
34249
  };
33900
34250
 
33901
34251
  // src/tools/server-info.ts
@@ -34974,6 +35324,28 @@ function loadTemplate(type, dir) {
34974
35324
  `Template file not found for '${type}' in '${dir}'. Run 'npm run build' to ensure templates are copied into dist/.`
34975
35325
  );
34976
35326
  }
35327
+ var PARTIAL_NAMES = ["org_header", "revision_block", "approver_signature"];
35328
+ function loadPartials() {
35329
+ const partials = {};
35330
+ for (const name of PARTIAL_NAMES) {
35331
+ 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`)
35335
+ ];
35336
+ for (const candidate of candidates) {
35337
+ try {
35338
+ partials[name] = (0, import_node_fs2.readFileSync)(candidate, "utf8");
35339
+ break;
35340
+ } catch {
35341
+ }
35342
+ }
35343
+ if (!partials[name]) {
35344
+ partials[name] = "";
35345
+ }
35346
+ }
35347
+ return partials;
35348
+ }
34977
35349
  function stripFrontmatter(raw) {
34978
35350
  const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
34979
35351
  if (!match) return { template: raw, clauseMappings: [], controlMappings: [] };
@@ -35113,8 +35485,10 @@ function handleCreatePolicy(args2) {
35113
35485
  approver: approver ?? "TBD",
35114
35486
  effective_date,
35115
35487
  next_review_date: next_review,
35116
- version: "1.0"
35117
- });
35488
+ version: "1.0",
35489
+ clause_mappings: clauseMappings.join(", "),
35490
+ control_mappings: controlMappings.join(", ")
35491
+ }, loadPartials());
35118
35492
  db.prepare(`
35119
35493
  INSERT INTO policies
35120
35494
  (id, type, organisation_name, scope, owner, approver, status, version,
@@ -35197,8 +35571,10 @@ function handleUpdatePolicy(args2) {
35197
35571
  approver: approver ?? current.approver ?? "TBD",
35198
35572
  effective_date: current.effective_date,
35199
35573
  next_review_date: current.next_review_date,
35200
- version: `${newVersion}.0`
35201
- });
35574
+ version: `${newVersion}.0`,
35575
+ clause_mappings: clauseMappings.join(", "),
35576
+ control_mappings: controlMappings.join(", ")
35577
+ }, loadPartials());
35202
35578
  db.prepare(`
35203
35579
  UPDATE policies SET
35204
35580
  scope = COALESCE(?, scope),
@@ -36036,8 +36412,10 @@ function handleCreateProcedure(args2) {
36036
36412
  effective_date,
36037
36413
  next_review_date: next_review,
36038
36414
  version: "1.0",
36039
- parent_policy_id: resolvedPolicyId ?? "N/A"
36040
- });
36415
+ parent_policy_id: resolvedPolicyId ?? "N/A",
36416
+ clause_mappings: clauseMappings.join(", "),
36417
+ control_mappings: controlMappings.join(", ")
36418
+ }, loadPartials());
36041
36419
  db.prepare(`
36042
36420
  INSERT INTO procedures
36043
36421
  (id, procedure_type, policy_id, organisation_name, scope, owner, approver,
@@ -36111,152 +36489,705 @@ function handleListProcedures(args2) {
36111
36489
  } = args2;
36112
36490
  const conditions = [];
36113
36491
  const params = [];
36114
- if (procedure_type) {
36115
- conditions.push("procedure_type = ?");
36116
- params.push(procedure_type);
36117
- }
36492
+ if (procedure_type) {
36493
+ conditions.push("procedure_type = ?");
36494
+ params.push(procedure_type);
36495
+ }
36496
+ if (status) {
36497
+ conditions.push("status = ?");
36498
+ params.push(status);
36499
+ }
36500
+ if (policy_id) {
36501
+ conditions.push("policy_id = ?");
36502
+ params.push(policy_id);
36503
+ }
36504
+ if (overdue_only) {
36505
+ conditions.push("next_review_date < date('now')");
36506
+ conditions.push("status = 'active'");
36507
+ }
36508
+ const where = conditions.length ? "WHERE " + conditions.join(" AND ") : "";
36509
+ const db = getDb();
36510
+ const total = db.prepare(`SELECT count(*) AS n FROM procedures ${where}`).get(...params).n;
36511
+ params.push(limit, offset);
36512
+ const rows = db.prepare(
36513
+ `SELECT id, procedure_type, policy_id, organisation_name, owner, approver,
36514
+ status, version, effective_date, next_review_date, review_cycle_months,
36515
+ created_at, updated_at
36516
+ FROM procedures ${where}
36517
+ ORDER BY next_review_date ASC, created_at DESC
36518
+ LIMIT ? OFFSET ?`
36519
+ ).all(...params);
36520
+ const today = /* @__PURE__ */ new Date();
36521
+ today.setHours(0, 0, 0, 0);
36522
+ const enriched = rows.map((p) => {
36523
+ const reviewDate = new Date(p.next_review_date);
36524
+ const diffMs = reviewDate.getTime() - today.getTime();
36525
+ const daysUntil = Math.ceil(diffMs / (1e3 * 60 * 60 * 24));
36526
+ return { ...p, days_until_review: daysUntil, overdue: daysUntil < 0 };
36527
+ });
36528
+ return ok9({ total, limit, offset, procedures: enriched });
36529
+ }
36530
+ function handleUpdateProcedure(args2) {
36531
+ const {
36532
+ procedure_id,
36533
+ scope,
36534
+ owner,
36535
+ approver,
36536
+ related_controls,
36537
+ reviewed_by,
36538
+ change_summary
36539
+ } = args2;
36540
+ const db = getDb();
36541
+ const current = db.prepare("SELECT * FROM procedures WHERE id = ?").get(procedure_id);
36542
+ if (!current) throw notFound("procedure", procedure_id);
36543
+ if (current.status === "archived") {
36544
+ throw businessRule("procedure", "Cannot update an archived procedure.");
36545
+ }
36546
+ const ts = now();
36547
+ const newVersion = current.version + 1;
36548
+ db.prepare(`
36549
+ INSERT INTO procedure_versions
36550
+ (id, procedure_id, version, content, change_summary, reviewed_by, archived_at)
36551
+ VALUES (?, ?, ?, ?, ?, ?, ?)
36552
+ `).run(newId(), procedure_id, current.version, current.content, change_summary, reviewed_by, ts);
36553
+ const raw = loadTemplate(current.procedure_type, "procedure-templates");
36554
+ const { template, clauseMappings, controlMappings } = stripFrontmatter(raw);
36555
+ const newScope = scope ?? current.scope;
36556
+ const newOwner = owner ?? current.owner;
36557
+ const rendered = import_mustache2.default.render(template, {
36558
+ procedure_id,
36559
+ organisation_name: current.organisation_name,
36560
+ scope: newScope,
36561
+ owner: newOwner,
36562
+ approver: approver ?? current.approver ?? "TBD",
36563
+ effective_date: current.effective_date,
36564
+ next_review_date: current.next_review_date,
36565
+ version: `${newVersion}.0`,
36566
+ parent_policy_id: current.policy_id ?? "N/A",
36567
+ clause_mappings: clauseMappings.join(", "),
36568
+ control_mappings: controlMappings.join(", ")
36569
+ }, loadPartials());
36570
+ const newRelatedControls = related_controls ? JSON.stringify(related_controls) : current.related_controls;
36571
+ db.prepare(`
36572
+ UPDATE procedures SET
36573
+ scope = COALESCE(?, scope),
36574
+ owner = COALESCE(?, owner),
36575
+ approver = COALESCE(?, approver),
36576
+ related_controls = COALESCE(?, related_controls),
36577
+ reviewed_by = ?,
36578
+ version = ?,
36579
+ content = ?,
36580
+ clause_mappings = ?,
36581
+ control_mappings = ?,
36582
+ updated_at = ?
36583
+ WHERE id = ?
36584
+ `).run(
36585
+ scope ?? null,
36586
+ owner ?? null,
36587
+ approver ?? null,
36588
+ newRelatedControls,
36589
+ reviewed_by,
36590
+ newVersion,
36591
+ rendered,
36592
+ JSON.stringify(clauseMappings),
36593
+ JSON.stringify(controlMappings),
36594
+ ts,
36595
+ procedure_id
36596
+ );
36597
+ return ok9({
36598
+ id: procedure_id,
36599
+ version: newVersion,
36600
+ reviewed_by,
36601
+ change_summary,
36602
+ updated_at: ts
36603
+ });
36604
+ }
36605
+ function handleExportProcedure(args2) {
36606
+ const { procedure_id, format } = args2;
36607
+ const db = getDb();
36608
+ const row = db.prepare("SELECT * FROM procedures WHERE id = ?").get(procedure_id);
36609
+ if (!row) throw notFound("procedure", procedure_id);
36610
+ if (format === "markdown") {
36611
+ const relatedControls = fromJsonArray(row.related_controls);
36612
+ const controlsSection = relatedControls.length > 0 ? `
36613
+
36614
+ ---
36615
+
36616
+ ## Related Controls
36617
+
36618
+ ${relatedControls.map((c) => `- ${c}`).join("\n")}` : "";
36619
+ return ok9({ format: "markdown", content: row.content + controlsSection });
36620
+ }
36621
+ return ok9({
36622
+ format: "json",
36623
+ procedure: {
36624
+ ...row,
36625
+ clause_mappings: fromJsonArray(row.clause_mappings),
36626
+ control_mappings: fromJsonArray(row.control_mappings),
36627
+ related_controls: fromJsonArray(row.related_controls)
36628
+ }
36629
+ });
36630
+ }
36631
+
36632
+ // src/tools/management-review.ts
36633
+ function ok10(data) {
36634
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], isError: false };
36635
+ }
36636
+ var REQUIRED_INPUT_CATEGORIES = [
36637
+ "previous_action_status",
36638
+ "external_internal_issues",
36639
+ "interested_party_needs",
36640
+ "isms_performance",
36641
+ "interested_party_feedback",
36642
+ "risk_assessment_results",
36643
+ "improvement_opportunities"
36644
+ ];
36645
+ function requireReview(id) {
36646
+ const db = getDb();
36647
+ const row = db.prepare("SELECT * FROM management_reviews WHERE id = ?").get(id);
36648
+ if (!row) throw notFound("management_review", id);
36649
+ return row;
36650
+ }
36651
+ function shapeReview(row, inputs, outputs) {
36652
+ return {
36653
+ ...row,
36654
+ reviewers: fromJsonArray(row.reviewers),
36655
+ inputs,
36656
+ outputs
36657
+ };
36658
+ }
36659
+ function handleCreateManagementReview(args2) {
36660
+ const { title, review_date, reviewers, scope_notes } = args2;
36661
+ const db = getDb();
36662
+ const id = newId();
36663
+ const ts = now();
36664
+ db.prepare(`
36665
+ INSERT INTO management_reviews (id, title, review_date, reviewers, scope_notes, status, created_at, updated_at)
36666
+ VALUES (?, ?, ?, ?, ?, 'planned', ?, ?)
36667
+ `).run(id, title, review_date, toJson(reviewers), scope_notes ?? null, ts, ts);
36668
+ return ok10({
36669
+ review_id: id,
36670
+ title,
36671
+ review_date,
36672
+ reviewers,
36673
+ status: "planned",
36674
+ message: `Management review scheduled. Record all 7 required 9.3.2 input categories before completing.`,
36675
+ required_inputs: REQUIRED_INPUT_CATEGORIES,
36676
+ created_at: ts
36677
+ });
36678
+ }
36679
+ function handleRecordReviewInput(args2) {
36680
+ const { review_id, input_category, summary, details, trend } = args2;
36681
+ const review = requireReview(review_id);
36682
+ if (review.status === "completed") {
36683
+ throw businessRule("review_id", "Cannot add inputs to a completed management review.");
36684
+ }
36685
+ const db = getDb();
36686
+ const existing = db.prepare(
36687
+ "SELECT id FROM review_inputs WHERE review_id = ? AND input_category = ?"
36688
+ ).get(review_id, input_category);
36689
+ const ts = now();
36690
+ if (existing) {
36691
+ db.prepare(`
36692
+ UPDATE review_inputs
36693
+ SET summary = ?, details = ?, trend = ?, updated_at = ?
36694
+ WHERE id = ?
36695
+ `).run(summary, details ?? null, trend ?? null, ts, existing.id);
36696
+ } else {
36697
+ const id = newId();
36698
+ db.prepare(`
36699
+ INSERT INTO review_inputs (id, review_id, input_category, summary, details, trend, created_at, updated_at)
36700
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
36701
+ `).run(id, review_id, input_category, summary, details ?? null, trend ?? null, ts, ts);
36702
+ if (review.status === "planned") {
36703
+ db.prepare("UPDATE management_reviews SET status = 'in_progress', updated_at = ? WHERE id = ?").run(ts, review_id);
36704
+ }
36705
+ }
36706
+ const recorded = db.prepare(
36707
+ "SELECT input_category FROM review_inputs WHERE review_id = ?"
36708
+ ).all(review_id);
36709
+ const recordedCategories = recorded.map((r) => r.input_category);
36710
+ const missing = REQUIRED_INPUT_CATEGORIES.filter((c) => !recordedCategories.includes(c));
36711
+ return ok10({
36712
+ review_id,
36713
+ input_category,
36714
+ recorded_at: ts,
36715
+ progress: {
36716
+ recorded_count: recordedCategories.length,
36717
+ required_count: REQUIRED_INPUT_CATEGORIES.length,
36718
+ remaining: missing,
36719
+ ready_to_complete: missing.length === 0
36720
+ }
36721
+ });
36722
+ }
36723
+ function handleRecordReviewOutput(args2) {
36724
+ const { review_id, output_type, decision, owner, due_date } = args2;
36725
+ const review = requireReview(review_id);
36726
+ if (review.status === "completed") {
36727
+ throw businessRule("review_id", "Cannot add outputs to a completed management review.");
36728
+ }
36729
+ const db = getDb();
36730
+ const id = newId();
36731
+ const ts = now();
36732
+ db.prepare(`
36733
+ INSERT INTO review_outputs (id, review_id, output_type, decision, owner, due_date, created_at, updated_at)
36734
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
36735
+ `).run(id, review_id, output_type, decision, owner ?? null, due_date ?? null, ts, ts);
36736
+ return ok10({ output_id: id, review_id, output_type, recorded_at: ts });
36737
+ }
36738
+ function handleCompleteManagementReview(args2) {
36739
+ const { review_id, completed_by } = args2;
36740
+ const review = requireReview(review_id);
36741
+ if (review.status === "completed") {
36742
+ throw businessRule("review_id", "Management review is already completed.");
36743
+ }
36744
+ const db = getDb();
36745
+ const recorded = db.prepare(
36746
+ "SELECT input_category FROM review_inputs WHERE review_id = ?"
36747
+ ).all(review_id);
36748
+ const recordedCategories = new Set(recorded.map((r) => r.input_category));
36749
+ const missing = REQUIRED_INPUT_CATEGORIES.filter((c) => !recordedCategories.has(c));
36750
+ if (missing.length > 0) {
36751
+ throw businessRule(
36752
+ "input_category",
36753
+ `ISO 27001:2022 \xA79.3.2 requires all 7 input categories before a review can be completed. Missing: ${missing.join(", ")}`
36754
+ );
36755
+ }
36756
+ const outputCount = db.prepare(
36757
+ "SELECT COUNT(*) AS c FROM review_outputs WHERE review_id = ?"
36758
+ ).get(review_id).c;
36759
+ if (outputCount === 0) {
36760
+ throw businessRule(
36761
+ "output_type",
36762
+ "ISO 27001:2022 \xA79.3.3 requires at least one output (improvement_decision or isms_change_decision) before completing a review."
36763
+ );
36764
+ }
36765
+ const ts = now();
36766
+ db.prepare(`
36767
+ UPDATE management_reviews
36768
+ SET status = 'completed', completed_at = ?, completed_by = ?, updated_at = ?
36769
+ WHERE id = ?
36770
+ `).run(ts, completed_by, ts, review_id);
36771
+ return ok10({
36772
+ review_id,
36773
+ status: "completed",
36774
+ completed_at: ts,
36775
+ completed_by,
36776
+ inputs_count: recorded.length,
36777
+ outputs_count: outputCount
36778
+ });
36779
+ }
36780
+ function handleGetManagementReview(args2) {
36781
+ const { review_id } = args2;
36782
+ const db = getDb();
36783
+ const review = requireReview(review_id);
36784
+ const inputs = db.prepare(
36785
+ "SELECT * FROM review_inputs WHERE review_id = ? ORDER BY input_category ASC"
36786
+ ).all(review_id);
36787
+ const outputs = db.prepare(
36788
+ "SELECT * FROM review_outputs WHERE review_id = ? ORDER BY created_at ASC"
36789
+ ).all(review_id);
36790
+ const recordedCategories = inputs.map((i) => i.input_category);
36791
+ const missingInputs = REQUIRED_INPUT_CATEGORIES.filter(
36792
+ (c) => !recordedCategories.includes(c)
36793
+ );
36794
+ return ok10({
36795
+ ...shapeReview(review, inputs, outputs),
36796
+ completion_status: {
36797
+ inputs_recorded: recordedCategories.length,
36798
+ inputs_required: REQUIRED_INPUT_CATEGORIES.length,
36799
+ missing_inputs: missingInputs,
36800
+ outputs_recorded: outputs.length,
36801
+ ready_to_complete: missingInputs.length === 0 && outputs.length > 0
36802
+ }
36803
+ });
36804
+ }
36805
+ function handleListManagementReviews(args2) {
36806
+ const { status, limit = 50, offset = 0 } = args2;
36807
+ const db = getDb();
36808
+ const conditions = [];
36809
+ const params = [];
36810
+ if (status) {
36811
+ conditions.push("status = ?");
36812
+ params.push(status);
36813
+ }
36814
+ const where = conditions.length > 0 ? "WHERE " + conditions.join(" AND ") : "";
36815
+ const sql = `
36816
+ SELECT id, title, review_date, reviewers, status, completed_at, completed_by, created_at
36817
+ FROM management_reviews
36818
+ ${where}
36819
+ ORDER BY review_date DESC
36820
+ LIMIT ? OFFSET ?
36821
+ `;
36822
+ params.push(limit, offset);
36823
+ const rows = db.prepare(sql).all(...params);
36824
+ return ok10({
36825
+ total: rows.length,
36826
+ offset,
36827
+ limit,
36828
+ reviews: rows.map((r) => ({
36829
+ ...r,
36830
+ reviewers: fromJsonArray(r.reviewers)
36831
+ }))
36832
+ });
36833
+ }
36834
+
36835
+ // src/tools/improvement-plan.ts
36836
+ function ok11(data) {
36837
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], isError: false };
36838
+ }
36839
+ var STATUS_ORDINAL = {
36840
+ open: 0,
36841
+ in_progress: 1,
36842
+ implemented: 2,
36843
+ closed: 3
36844
+ };
36845
+ function requireOpportunity(id) {
36846
+ const db = getDb();
36847
+ const row = db.prepare("SELECT * FROM improvement_opportunities WHERE id = ?").get(id);
36848
+ if (!row) throw notFound("improvement_opportunity", id);
36849
+ return row;
36850
+ }
36851
+ function computeHealthRating(stats) {
36852
+ const active = stats.open + stats.in_progress;
36853
+ if (stats.overdue > 3) return "at_risk";
36854
+ if (active > 10) return "needs_attention";
36855
+ if (active === 0) return "excellent";
36856
+ if (stats.overdue === 0) return "good";
36857
+ return "fair";
36858
+ }
36859
+ function handleCreateImprovementOpportunity(args2) {
36860
+ const { title, description, source, priority, owner, target_date, review_id } = args2;
36861
+ const db = getDb();
36862
+ const id = newId();
36863
+ const ts = now();
36864
+ db.prepare(`
36865
+ INSERT INTO improvement_opportunities
36866
+ (id, title, description, source, priority, owner, target_date, status, review_id, created_at, updated_at)
36867
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'open', ?, ?, ?)
36868
+ `).run(
36869
+ id,
36870
+ title,
36871
+ description,
36872
+ source,
36873
+ priority ?? "medium",
36874
+ owner ?? null,
36875
+ target_date ?? null,
36876
+ review_id ?? null,
36877
+ ts,
36878
+ ts
36879
+ );
36880
+ return ok11({
36881
+ opportunity_id: id,
36882
+ title,
36883
+ source,
36884
+ priority: priority ?? "medium",
36885
+ status: "open",
36886
+ created_at: ts
36887
+ });
36888
+ }
36889
+ function handleUpdateImprovementOpportunity(args2) {
36890
+ const { opportunity_id, status, owner, target_date, priority, description } = args2;
36891
+ const opp = requireOpportunity(opportunity_id);
36892
+ if (status !== void 0) {
36893
+ const currentOrdinal = STATUS_ORDINAL[opp.status] ?? 0;
36894
+ const newOrdinal = STATUS_ORDINAL[status] ?? 0;
36895
+ if (newOrdinal < currentOrdinal) {
36896
+ throw businessRule(
36897
+ "status",
36898
+ `Status transition '${opp.status}' \u2192 '${status}' is not permitted. Improvement opportunity status can only advance forward: open \u2192 in_progress \u2192 implemented \u2192 closed.`
36899
+ );
36900
+ }
36901
+ }
36902
+ const db = getDb();
36903
+ const ts = now();
36904
+ const updates = ["updated_at = ?"];
36905
+ const params = [ts];
36906
+ if (status !== void 0) {
36907
+ updates.push("status = ?");
36908
+ params.push(status);
36909
+ }
36910
+ if (owner !== void 0) {
36911
+ updates.push("owner = ?");
36912
+ params.push(owner);
36913
+ }
36914
+ if (target_date !== void 0) {
36915
+ updates.push("target_date = ?");
36916
+ params.push(target_date);
36917
+ }
36918
+ if (priority !== void 0) {
36919
+ updates.push("priority = ?");
36920
+ params.push(priority);
36921
+ }
36922
+ if (description !== void 0) {
36923
+ updates.push("description = ?");
36924
+ params.push(description);
36925
+ }
36926
+ params.push(opportunity_id);
36927
+ db.prepare(`UPDATE improvement_opportunities SET ${updates.join(", ")} WHERE id = ?`).run(...params);
36928
+ const updated = db.prepare("SELECT * FROM improvement_opportunities WHERE id = ?").get(opportunity_id);
36929
+ return ok11(updated);
36930
+ }
36931
+ function handleGetImprovementOpportunity(args2) {
36932
+ const { opportunity_id } = args2;
36933
+ const opp = requireOpportunity(opportunity_id);
36934
+ return ok11(opp);
36935
+ }
36936
+ function handleListImprovementOpportunities(args2) {
36937
+ const { status, source, priority, review_id, limit = 50, offset = 0 } = args2;
36938
+ const db = getDb();
36939
+ const conditions = [];
36940
+ const params = [];
36118
36941
  if (status) {
36119
36942
  conditions.push("status = ?");
36120
36943
  params.push(status);
36121
36944
  }
36122
- if (policy_id) {
36123
- conditions.push("policy_id = ?");
36124
- params.push(policy_id);
36945
+ if (source) {
36946
+ conditions.push("source = ?");
36947
+ params.push(source);
36125
36948
  }
36126
- if (overdue_only) {
36127
- conditions.push("next_review_date < date('now')");
36128
- conditions.push("status = 'active'");
36949
+ if (priority) {
36950
+ conditions.push("priority = ?");
36951
+ params.push(priority);
36129
36952
  }
36130
- const where = conditions.length ? "WHERE " + conditions.join(" AND ") : "";
36131
- const db = getDb();
36132
- const total = db.prepare(`SELECT count(*) AS n FROM procedures ${where}`).get(...params).n;
36133
- params.push(limit, offset);
36134
- const rows = db.prepare(
36135
- `SELECT id, procedure_type, policy_id, organisation_name, owner, approver,
36136
- status, version, effective_date, next_review_date, review_cycle_months,
36137
- created_at, updated_at
36138
- FROM procedures ${where}
36139
- ORDER BY next_review_date ASC, created_at DESC
36140
- LIMIT ? OFFSET ?`
36141
- ).all(...params);
36142
- const today = /* @__PURE__ */ new Date();
36143
- today.setHours(0, 0, 0, 0);
36144
- const enriched = rows.map((p) => {
36145
- const reviewDate = new Date(p.next_review_date);
36146
- const diffMs = reviewDate.getTime() - today.getTime();
36147
- const daysUntil = Math.ceil(diffMs / (1e3 * 60 * 60 * 24));
36148
- return { ...p, days_until_review: daysUntil, overdue: daysUntil < 0 };
36953
+ if (review_id) {
36954
+ conditions.push("review_id = ?");
36955
+ params.push(review_id);
36956
+ }
36957
+ const where = conditions.length > 0 ? "WHERE " + conditions.join(" AND ") : "";
36958
+ const rows = db.prepare(`
36959
+ SELECT * FROM improvement_opportunities
36960
+ ${where}
36961
+ ORDER BY
36962
+ CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END ASC,
36963
+ target_date ASC NULLS LAST,
36964
+ created_at DESC
36965
+ LIMIT ? OFFSET ?
36966
+ `).all(...params, limit, offset);
36967
+ const stats = db.prepare(`
36968
+ SELECT
36969
+ SUM(CASE WHEN status = 'open' THEN 1 ELSE 0 END) AS open,
36970
+ SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) AS in_progress,
36971
+ SUM(CASE WHEN status = 'implemented' THEN 1 ELSE 0 END) AS implemented,
36972
+ SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END) AS closed,
36973
+ SUM(
36974
+ CASE WHEN status IN ('open','in_progress')
36975
+ AND target_date IS NOT NULL
36976
+ AND target_date < date('now') THEN 1 ELSE 0 END
36977
+ ) AS overdue
36978
+ FROM improvement_opportunities
36979
+ `).get();
36980
+ return ok11({
36981
+ total: rows.length,
36982
+ offset,
36983
+ limit,
36984
+ opportunities: rows,
36985
+ health: {
36986
+ open: stats.open ?? 0,
36987
+ in_progress: stats.in_progress ?? 0,
36988
+ implemented: stats.implemented ?? 0,
36989
+ closed: stats.closed ?? 0,
36990
+ overdue: stats.overdue ?? 0,
36991
+ rating: computeHealthRating({
36992
+ open: stats.open ?? 0,
36993
+ in_progress: stats.in_progress ?? 0,
36994
+ implemented: stats.implemented ?? 0,
36995
+ closed: stats.closed ?? 0,
36996
+ overdue: stats.overdue ?? 0
36997
+ })
36998
+ }
36149
36999
  });
36150
- return ok9({ total, limit, offset, procedures: enriched });
36151
37000
  }
36152
- function handleUpdateProcedure(args2) {
37001
+
37002
+ // src/tools/evidence-templates.ts
37003
+ var import_mustache3 = __toESM(require("mustache"));
37004
+ function ok12(data) {
37005
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }], isError: false };
37006
+ }
37007
+ var EVIDENCE_TYPE_MAP = {
37008
+ access_review_attestation: "audit_report",
37009
+ training_acknowledgement: "training_record",
37010
+ supplier_security_questionnaire: "report",
37011
+ incident_post_mortem: "report",
37012
+ bcp_test_report: "report",
37013
+ risk_treatment_sign_off: "report"
37014
+ };
37015
+ var ORG_PROFILE_ID2 = "00000000-0000-4000-8000-000000000001";
37016
+ function loadOrgRaci(db) {
37017
+ const row = db.prepare("SELECT raci_roles, isms_scope_statement FROM organization_profile WHERE id = ?").get(ORG_PROFILE_ID2);
37018
+ if (!row) {
37019
+ return { ciso: "", dpo: "", data_owner: "", isms_manager: "", internal_auditor: "" };
37020
+ }
37021
+ try {
37022
+ const raci = JSON.parse(row.raci_roles);
37023
+ return {
37024
+ ciso: raci.ciso ?? "",
37025
+ dpo: raci.dpo ?? "",
37026
+ data_owner: raci.data_owner ?? "",
37027
+ isms_manager: raci.isms_manager ?? "",
37028
+ internal_auditor: raci.internal_auditor ?? ""
37029
+ };
37030
+ } catch {
37031
+ return { ciso: "", dpo: "", data_owner: "", isms_manager: "", internal_auditor: "" };
37032
+ }
37033
+ }
37034
+ function loadIsmsScope(db) {
37035
+ const row = db.prepare("SELECT isms_scope_statement FROM organization_profile WHERE id = ?").get(ORG_PROFILE_ID2);
37036
+ return row?.isms_scope_statement ?? "";
37037
+ }
37038
+ function handleGenerateEvidenceDocument(args2) {
36153
37039
  const {
36154
- procedure_id,
36155
- scope,
36156
- owner,
36157
- approver,
36158
- related_controls,
36159
- reviewed_by,
36160
- change_summary
37040
+ template_type,
37041
+ title,
37042
+ generated_by,
37043
+ organisation_name: callerOrgName,
37044
+ control_id,
37045
+ vars = {}
36161
37046
  } = args2;
36162
37047
  const db = getDb();
36163
- const current = db.prepare("SELECT * FROM procedures WHERE id = ?").get(procedure_id);
36164
- if (!current) throw notFound("procedure", procedure_id);
36165
- if (current.status === "archived") {
36166
- throw businessRule("procedure", "Cannot update an archived procedure.");
36167
- }
36168
- const ts = now();
36169
- const newVersion = current.version + 1;
36170
- db.prepare(`
36171
- INSERT INTO procedure_versions
36172
- (id, procedure_id, version, content, change_summary, reviewed_by, archived_at)
36173
- VALUES (?, ?, ?, ?, ?, ?, ?)
36174
- `).run(newId(), procedure_id, current.version, current.content, change_summary, reviewed_by, ts);
36175
- const raw = loadTemplate(current.procedure_type, "procedure-templates");
37048
+ const orgDefaults = loadOrgProfileDefaults(db);
37049
+ const raci = loadOrgRaci(db);
37050
+ const ismsScope = loadIsmsScope(db);
37051
+ const organisation_name = callerOrgName ?? orgDefaults?.organisation_name ?? "";
37052
+ const raw = loadTemplate(template_type, "evidence-templates");
36176
37053
  const { template, clauseMappings, controlMappings } = stripFrontmatter(raw);
36177
- const newScope = scope ?? current.scope;
36178
- const newOwner = owner ?? current.owner;
36179
- const rendered = import_mustache2.default.render(template, {
36180
- procedure_id,
36181
- organisation_name: current.organisation_name,
36182
- scope: newScope,
36183
- owner: newOwner,
36184
- approver: approver ?? current.approver ?? "TBD",
36185
- effective_date: current.effective_date,
36186
- next_review_date: current.next_review_date,
36187
- version: `${newVersion}.0`,
36188
- parent_policy_id: current.policy_id ?? "N/A"
36189
- });
36190
- const newRelatedControls = related_controls ? JSON.stringify(related_controls) : current.related_controls;
37054
+ const today = now().slice(0, 10);
37055
+ const view = {
37056
+ // Org-profile auto-injections
37057
+ organisation_name,
37058
+ isms_scope_statement: ismsScope,
37059
+ ciso: raci.ciso,
37060
+ dpo: raci.dpo,
37061
+ data_owner: raci.data_owner,
37062
+ isms_manager: raci.isms_manager,
37063
+ internal_auditor: raci.internal_auditor,
37064
+ // Document-level fields
37065
+ title,
37066
+ generated_by,
37067
+ generated_date: today,
37068
+ // Caller-supplied template-specific variables (override org defaults if clashing)
37069
+ ...vars
37070
+ };
37071
+ const viewWithMappings = {
37072
+ ...view,
37073
+ clause_mappings: clauseMappings.join(", "),
37074
+ control_mappings: controlMappings.join(", ")
37075
+ };
37076
+ const content = import_mustache3.default.render(template, viewWithMappings, loadPartials());
37077
+ const ts = now();
37078
+ const docId = newId();
37079
+ const evidenceType = EVIDENCE_TYPE_MAP[template_type] ?? "report";
37080
+ const evidenceId = newId();
37081
+ const evidenceControlId = control_id ?? "general";
36191
37082
  db.prepare(`
36192
- UPDATE procedures SET
36193
- scope = COALESCE(?, scope),
36194
- owner = COALESCE(?, owner),
36195
- approver = COALESCE(?, approver),
36196
- related_controls = COALESCE(?, related_controls),
36197
- reviewed_by = ?,
36198
- version = ?,
36199
- content = ?,
36200
- clause_mappings = ?,
36201
- control_mappings = ?,
36202
- updated_at = ?
36203
- WHERE id = ?
37083
+ INSERT INTO evidence
37084
+ (id, control_id, type, description, source_url, collected_by, collected_date,
37085
+ expiry_date, jira_key, jira_url, github_issue_url, github_issue_number,
37086
+ created_at, updated_at)
37087
+ VALUES (?, ?, ?, ?, NULL, ?, ?, NULL, NULL, NULL, NULL, NULL, ?, ?)
36204
37088
  `).run(
36205
- scope ?? null,
36206
- owner ?? null,
36207
- approver ?? null,
36208
- newRelatedControls,
36209
- reviewed_by,
36210
- newVersion,
36211
- rendered,
36212
- JSON.stringify(clauseMappings),
36213
- JSON.stringify(controlMappings),
37089
+ evidenceId,
37090
+ evidenceControlId,
37091
+ evidenceType,
37092
+ title,
37093
+ generated_by,
37094
+ today,
36214
37095
  ts,
36215
- procedure_id
37096
+ ts
36216
37097
  );
36217
- return ok9({
36218
- id: procedure_id,
36219
- version: newVersion,
36220
- reviewed_by,
36221
- change_summary,
36222
- updated_at: ts
37098
+ db.prepare(`
37099
+ INSERT INTO generated_evidence
37100
+ (id, template_type, title, content, organisation_name, generated_by,
37101
+ clause_mappings, control_mappings, template_vars, evidence_id, created_at)
37102
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
37103
+ `).run(
37104
+ docId,
37105
+ template_type,
37106
+ title,
37107
+ content,
37108
+ organisation_name,
37109
+ generated_by,
37110
+ toJson(clauseMappings),
37111
+ toJson(controlMappings),
37112
+ JSON.stringify(vars),
37113
+ evidenceId,
37114
+ ts
37115
+ );
37116
+ return ok12({
37117
+ document_id: docId,
37118
+ evidence_id: evidenceId,
37119
+ template_type,
37120
+ title,
37121
+ organisation_name,
37122
+ clause_mappings: clauseMappings,
37123
+ control_mappings: controlMappings,
37124
+ generated_by,
37125
+ generated_at: ts,
37126
+ content
36223
37127
  });
36224
37128
  }
36225
- function handleExportProcedure(args2) {
36226
- const { procedure_id, format } = args2;
37129
+ function handleGetEvidenceDocument(args2) {
37130
+ const { document_id } = args2;
36227
37131
  const db = getDb();
36228
- const row = db.prepare("SELECT * FROM procedures WHERE id = ?").get(procedure_id);
36229
- if (!row) throw notFound("procedure", procedure_id);
36230
- if (format === "markdown") {
36231
- const relatedControls = fromJsonArray(row.related_controls);
36232
- const controlsSection = relatedControls.length > 0 ? `
36233
-
36234
- ---
36235
-
36236
- ## Related Controls
36237
-
36238
- ${relatedControls.map((c) => `- ${c}`).join("\n")}` : "";
36239
- return ok9({ format: "markdown", content: row.content + controlsSection });
37132
+ const row = db.prepare("SELECT * FROM generated_evidence WHERE id = ?").get(document_id);
37133
+ if (!row) throw notFound("evidence_document", document_id);
37134
+ return ok12({
37135
+ ...row,
37136
+ clause_mappings: fromJsonArray(row.clause_mappings),
37137
+ control_mappings: fromJsonArray(row.control_mappings),
37138
+ template_vars: JSON.parse(row.template_vars)
37139
+ });
37140
+ }
37141
+ function handleListEvidenceDocuments(args2) {
37142
+ const { template_type, generated_by, control_id, limit = 50, offset = 0 } = args2;
37143
+ const db = getDb();
37144
+ const conditions = [];
37145
+ const params = [];
37146
+ if (template_type) {
37147
+ conditions.push("g.template_type = ?");
37148
+ params.push(template_type);
36240
37149
  }
36241
- return ok9({
36242
- format: "json",
36243
- procedure: {
36244
- ...row,
36245
- clause_mappings: fromJsonArray(row.clause_mappings),
36246
- control_mappings: fromJsonArray(row.control_mappings),
36247
- related_controls: fromJsonArray(row.related_controls)
36248
- }
37150
+ if (generated_by) {
37151
+ conditions.push("g.generated_by = ?");
37152
+ params.push(generated_by);
37153
+ }
37154
+ if (control_id) {
37155
+ conditions.push("e.control_id = ?");
37156
+ params.push(control_id);
37157
+ }
37158
+ const join2 = control_id ? "LEFT JOIN evidence e ON e.id = g.evidence_id" : "";
37159
+ const where = conditions.length > 0 ? "WHERE " + conditions.join(" AND ") : "";
37160
+ const rows = db.prepare(`
37161
+ SELECT g.id, g.template_type, g.title, g.organisation_name,
37162
+ g.generated_by, g.evidence_id, g.created_at,
37163
+ g.clause_mappings, g.control_mappings
37164
+ FROM generated_evidence g
37165
+ ${join2}
37166
+ ${where}
37167
+ ORDER BY g.created_at DESC
37168
+ LIMIT ? OFFSET ?
37169
+ `).all(...params, limit, offset);
37170
+ return ok12({
37171
+ total: rows.length,
37172
+ offset,
37173
+ limit,
37174
+ documents: rows.map((r) => ({
37175
+ ...r,
37176
+ clause_mappings: fromJsonArray(r.clause_mappings),
37177
+ control_mappings: fromJsonArray(r.control_mappings)
37178
+ }))
36249
37179
  });
36250
37180
  }
36251
37181
 
36252
37182
  // src/tools/index.ts
36253
37183
  function extractShape(schema) {
36254
- if (schema instanceof import_zod2.z.ZodEffects) {
36255
- return schema.innerType().shape;
37184
+ let s = schema;
37185
+ while (s instanceof import_zod2.z.ZodEffects) {
37186
+ s = s.innerType();
36256
37187
  }
36257
- return schema.shape;
37188
+ return s.shape;
36258
37189
  }
36259
- function ok10(data) {
37190
+ function ok13(data) {
36260
37191
  return {
36261
37192
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
36262
37193
  isError: false
@@ -36323,7 +37254,23 @@ var TOOL_DESCRIPTIONS = {
36323
37254
  get_procedure: "Retrieve a procedure record by ID, optionally including version history.",
36324
37255
  update_procedure: "Archive the current procedure version and re-render with updated fields, incrementing the version number. Requires admin role.",
36325
37256
  list_procedures: "List procedures with optional filters: procedure_type, status, policy_id, overdue_only, and pagination.",
36326
- export_procedure: "Export a procedure as a markdown document (with related controls appended) or as structured JSON."
37257
+ export_procedure: "Export a procedure as a markdown document (with related controls appended) or as structured JSON.",
37258
+ // Group 12 — Management Review, Clause 9.3 (reads: viewer+, writes: admin)
37259
+ create_management_review: "Schedule a new management review (ISO 27001:2022 Clause 9.3) with title, date, and reviewers list.",
37260
+ record_review_input: "Record one of the 7 mandatory Clause 9.3.2 input categories for a management review. Upserts on re-submission; advances status to in_progress on first input.",
37261
+ record_review_output: "Record a Clause 9.3.3 output decision (improvement_decision or isms_change_decision) for a management review.",
37262
+ complete_management_review: "Mark a management review as completed. Enforces ISO 27001:2022 \xA79.3.2: all 7 input categories must be recorded, and at least one output must be present.",
37263
+ get_management_review: "Retrieve a management review record with all inputs, outputs, and a completion-progress summary.",
37264
+ list_management_reviews: "List management reviews with optional status filter and pagination.",
37265
+ // Group 13 — Improvement Plan, Clause 10.1 (reads: viewer+, writes: analyst+)
37266
+ create_improvement_opportunity: "Register a proactive improvement opportunity (ISO 27001:2022 Clause 10.1) with source, priority, owner, and optional target date. Not linked to a nonconformity.",
37267
+ update_improvement_opportunity: "Advance an improvement opportunity's status (forward-only: open \u2192 in_progress \u2192 implemented \u2192 closed) or update owner, target date, priority, or description.",
37268
+ get_improvement_opportunity: "Retrieve a single improvement opportunity by ID.",
37269
+ list_improvement_opportunities: "List improvement opportunities with optional filters (status, source, priority, review_id) and a backlog health rating (excellent/good/fair/needs_attention/at_risk).",
37270
+ // Group 14 — Evidence Templates (reads: viewer+, generate: analyst+)
37271
+ generate_evidence_document: "Render one of 6 Mustache evidence templates (access_review_attestation, training_acknowledgement, supplier_security_questionnaire, incident_post_mortem, bcp_test_report, risk_treatment_sign_off) with org-profile auto-injection. Returns rendered Markdown and simultaneously registers an evidence record.",
37272
+ get_evidence_document: "Retrieve a previously generated evidence document by ID, including its rendered Markdown content and template variables used.",
37273
+ list_evidence_documents: "List generated evidence documents with optional filters: template_type, generated_by, control_id, and pagination."
36327
37274
  };
36328
37275
  var TOOL_HANDLERS = {
36329
37276
  // ── Group 1: Control Registry ────────────────────────────
@@ -36414,22 +37361,22 @@ var TOOL_HANDLERS = {
36414
37361
  params.push(key_hash);
36415
37362
  }
36416
37363
  const where = conditions.length > 0 ? "WHERE " + conditions.join(" AND ") : "";
36417
- const sql = `SELECT id, timestamp, tool, role, outcome, error_message, duration_ms, row_hash
37364
+ const sql = `SELECT id, timestamp, tool, role, outcome, error_message, duration_ms, prev_hash, row_hash
36418
37365
  FROM audit_log ${where}
36419
37366
  ORDER BY timestamp DESC
36420
37367
  LIMIT ? OFFSET ?`;
36421
37368
  params.push(limit, offset);
36422
37369
  const rows = db.prepare(sql).all(...params);
36423
- return ok10({ total: rows.length, offset, limit, entries: rows });
37370
+ return ok13({ total: rows.length, offset, limit, entries: rows });
36424
37371
  },
36425
37372
  list_api_keys: (_args) => {
36426
37373
  const keys = listKeys();
36427
- return ok10({ count: keys.length, keys });
37374
+ return ok13({ count: keys.length, keys });
36428
37375
  },
36429
37376
  revoke_api_key: (args2) => {
36430
37377
  const { label } = args2;
36431
37378
  revokeKey(label);
36432
- return ok10({ revoked: true, label });
37379
+ return ok13({ revoked: true, label });
36433
37380
  },
36434
37381
  // ── Group 10: Organization Profile ───────────────────────────
36435
37382
  set_organization_profile: handleSetOrganizationProfile,
@@ -36439,7 +37386,23 @@ var TOOL_HANDLERS = {
36439
37386
  get_procedure: handleGetProcedure,
36440
37387
  update_procedure: handleUpdateProcedure,
36441
37388
  list_procedures: handleListProcedures,
36442
- export_procedure: handleExportProcedure
37389
+ export_procedure: handleExportProcedure,
37390
+ // ── Group 12: Management Review (Clause 9.3) ─────────────────
37391
+ create_management_review: handleCreateManagementReview,
37392
+ record_review_input: handleRecordReviewInput,
37393
+ record_review_output: handleRecordReviewOutput,
37394
+ complete_management_review: handleCompleteManagementReview,
37395
+ get_management_review: handleGetManagementReview,
37396
+ list_management_reviews: handleListManagementReviews,
37397
+ // ── Group 13: Improvement Plan (Clause 10.1) ──────────────────
37398
+ create_improvement_opportunity: handleCreateImprovementOpportunity,
37399
+ update_improvement_opportunity: handleUpdateImprovementOpportunity,
37400
+ get_improvement_opportunity: handleGetImprovementOpportunity,
37401
+ list_improvement_opportunities: handleListImprovementOpportunities,
37402
+ // ── Group 14: Evidence Templates ──────────────────────────────
37403
+ generate_evidence_document: handleGenerateEvidenceDocument,
37404
+ get_evidence_document: handleGetEvidenceDocument,
37405
+ list_evidence_documents: handleListEvidenceDocuments
36443
37406
  };
36444
37407
  function registerAllTools(server) {
36445
37408
  for (const [toolName, description] of Object.entries(TOOL_DESCRIPTIONS)) {
@@ -37198,6 +38161,188 @@ function registerOrgProfileResource(server) {
37198
38161
  );
37199
38162
  }
37200
38163
 
38164
+ // src/resources/management-review.ts
38165
+ var import_mcp6 = require("@modelcontextprotocol/sdk/server/mcp.js");
38166
+ function registerManagementReviewResources(server) {
38167
+ server.resource(
38168
+ "iso27001-management-review",
38169
+ new import_mcp6.ResourceTemplate("iso27001://management-review/{review_id}", {
38170
+ list: () => {
38171
+ const rows = getDb().prepare(
38172
+ `SELECT id, title, review_date, status, completed_at
38173
+ FROM management_reviews
38174
+ ORDER BY review_date DESC`
38175
+ ).all();
38176
+ return {
38177
+ resources: rows.map((r) => ({
38178
+ uri: `iso27001://management-review/${r.id}`,
38179
+ name: r.title,
38180
+ description: `Management review on ${r.review_date}, status: ${r.status}` + (r.completed_at ? `, completed: ${r.completed_at}` : ""),
38181
+ mimeType: "application/json"
38182
+ }))
38183
+ };
38184
+ }
38185
+ }),
38186
+ {
38187
+ description: "ISO 27001 management review record (Clause 9.3) with all inputs (9.3.2) and output decisions (9.3.3) nested. Fields: id, title, review_date, reviewers, scope_notes, status, completed_at, completed_by, inputs[], outputs[].",
38188
+ mimeType: "application/json"
38189
+ },
38190
+ (uri, variables, extra) => {
38191
+ assertResourceAuth(extra);
38192
+ const { review_id } = variables;
38193
+ const db = getDb();
38194
+ const review = db.prepare("SELECT * FROM management_reviews WHERE id = ?").get(review_id);
38195
+ if (!review) {
38196
+ throw new Error(
38197
+ `Management review not found: '${review_id}'. Use list_management_reviews to find valid IDs.`
38198
+ );
38199
+ }
38200
+ const inputs = db.prepare(
38201
+ "SELECT * FROM review_inputs WHERE review_id = ? ORDER BY input_category ASC"
38202
+ ).all(review_id);
38203
+ const outputs = db.prepare(
38204
+ "SELECT * FROM review_outputs WHERE review_id = ? ORDER BY created_at ASC"
38205
+ ).all(review_id);
38206
+ const payload = {
38207
+ ...review,
38208
+ reviewers: fromJsonArray(review.reviewers),
38209
+ inputs,
38210
+ outputs
38211
+ };
38212
+ return {
38213
+ contents: [{
38214
+ uri: uri.toString(),
38215
+ mimeType: "application/json",
38216
+ text: JSON.stringify(payload, null, 2)
38217
+ }]
38218
+ };
38219
+ }
38220
+ );
38221
+ server.resource(
38222
+ "iso27001-improvement-plan",
38223
+ new import_mcp6.ResourceTemplate("iso27001://improvement-plan", { list: void 0 }),
38224
+ {
38225
+ description: "ISO 27001 improvement opportunity backlog (Clause 10.1). Returns all opportunities sorted by priority and target date, with a health rating (excellent/good/fair/needs_attention/at_risk) and counts by status.",
38226
+ mimeType: "application/json"
38227
+ },
38228
+ (uri, _variables, extra) => {
38229
+ assertResourceAuth(extra);
38230
+ const db = getDb();
38231
+ const opportunities = db.prepare(`
38232
+ SELECT * FROM improvement_opportunities
38233
+ ORDER BY
38234
+ CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 ELSE 3 END ASC,
38235
+ target_date ASC NULLS LAST,
38236
+ created_at DESC
38237
+ `).all();
38238
+ const stats = db.prepare(`
38239
+ SELECT
38240
+ SUM(CASE WHEN status = 'open' THEN 1 ELSE 0 END) AS open,
38241
+ SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END) AS in_progress,
38242
+ SUM(CASE WHEN status = 'implemented' THEN 1 ELSE 0 END) AS implemented,
38243
+ SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END) AS closed,
38244
+ SUM(
38245
+ CASE WHEN status IN ('open','in_progress')
38246
+ AND target_date IS NOT NULL
38247
+ AND target_date < date('now') THEN 1 ELSE 0 END
38248
+ ) AS overdue
38249
+ FROM improvement_opportunities
38250
+ `).get();
38251
+ const open = stats.open ?? 0;
38252
+ const in_progress = stats.in_progress ?? 0;
38253
+ const implemented = stats.implemented ?? 0;
38254
+ const closed = stats.closed ?? 0;
38255
+ const overdue = stats.overdue ?? 0;
38256
+ let rating;
38257
+ const active = open + in_progress;
38258
+ if (overdue > 3) rating = "at_risk";
38259
+ else if (active > 10) rating = "needs_attention";
38260
+ else if (active === 0) rating = "excellent";
38261
+ else if (overdue === 0) rating = "good";
38262
+ else rating = "fair";
38263
+ const payload = {
38264
+ total: opportunities.length,
38265
+ health: { open, in_progress, implemented, closed, overdue, rating },
38266
+ opportunities
38267
+ };
38268
+ return {
38269
+ contents: [{
38270
+ uri: uri.toString(),
38271
+ mimeType: "application/json",
38272
+ text: JSON.stringify(payload, null, 2)
38273
+ }]
38274
+ };
38275
+ }
38276
+ );
38277
+ }
38278
+
38279
+ // src/resources/evidence-templates.ts
38280
+ var import_mcp7 = require("@modelcontextprotocol/sdk/server/mcp.js");
38281
+ var TEMPLATE_LABELS = {
38282
+ access_review_attestation: "Access Review Attestation",
38283
+ training_acknowledgement: "Training Acknowledgement",
38284
+ supplier_security_questionnaire: "Supplier Security Questionnaire",
38285
+ incident_post_mortem: "Incident Post-Mortem",
38286
+ bcp_test_report: "BCP Test Report",
38287
+ risk_treatment_sign_off: "Risk Treatment Sign-Off"
38288
+ };
38289
+ function registerEvidenceDocumentResources(server) {
38290
+ server.resource(
38291
+ "iso27001-evidence-document",
38292
+ new import_mcp7.ResourceTemplate("iso27001://evidence-document/{document_id}", {
38293
+ list: () => {
38294
+ const rows = getDb().prepare(
38295
+ `SELECT id, template_type, title, generated_by, evidence_id, created_at
38296
+ FROM generated_evidence
38297
+ ORDER BY created_at DESC`
38298
+ ).all();
38299
+ return {
38300
+ resources: rows.map((r) => ({
38301
+ uri: `iso27001://evidence-document/${r.id}`,
38302
+ name: r.title,
38303
+ description: `${TEMPLATE_LABELS[r.template_type] ?? r.template_type} \u2014 generated by ${r.generated_by} on ${r.created_at.slice(0, 10)}`,
38304
+ mimeType: "application/json"
38305
+ }))
38306
+ };
38307
+ }
38308
+ }),
38309
+ {
38310
+ description: "Generated ISO 27001 evidence document with full rendered Markdown content. Template types: access_review_attestation, training_acknowledgement, supplier_security_questionnaire, incident_post_mortem, bcp_test_report, risk_treatment_sign_off. Fields: id, template_type, title, content (Markdown), organisation_name, generated_by, clause_mappings, control_mappings, template_vars, evidence_id, created_at.",
38311
+ mimeType: "application/json"
38312
+ },
38313
+ (uri, variables, extra) => {
38314
+ assertResourceAuth(extra);
38315
+ const { document_id } = variables;
38316
+ const db = getDb();
38317
+ const row = db.prepare("SELECT * FROM generated_evidence WHERE id = ?").get(document_id);
38318
+ if (!row) {
38319
+ throw new Error(
38320
+ `Evidence document not found: '${document_id}'. Use list_evidence_documents to find valid IDs.`
38321
+ );
38322
+ }
38323
+ const payload = {
38324
+ ...row,
38325
+ clause_mappings: fromJsonArray(row.clause_mappings),
38326
+ control_mappings: fromJsonArray(row.control_mappings),
38327
+ template_vars: (() => {
38328
+ try {
38329
+ return JSON.parse(row.template_vars);
38330
+ } catch {
38331
+ return {};
38332
+ }
38333
+ })()
38334
+ };
38335
+ return {
38336
+ contents: [{
38337
+ uri: uri.toString(),
38338
+ mimeType: "application/json",
38339
+ text: JSON.stringify(payload, null, 2)
38340
+ }]
38341
+ };
38342
+ }
38343
+ );
38344
+ }
38345
+
37201
38346
  // src/resources/index.ts
37202
38347
  function registerAllResources(server) {
37203
38348
  registerControlResources(server);
@@ -37206,11 +38351,13 @@ function registerAllResources(server) {
37206
38351
  registerProcedureResources(server);
37207
38352
  registerRiskResources(server);
37208
38353
  registerAssessmentResources(server);
38354
+ registerManagementReviewResources(server);
38355
+ registerEvidenceDocumentResources(server);
37209
38356
  }
37210
38357
 
37211
38358
  // src/server.ts
37212
38359
  function createServer() {
37213
- const server = new import_mcp6.McpServer({
38360
+ const server = new import_mcp8.McpServer({
37214
38361
  name: "iso27001-mcp",
37215
38362
  version: process.env["npm_package_version"] ?? "2.0.0"
37216
38363
  });