security-controls-mcp 0.2.0__py3-none-any.whl
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.
- security_controls_mcp/__init__.py +3 -0
- security_controls_mcp/__main__.py +8 -0
- security_controls_mcp/cli.py +255 -0
- security_controls_mcp/config.py +145 -0
- security_controls_mcp/data/framework-to-scf.json +13986 -0
- security_controls_mcp/data/scf-controls.json +50162 -0
- security_controls_mcp/data_loader.py +180 -0
- security_controls_mcp/extractors/__init__.py +5 -0
- security_controls_mcp/extractors/pdf_extractor.py +248 -0
- security_controls_mcp/http_server.py +477 -0
- security_controls_mcp/legal_notice.py +82 -0
- security_controls_mcp/providers.py +238 -0
- security_controls_mcp/registry.py +132 -0
- security_controls_mcp/server.py +613 -0
- security_controls_mcp-0.2.0.dist-info/METADATA +467 -0
- security_controls_mcp-0.2.0.dist-info/RECORD +21 -0
- security_controls_mcp-0.2.0.dist-info/WHEEL +5 -0
- security_controls_mcp-0.2.0.dist-info/entry_points.txt +2 -0
- security_controls_mcp-0.2.0.dist-info/licenses/LICENSE +17 -0
- security_controls_mcp-0.2.0.dist-info/licenses/LICENSE-DATA.md +61 -0
- security_controls_mcp-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""HTTP Server Entry Point for Security Controls MCP.
|
|
3
|
+
|
|
4
|
+
This provides HTTP transport (Server-Sent Events) for remote MCP clients.
|
|
5
|
+
Compatible with Ansvar platform's HTTP MCP client.
|
|
6
|
+
"""
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from typing import Dict
|
|
10
|
+
|
|
11
|
+
import uvicorn
|
|
12
|
+
from mcp.server import Server
|
|
13
|
+
from mcp.types import TextContent, Tool
|
|
14
|
+
from starlette.applications import Starlette
|
|
15
|
+
from starlette.responses import JSONResponse, StreamingResponse
|
|
16
|
+
from starlette.routing import Route
|
|
17
|
+
|
|
18
|
+
from .data_loader import SCFData
|
|
19
|
+
from .legal_notice import print_legal_notice
|
|
20
|
+
|
|
21
|
+
# Initialize data loader
|
|
22
|
+
scf_data = SCFData()
|
|
23
|
+
|
|
24
|
+
# Create MCP server instance
|
|
25
|
+
mcp_server = Server("security-controls-mcp")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@mcp_server.list_tools()
|
|
29
|
+
async def list_tools() -> list[Tool]:
|
|
30
|
+
"""List available tools."""
|
|
31
|
+
return [
|
|
32
|
+
Tool(
|
|
33
|
+
name="get_control",
|
|
34
|
+
description="Get details about a specific SCF control by its ID (e.g., GOV-01, IAC-05)",
|
|
35
|
+
inputSchema={
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"control_id": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "SCF control ID (e.g., GOV-01)",
|
|
41
|
+
},
|
|
42
|
+
"include_mappings": {
|
|
43
|
+
"type": "boolean",
|
|
44
|
+
"description": "Include framework mappings (default: true)",
|
|
45
|
+
"default": True,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
"required": ["control_id"],
|
|
49
|
+
},
|
|
50
|
+
),
|
|
51
|
+
Tool(
|
|
52
|
+
name="search_controls",
|
|
53
|
+
description=(
|
|
54
|
+
"Search for controls by keyword in name or description. "
|
|
55
|
+
"Returns relevant controls with snippets."
|
|
56
|
+
),
|
|
57
|
+
inputSchema={
|
|
58
|
+
"type": "object",
|
|
59
|
+
"properties": {
|
|
60
|
+
"query": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": (
|
|
63
|
+
"Search query (e.g., 'encryption', 'access control', "
|
|
64
|
+
"'incident response')"
|
|
65
|
+
),
|
|
66
|
+
},
|
|
67
|
+
"frameworks": {
|
|
68
|
+
"type": "array",
|
|
69
|
+
"items": {"type": "string"},
|
|
70
|
+
"description": (
|
|
71
|
+
"Optional: filter to controls that map to specific frameworks"
|
|
72
|
+
),
|
|
73
|
+
},
|
|
74
|
+
"limit": {
|
|
75
|
+
"type": "integer",
|
|
76
|
+
"description": "Maximum number of results (default: 10)",
|
|
77
|
+
"default": 10,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
"required": ["query"],
|
|
81
|
+
},
|
|
82
|
+
),
|
|
83
|
+
Tool(
|
|
84
|
+
name="list_frameworks",
|
|
85
|
+
description="List all available security frameworks with metadata",
|
|
86
|
+
inputSchema={
|
|
87
|
+
"type": "object",
|
|
88
|
+
"properties": {
|
|
89
|
+
"detailed": {
|
|
90
|
+
"type": "boolean",
|
|
91
|
+
"description": "Include detailed information (default: false)",
|
|
92
|
+
"default": False,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
),
|
|
97
|
+
Tool(
|
|
98
|
+
name="get_framework_controls",
|
|
99
|
+
description="Get all SCF controls that map to a specific framework",
|
|
100
|
+
inputSchema={
|
|
101
|
+
"type": "object",
|
|
102
|
+
"properties": {
|
|
103
|
+
"framework": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"description": "Framework key (e.g., dora, iso_27001_2022, nist_csf_2_0)",
|
|
106
|
+
},
|
|
107
|
+
"include_descriptions": {
|
|
108
|
+
"type": "boolean",
|
|
109
|
+
"description": (
|
|
110
|
+
"Include control descriptions "
|
|
111
|
+
"(increases token usage, default: false)"
|
|
112
|
+
),
|
|
113
|
+
"default": False,
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
"required": ["framework"],
|
|
117
|
+
},
|
|
118
|
+
),
|
|
119
|
+
Tool(
|
|
120
|
+
name="map_frameworks",
|
|
121
|
+
description=(
|
|
122
|
+
"Map controls between two frameworks via SCF. Shows which target "
|
|
123
|
+
"framework requirements are satisfied by source framework controls."
|
|
124
|
+
),
|
|
125
|
+
inputSchema={
|
|
126
|
+
"type": "object",
|
|
127
|
+
"properties": {
|
|
128
|
+
"source_framework": {
|
|
129
|
+
"type": "string",
|
|
130
|
+
"description": (
|
|
131
|
+
"Source framework key (what you HAVE, e.g., iso_27001_2022)"
|
|
132
|
+
),
|
|
133
|
+
},
|
|
134
|
+
"source_control": {
|
|
135
|
+
"type": "string",
|
|
136
|
+
"description": (
|
|
137
|
+
"Optional: specific source control ID (e.g., A.5.15) "
|
|
138
|
+
"to filter results"
|
|
139
|
+
),
|
|
140
|
+
},
|
|
141
|
+
"target_framework": {
|
|
142
|
+
"type": "string",
|
|
143
|
+
"description": (
|
|
144
|
+
"Target framework key (what you want to SATISFY, e.g., dora)"
|
|
145
|
+
),
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
"required": ["source_framework", "target_framework"],
|
|
149
|
+
},
|
|
150
|
+
),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@mcp_server.call_tool()
|
|
155
|
+
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
|
|
156
|
+
"""Handle tool calls."""
|
|
157
|
+
|
|
158
|
+
if name == "get_control":
|
|
159
|
+
control_id = arguments["control_id"]
|
|
160
|
+
include_mappings = arguments.get("include_mappings", True)
|
|
161
|
+
|
|
162
|
+
control = scf_data.get_control(control_id)
|
|
163
|
+
if not control:
|
|
164
|
+
return [
|
|
165
|
+
TextContent(
|
|
166
|
+
type="text",
|
|
167
|
+
text=f"Control {control_id} not found. Use search_controls to find controls.",
|
|
168
|
+
)
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
response = {
|
|
172
|
+
"id": control["id"],
|
|
173
|
+
"domain": control["domain"],
|
|
174
|
+
"name": control["name"],
|
|
175
|
+
"description": control["description"],
|
|
176
|
+
"weight": control["weight"],
|
|
177
|
+
"pptdf": control["pptdf"],
|
|
178
|
+
"validation_cadence": control["validation_cadence"],
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if include_mappings:
|
|
182
|
+
response["framework_mappings"] = control["framework_mappings"]
|
|
183
|
+
|
|
184
|
+
# Format response
|
|
185
|
+
text = f"**{response['id']}: {response['name']}**\n\n"
|
|
186
|
+
text += f"**Domain:** {response['domain']}\n"
|
|
187
|
+
text += f"**Description:** {response['description']}\n\n"
|
|
188
|
+
text += f"**Weight:** {response['weight']}/10\n"
|
|
189
|
+
text += f"**PPTDF:** {response['pptdf']}\n"
|
|
190
|
+
text += f"**Validation Cadence:** {response['validation_cadence']}\n"
|
|
191
|
+
|
|
192
|
+
if include_mappings:
|
|
193
|
+
text += "\n**Framework Mappings:**\n"
|
|
194
|
+
for fw_key, mappings in response["framework_mappings"].items():
|
|
195
|
+
if mappings:
|
|
196
|
+
fw_name = scf_data.frameworks.get(fw_key, {}).get("name", fw_key)
|
|
197
|
+
text += f"- **{fw_name}:** {', '.join(mappings)}\n"
|
|
198
|
+
|
|
199
|
+
return [TextContent(type="text", text=text)]
|
|
200
|
+
|
|
201
|
+
elif name == "search_controls":
|
|
202
|
+
query = arguments["query"]
|
|
203
|
+
frameworks = arguments.get("frameworks")
|
|
204
|
+
limit = arguments.get("limit", 10)
|
|
205
|
+
|
|
206
|
+
results = scf_data.search_controls(query, frameworks, limit)
|
|
207
|
+
|
|
208
|
+
if not results:
|
|
209
|
+
return [
|
|
210
|
+
TextContent(
|
|
211
|
+
type="text",
|
|
212
|
+
text=f"No controls found matching '{query}'. Try different keywords.",
|
|
213
|
+
)
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
text = f"**Found {len(results)} control(s) matching '{query}'**\n\n"
|
|
217
|
+
for result in results:
|
|
218
|
+
text += f"**{result['control_id']}: {result['name']}**\n"
|
|
219
|
+
text += f"{result['snippet']}\n"
|
|
220
|
+
text += f"*Mapped to: {', '.join(result['mapped_frameworks'][:5])}*\n\n"
|
|
221
|
+
|
|
222
|
+
return [TextContent(type="text", text=text)]
|
|
223
|
+
|
|
224
|
+
elif name == "list_frameworks":
|
|
225
|
+
frameworks = list(scf_data.frameworks.values())
|
|
226
|
+
frameworks.sort(key=lambda x: x["controls_mapped"], reverse=True)
|
|
227
|
+
|
|
228
|
+
text = f"**Available Frameworks ({len(frameworks)} total)**\n\n"
|
|
229
|
+
for fw in frameworks:
|
|
230
|
+
text += f"- **{fw['key']}**: {fw['name']} ({fw['controls_mapped']} controls)\n"
|
|
231
|
+
|
|
232
|
+
return [TextContent(type="text", text=text)]
|
|
233
|
+
|
|
234
|
+
elif name == "get_framework_controls":
|
|
235
|
+
framework = arguments["framework"]
|
|
236
|
+
include_descriptions = arguments.get("include_descriptions", False)
|
|
237
|
+
|
|
238
|
+
if framework not in scf_data.frameworks:
|
|
239
|
+
available = ", ".join(scf_data.frameworks.keys())
|
|
240
|
+
return [
|
|
241
|
+
TextContent(
|
|
242
|
+
type="text",
|
|
243
|
+
text=f"Framework '{framework}' not found. Available: {available}",
|
|
244
|
+
)
|
|
245
|
+
]
|
|
246
|
+
|
|
247
|
+
controls = scf_data.get_framework_controls(framework, include_descriptions)
|
|
248
|
+
|
|
249
|
+
fw_info = scf_data.frameworks[framework]
|
|
250
|
+
text = f"**{fw_info['name']}**\n"
|
|
251
|
+
text += f"**Total Controls:** {len(controls)}\n\n"
|
|
252
|
+
|
|
253
|
+
# Group by domain for readability
|
|
254
|
+
by_domain: Dict[str, list] = {}
|
|
255
|
+
for ctrl in controls:
|
|
256
|
+
# Get full control to get domain
|
|
257
|
+
full_ctrl = scf_data.get_control(ctrl["scf_id"])
|
|
258
|
+
if full_ctrl:
|
|
259
|
+
domain = full_ctrl["domain"]
|
|
260
|
+
if domain not in by_domain:
|
|
261
|
+
by_domain[domain] = []
|
|
262
|
+
by_domain[domain].append(ctrl)
|
|
263
|
+
|
|
264
|
+
for domain, domain_ctrls in sorted(by_domain.items()):
|
|
265
|
+
text += f"\n**{domain}**\n"
|
|
266
|
+
for ctrl in domain_ctrls[:10]: # Limit per domain for readability
|
|
267
|
+
text += f"- **{ctrl['scf_id']}**: {ctrl['scf_name']}\n"
|
|
268
|
+
text += f" Maps to: {', '.join(ctrl['framework_control_ids'][:5])}\n"
|
|
269
|
+
if include_descriptions:
|
|
270
|
+
text += f" {ctrl['description'][:100]}...\n"
|
|
271
|
+
|
|
272
|
+
if len(domain_ctrls) > 10:
|
|
273
|
+
text += f" *... and {len(domain_ctrls) - 10} more controls*\n"
|
|
274
|
+
|
|
275
|
+
return [TextContent(type="text", text=text)]
|
|
276
|
+
|
|
277
|
+
elif name == "map_frameworks":
|
|
278
|
+
source_framework = arguments["source_framework"]
|
|
279
|
+
target_framework = arguments["target_framework"]
|
|
280
|
+
source_control = arguments.get("source_control")
|
|
281
|
+
|
|
282
|
+
# Validate frameworks exist
|
|
283
|
+
if source_framework not in scf_data.frameworks:
|
|
284
|
+
available = ", ".join(scf_data.frameworks.keys())
|
|
285
|
+
return [
|
|
286
|
+
TextContent(
|
|
287
|
+
type="text",
|
|
288
|
+
text=f"Source framework '{source_framework}' not found. Available: {available}",
|
|
289
|
+
)
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
if target_framework not in scf_data.frameworks:
|
|
293
|
+
available = ", ".join(scf_data.frameworks.keys())
|
|
294
|
+
return [
|
|
295
|
+
TextContent(
|
|
296
|
+
type="text",
|
|
297
|
+
text=f"Target framework '{target_framework}' not found. Available: {available}",
|
|
298
|
+
)
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
mappings = scf_data.map_frameworks(source_framework, target_framework, source_control)
|
|
302
|
+
|
|
303
|
+
if not mappings:
|
|
304
|
+
return [
|
|
305
|
+
TextContent(
|
|
306
|
+
type="text",
|
|
307
|
+
text=f"No mappings found between {source_framework} and {target_framework}",
|
|
308
|
+
)
|
|
309
|
+
]
|
|
310
|
+
|
|
311
|
+
source_name = scf_data.frameworks[source_framework]["name"]
|
|
312
|
+
target_name = scf_data.frameworks[target_framework]["name"]
|
|
313
|
+
|
|
314
|
+
text = f"**Mapping: {source_name} → {target_name}**\n"
|
|
315
|
+
if source_control:
|
|
316
|
+
text += f"**Filtered to source control: {source_control}**\n"
|
|
317
|
+
text += f"**Found {len(mappings)} SCF controls**\n\n"
|
|
318
|
+
|
|
319
|
+
for mapping in mappings[:20]: # Limit for readability
|
|
320
|
+
text += (
|
|
321
|
+
f"**{mapping['scf_id']}: {mapping['scf_name']}** (weight: {mapping['weight']})\n"
|
|
322
|
+
)
|
|
323
|
+
text += f"- Source ({source_framework}): {', '.join(mapping['source_controls'][:5])}\n"
|
|
324
|
+
if mapping["target_controls"]:
|
|
325
|
+
text += (
|
|
326
|
+
f"- Target ({target_framework}): {', '.join(mapping['target_controls'][:5])}\n"
|
|
327
|
+
)
|
|
328
|
+
else:
|
|
329
|
+
text += f"- Target ({target_framework}): *No direct mapping*\n"
|
|
330
|
+
text += "\n"
|
|
331
|
+
|
|
332
|
+
if len(mappings) > 20:
|
|
333
|
+
text += f"\n*Showing first 20 of {len(mappings)} mappings*\n"
|
|
334
|
+
|
|
335
|
+
return [TextContent(type="text", text=text)]
|
|
336
|
+
|
|
337
|
+
else:
|
|
338
|
+
raise ValueError(f"Unknown tool: {name}")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
async def health_check(request):
|
|
342
|
+
"""Health check endpoint."""
|
|
343
|
+
return JSONResponse(
|
|
344
|
+
{
|
|
345
|
+
"status": "ok",
|
|
346
|
+
"server": "security-controls-mcp",
|
|
347
|
+
"database_version": "SCF 2025.4",
|
|
348
|
+
"controls_count": len(scf_data.controls),
|
|
349
|
+
"frameworks_count": len(scf_data.frameworks),
|
|
350
|
+
}
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
async def mcp_endpoint(request):
|
|
355
|
+
"""MCP endpoint - accepts JSON-RPC requests."""
|
|
356
|
+
try:
|
|
357
|
+
# Parse JSON-RPC request
|
|
358
|
+
body = await request.json()
|
|
359
|
+
method = body.get("method")
|
|
360
|
+
params = body.get("params", {})
|
|
361
|
+
request_id = body.get("id", 1)
|
|
362
|
+
|
|
363
|
+
# Handle initialize
|
|
364
|
+
if method == "initialize":
|
|
365
|
+
response = {
|
|
366
|
+
"jsonrpc": "2.0",
|
|
367
|
+
"id": request_id,
|
|
368
|
+
"result": {
|
|
369
|
+
"protocolVersion": "2024-11-05",
|
|
370
|
+
"capabilities": {"tools": {}},
|
|
371
|
+
"serverInfo": {"name": "security-controls-mcp", "version": "0.1.0"},
|
|
372
|
+
},
|
|
373
|
+
}
|
|
374
|
+
return StreamingResponse(
|
|
375
|
+
iter([f"event: message\ndata: {json.dumps(response)}\n\n"]),
|
|
376
|
+
media_type="text/event-stream",
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Handle list tools
|
|
380
|
+
elif method == "tools/list":
|
|
381
|
+
tools = await list_tools()
|
|
382
|
+
response = {
|
|
383
|
+
"jsonrpc": "2.0",
|
|
384
|
+
"id": request_id,
|
|
385
|
+
"result": {
|
|
386
|
+
"tools": [
|
|
387
|
+
{
|
|
388
|
+
"name": tool.name,
|
|
389
|
+
"description": tool.description,
|
|
390
|
+
"inputSchema": tool.inputSchema,
|
|
391
|
+
}
|
|
392
|
+
for tool in tools
|
|
393
|
+
]
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
return StreamingResponse(
|
|
397
|
+
iter([f"event: message\ndata: {json.dumps(response)}\n\n"]),
|
|
398
|
+
media_type="text/event-stream",
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# Handle tool call
|
|
402
|
+
elif method == "tools/call":
|
|
403
|
+
tool_name = params.get("name")
|
|
404
|
+
arguments = params.get("arguments", {})
|
|
405
|
+
|
|
406
|
+
# Call the tool
|
|
407
|
+
result = await call_tool(tool_name, arguments)
|
|
408
|
+
|
|
409
|
+
response = {
|
|
410
|
+
"jsonrpc": "2.0",
|
|
411
|
+
"id": request_id,
|
|
412
|
+
"result": {"content": [{"type": "text", "text": item.text} for item in result]},
|
|
413
|
+
}
|
|
414
|
+
return StreamingResponse(
|
|
415
|
+
iter([f"event: message\ndata: {json.dumps(response)}\n\n"]),
|
|
416
|
+
media_type="text/event-stream",
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
else:
|
|
420
|
+
# Unknown method
|
|
421
|
+
response = {
|
|
422
|
+
"jsonrpc": "2.0",
|
|
423
|
+
"id": request_id,
|
|
424
|
+
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
|
425
|
+
}
|
|
426
|
+
return StreamingResponse(
|
|
427
|
+
iter([f"event: message\ndata: {json.dumps(response)}\n\n"]),
|
|
428
|
+
media_type="text/event-stream",
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
except Exception as e:
|
|
432
|
+
# Error response
|
|
433
|
+
response = {
|
|
434
|
+
"jsonrpc": "2.0",
|
|
435
|
+
"id": 1,
|
|
436
|
+
"error": {"code": -32603, "message": f"Internal error: {str(e)}"},
|
|
437
|
+
}
|
|
438
|
+
return StreamingResponse(
|
|
439
|
+
iter([f"event: message\ndata: {json.dumps(response)}\n\n"]),
|
|
440
|
+
media_type="text/event-stream",
|
|
441
|
+
status_code=500,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
# Starlette app
|
|
446
|
+
app = Starlette(
|
|
447
|
+
routes=[
|
|
448
|
+
Route("/health", health_check),
|
|
449
|
+
Route("/mcp", mcp_endpoint, methods=["POST"]),
|
|
450
|
+
],
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def main():
|
|
455
|
+
"""Start HTTP server."""
|
|
456
|
+
# Display legal notice on startup
|
|
457
|
+
print_legal_notice()
|
|
458
|
+
|
|
459
|
+
# Get port from environment
|
|
460
|
+
port = int(os.getenv("PORT", "3000"))
|
|
461
|
+
|
|
462
|
+
print(f"\n✓ Security Controls MCP HTTP server starting on port {port}")
|
|
463
|
+
print(
|
|
464
|
+
f"✓ Loaded {len(scf_data.controls)} controls across {len(scf_data.frameworks)} frameworks\n"
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# Run server
|
|
468
|
+
uvicorn.run(
|
|
469
|
+
app,
|
|
470
|
+
host="0.0.0.0",
|
|
471
|
+
port=port,
|
|
472
|
+
log_level="info",
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
if __name__ == "__main__":
|
|
477
|
+
main()
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Legal compliance notices for SCF data usage."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
LEGAL_NOTICE = """
|
|
6
|
+
╔════════════════════════════════════════════════════════════════════════════╗
|
|
7
|
+
║ SECURITY CONTROLS MCP SERVER ║
|
|
8
|
+
║ LEGAL USAGE NOTICE ║
|
|
9
|
+
╚════════════════════════════════════════════════════════════════════════════╝
|
|
10
|
+
|
|
11
|
+
This server provides Secure Controls Framework (SCF) data licensed under
|
|
12
|
+
CC BY-ND 4.0 by ComplianceForge.
|
|
13
|
+
|
|
14
|
+
⚠️ AI DERIVATIVE CONTENT RESTRICTION:
|
|
15
|
+
|
|
16
|
+
The SCF license PROHIBITS using AI systems (including Claude) to generate
|
|
17
|
+
derivative content such as policies, standards, procedures, or metrics
|
|
18
|
+
based on SCF controls.
|
|
19
|
+
|
|
20
|
+
✓ PERMITTED USES:
|
|
21
|
+
• Query control details and mappings
|
|
22
|
+
• Map between frameworks (ISO 27001 → DORA, etc.)
|
|
23
|
+
• Reference controls in your work (with attribution)
|
|
24
|
+
• Understand compliance requirements
|
|
25
|
+
|
|
26
|
+
✗ PROHIBITED USES:
|
|
27
|
+
• Asking Claude to write policies/procedures from SCF controls
|
|
28
|
+
• Creating derivative frameworks for distribution
|
|
29
|
+
• Generating automated compliance content using AI
|
|
30
|
+
|
|
31
|
+
Full terms: https://securecontrolsframework.com/terms-conditions/
|
|
32
|
+
|
|
33
|
+
This is not legal advice. Consult legal professionals for compliance guidance.
|
|
34
|
+
|
|
35
|
+
════════════════════════════════════════════════════════════════════════════
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def print_legal_notice(registry=None):
|
|
40
|
+
"""Print legal notice to stderr on server startup.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
registry: Optional StandardRegistry to show information about paid standards
|
|
44
|
+
"""
|
|
45
|
+
print(LEGAL_NOTICE, file=sys.stderr)
|
|
46
|
+
|
|
47
|
+
# If paid standards are loaded, show additional notice
|
|
48
|
+
if registry and registry.has_paid_standards():
|
|
49
|
+
paid_notice = (
|
|
50
|
+
"\n╔════════════════════════════════════════════════════════════════════════════╗\n"
|
|
51
|
+
)
|
|
52
|
+
paid_notice += (
|
|
53
|
+
"║ PAID STANDARDS LOADED ║\n"
|
|
54
|
+
)
|
|
55
|
+
paid_notice += (
|
|
56
|
+
"╚════════════════════════════════════════════════════════════════════════════╝\n\n"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
standards = registry.list_standards()
|
|
60
|
+
paid_standards = [s for s in standards if s["type"] == "paid"]
|
|
61
|
+
|
|
62
|
+
if paid_standards:
|
|
63
|
+
paid_notice += "Loaded purchased standards:\n\n"
|
|
64
|
+
for std in paid_standards:
|
|
65
|
+
paid_notice += f" ✓ {std['title']}\n"
|
|
66
|
+
paid_notice += f" License: {std['license']}\n"
|
|
67
|
+
paid_notice += f" Purchased: {std['purchase_date']}\n\n"
|
|
68
|
+
|
|
69
|
+
paid_notice += "⚠️ IMPORTANT LICENSE RESTRICTIONS:\n\n"
|
|
70
|
+
paid_notice += " • Licensed for your personal use only\n"
|
|
71
|
+
paid_notice += " • No redistribution or sharing of content\n"
|
|
72
|
+
paid_notice += (
|
|
73
|
+
" • Do not share extracted text or create derivatives for distribution\n"
|
|
74
|
+
)
|
|
75
|
+
paid_notice += " • Consult your purchase agreement for full terms\n\n"
|
|
76
|
+
paid_notice += "This tool provides query access only. Ensure your use complies with\n"
|
|
77
|
+
paid_notice += "all applicable license agreements.\n\n"
|
|
78
|
+
paid_notice += (
|
|
79
|
+
"════════════════════════════════════════════════════════════════════════════\n"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
print(paid_notice, file=sys.stderr)
|