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.
- package/dist/index.js +1955 -808
- package/dist/seed/evidence-templates/access_review_attestation.md +63 -0
- package/dist/seed/evidence-templates/bcp_test_report.md +139 -0
- package/dist/seed/evidence-templates/incident_post_mortem.md +142 -0
- package/dist/seed/evidence-templates/risk_treatment_sign_off.md +112 -0
- package/dist/seed/evidence-templates/supplier_security_questionnaire.md +146 -0
- package/dist/seed/evidence-templates/training_acknowledgement.md +75 -0
- package/dist/seed/partials/approver_signature.md +31 -0
- package/dist/seed/partials/org_header.md +14 -0
- package/dist/seed/partials/revision_block.md +7 -0
- package/dist/seed/policy-templates/acceptable_use.md +15 -11
- package/dist/seed/policy-templates/access_control.md +15 -11
- package/dist/seed/policy-templates/asset_management.md +16 -11
- package/dist/seed/policy-templates/business_continuity.md +14 -11
- package/dist/seed/policy-templates/cryptography.md +13 -11
- package/dist/seed/policy-templates/data_classification.md +12 -11
- package/dist/seed/policy-templates/incident_response.md +15 -11
- package/dist/seed/policy-templates/information_security.md +15 -11
- package/dist/seed/policy-templates/physical_security.md +16 -11
- package/dist/seed/policy-templates/risk_management.md +14 -11
- package/dist/seed/policy-templates/secure_development.md +14 -11
- package/dist/seed/policy-templates/supplier_security.md +15 -11
- package/dist/seed/procedure-templates/access_provisioning.md +17 -12
- package/dist/seed/procedure-templates/asset_onboarding_offboarding.md +16 -12
- package/dist/seed/procedure-templates/audit_log_review.md +18 -12
- package/dist/seed/procedure-templates/backup_restore.md +15 -12
- package/dist/seed/procedure-templates/bcp_testing.md +15 -12
- package/dist/seed/procedure-templates/change_management.md +16 -12
- package/dist/seed/procedure-templates/cryptographic_key_management.md +18 -12
- package/dist/seed/procedure-templates/data_classification_handling.md +17 -12
- package/dist/seed/procedure-templates/incident_handling.md +14 -12
- package/dist/seed/procedure-templates/secure_development_workflow.md +18 -12
- package/dist/seed/procedure-templates/supplier_onboarding.md +15 -12
- package/dist/seed/procedure-templates/vulnerability_management.md +18 -12
- 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.
|
|
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
|
-
|
|
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/
|
|
26296
|
-
var
|
|
26297
|
-
|
|
26298
|
-
|
|
26299
|
-
|
|
26300
|
-
|
|
26301
|
-
|
|
26302
|
-
|
|
26303
|
-
|
|
26304
|
-
|
|
26305
|
-
|
|
26306
|
-
|
|
26307
|
-
|
|
26308
|
-
|
|
26309
|
-
|
|
26310
|
-
|
|
26311
|
-
|
|
26312
|
-
|
|
26313
|
-
|
|
26314
|
-
|
|
26315
|
-
|
|
26316
|
-
|
|
26317
|
-
|
|
26318
|
-
|
|
26319
|
-
|
|
26320
|
-
|
|
26321
|
-
|
|
26322
|
-
|
|
26323
|
-
|
|
26324
|
-
|
|
26325
|
-
|
|
26326
|
-
|
|
26327
|
-
|
|
26328
|
-
|
|
26329
|
-
|
|
26330
|
-
|
|
26331
|
-
|
|
26332
|
-
|
|
26333
|
-
|
|
26334
|
-
|
|
26335
|
-
|
|
26336
|
-
|
|
26337
|
-
|
|
26338
|
-
|
|
26339
|
-
|
|
26340
|
-
|
|
26341
|
-
|
|
26342
|
-
|
|
26343
|
-
|
|
26344
|
-
|
|
26345
|
-
|
|
26346
|
-
|
|
26347
|
-
|
|
26348
|
-
|
|
26349
|
-
|
|
26350
|
-
|
|
26351
|
-
|
|
26352
|
-
|
|
26353
|
-
|
|
26354
|
-
|
|
26355
|
-
|
|
26356
|
-
|
|
26357
|
-
|
|
26358
|
-
|
|
26359
|
-
|
|
26360
|
-
|
|
26361
|
-
|
|
26362
|
-
|
|
26363
|
-
|
|
26364
|
-
|
|
26365
|
-
|
|
26366
|
-
|
|
26367
|
-
|
|
26368
|
-
|
|
26369
|
-
|
|
26370
|
-
|
|
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
|
-
|
|
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/
|
|
32921
|
-
var
|
|
32922
|
-
|
|
32923
|
-
|
|
32924
|
-
|
|
32925
|
-
|
|
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/
|
|
33033
|
-
function
|
|
33034
|
-
return (0,
|
|
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
|
|
33083
|
-
const
|
|
33084
|
-
|
|
33085
|
-
|
|
33086
|
-
|
|
33087
|
-
|
|
33088
|
-
|
|
33089
|
-
|
|
33090
|
-
const
|
|
33091
|
-
|
|
33092
|
-
|
|
33093
|
-
|
|
33094
|
-
|
|
33095
|
-
|
|
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
|
-
|
|
33183
|
+
console.error("[seeder] Checksum verification passed.");
|
|
33098
33184
|
}
|
|
33099
|
-
function
|
|
33100
|
-
const
|
|
33101
|
-
|
|
33102
|
-
|
|
33103
|
-
|
|
33104
|
-
|
|
33105
|
-
|
|
33106
|
-
|
|
33107
|
-
|
|
33108
|
-
|
|
33109
|
-
|
|
33110
|
-
|
|
33111
|
-
|
|
33112
|
-
|
|
33113
|
-
|
|
33114
|
-
|
|
33115
|
-
|
|
33116
|
-
|
|
33117
|
-
|
|
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
|
|
33126
|
-
const
|
|
33127
|
-
|
|
33128
|
-
|
|
33129
|
-
|
|
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
|
-
|
|
33132
|
-
console.log(`[auth] Key '${label}' revoked.`);
|
|
33243
|
+
console.error(`[seeder] Inserted ${controls_2022_default.length} 2022 controls.`);
|
|
33133
33244
|
}
|
|
33134
|
-
function
|
|
33135
|
-
const
|
|
33136
|
-
|
|
33137
|
-
|
|
33138
|
-
|
|
33139
|
-
|
|
33140
|
-
|
|
33141
|
-
|
|
33142
|
-
|
|
33143
|
-
|
|
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
|
|
33148
|
-
|
|
33149
|
-
|
|
33150
|
-
|
|
33151
|
-
|
|
33152
|
-
|
|
33153
|
-
|
|
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
|
-
|
|
33156
|
-
|
|
33157
|
-
|
|
33158
|
-
|
|
33159
|
-
|
|
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
|
-
|
|
33162
|
-
|
|
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
|
|
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 (
|
|
36123
|
-
conditions.push("
|
|
36124
|
-
params.push(
|
|
36945
|
+
if (source) {
|
|
36946
|
+
conditions.push("source = ?");
|
|
36947
|
+
params.push(source);
|
|
36125
36948
|
}
|
|
36126
|
-
if (
|
|
36127
|
-
conditions.push("
|
|
36128
|
-
|
|
36949
|
+
if (priority) {
|
|
36950
|
+
conditions.push("priority = ?");
|
|
36951
|
+
params.push(priority);
|
|
36129
36952
|
}
|
|
36130
|
-
|
|
36131
|
-
|
|
36132
|
-
|
|
36133
|
-
|
|
36134
|
-
const
|
|
36135
|
-
|
|
36136
|
-
|
|
36137
|
-
|
|
36138
|
-
|
|
36139
|
-
|
|
36140
|
-
|
|
36141
|
-
|
|
36142
|
-
|
|
36143
|
-
|
|
36144
|
-
const
|
|
36145
|
-
|
|
36146
|
-
|
|
36147
|
-
|
|
36148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36155
|
-
|
|
36156
|
-
|
|
36157
|
-
|
|
36158
|
-
|
|
36159
|
-
|
|
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
|
|
36164
|
-
|
|
36165
|
-
|
|
36166
|
-
|
|
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
|
|
36178
|
-
const
|
|
36179
|
-
|
|
36180
|
-
|
|
36181
|
-
|
|
36182
|
-
|
|
36183
|
-
|
|
36184
|
-
|
|
36185
|
-
|
|
36186
|
-
|
|
36187
|
-
|
|
36188
|
-
|
|
36189
|
-
|
|
36190
|
-
|
|
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
|
-
|
|
36193
|
-
|
|
36194
|
-
|
|
36195
|
-
|
|
36196
|
-
|
|
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
|
-
|
|
36206
|
-
|
|
36207
|
-
|
|
36208
|
-
|
|
36209
|
-
|
|
36210
|
-
|
|
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
|
-
|
|
37096
|
+
ts
|
|
36216
37097
|
);
|
|
36217
|
-
|
|
36218
|
-
|
|
36219
|
-
|
|
36220
|
-
|
|
36221
|
-
|
|
36222
|
-
|
|
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
|
|
36226
|
-
const {
|
|
37129
|
+
function handleGetEvidenceDocument(args2) {
|
|
37130
|
+
const { document_id } = args2;
|
|
36227
37131
|
const db = getDb();
|
|
36228
|
-
const row = db.prepare("SELECT * FROM
|
|
36229
|
-
if (!row) throw notFound("
|
|
36230
|
-
|
|
36231
|
-
|
|
36232
|
-
|
|
36233
|
-
|
|
36234
|
-
|
|
36235
|
-
|
|
36236
|
-
|
|
36237
|
-
|
|
36238
|
-
|
|
36239
|
-
|
|
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
|
-
|
|
36242
|
-
|
|
36243
|
-
|
|
36244
|
-
|
|
36245
|
-
|
|
36246
|
-
|
|
36247
|
-
|
|
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
|
-
|
|
36255
|
-
|
|
37184
|
+
let s = schema;
|
|
37185
|
+
while (s instanceof import_zod2.z.ZodEffects) {
|
|
37186
|
+
s = s.innerType();
|
|
36256
37187
|
}
|
|
36257
|
-
return
|
|
37188
|
+
return s.shape;
|
|
36258
37189
|
}
|
|
36259
|
-
function
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
});
|