-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathcommandmcp.py
More file actions
163 lines (152 loc) · 9.13 KB
/
Copy pathcommandmcp.py
File metadata and controls
163 lines (152 loc) · 9.13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
from datetime import date, datetime
import os, sys
from mcp.server.fastmcp import FastMCP
import command_sdk_lite as keyfactor
from keyfactor_v_1_client import client as kfclient
from keyfactor_v_1_client.models.keyfactor_common_queryable_extensions_sort_order import KeyfactorCommonQueryableExtensionsSortOrder
from keyfactor_v_1_client.models.keyfactor_pki_enums_revoke_code import KeyfactorPKIEnumsRevokeCode
commandclient = {}
mcp = FastMCP("keyfactorCommand")
@mcp.tool()
def get_module_info_for_mcp():
"""
This MCP server exposes certain functions of Keyfactor Command, a Certificate Lifecycle Management and Automation platform
that performs many PKI and X.509 certificate operations and integrates with a wide range of CAs and enterprise tools.
The most basic CLM operations supported are to enroll a certificate, revoke a certificate, and search for issued certificates.
Certificate search is a particularly good fit for an LLM, as users may be unfmailiar with the query syntax, elaborate queries
are cumbersome to create by hand, and reviewing results to find one particular certificate is still a challenge.
Keyfactor Command also includes a "Risk Intelligence" component that consists of a series of findings about a particular
certificate. These risks are quantified in a proprietary formula that does not reflect universal impact and should be considered
directional guidance or for reference only, and the LLM's independent assessment should always be incorporated as well.
A report combining both is of most use to users. The scores of all findings are added up and defined as the certificate's
overall risk score. Factors that influence the weighting include immediate cryptographic vulnerability, but factors that
imply insecure CA configuration, certificate chain, or overall PKI operations are weighted more highly, as this is a
significantly higher concern for PKI teams. Findings that compromise the whole PKI's ability to enforce enterprise policy,
such as fatally permissive key usage or path errors, are weighted highest. Findings that might cause an attacker to
select one organization's PKI for attack vs another organization's are also considered very significant.
The range for a finding is that 1000 or higher is very high, 100-999 is high, 10-99 is low, 1-9 is minimal,
and 0 is informational only. Likely users of this product may not have all PKI and cryptographic skills to interpret the findings
and their significance, and the tools here expose the functions that allow a user to augment the findings with AI insights
about the particular risks. The available functions to fetch or query certificates will include the risk score, and the raw
findings and their relative weighting.
"""
return "Keyfactor Command plus Risk Intelligence MCP server"
@mcp.tool()
def get_license():
"""
Get user license for Keyfactor Command
"""
return keyfactor.get_license_()
@mcp.tool()
def get_certificate_details_by_id(certid):
"""
Get PEM encoded content for a certificate by id along with all metadata stored in Command, including Risk Intelligence output
Risk Intelligence metadata should always be blended with the MCP client's independent assessment of how the cert reflects on quality of PKI operations.
MCP clients can use the query_certificates by thumbprint to retrieve all details known to Command.
"""
response = keyfactor.get_certificates_id_(id=certid,include_metadata=True)
return {"certificate":response.content_bytes, "metadata":response.metadata,"client_directions":"Always conduct independent assessment of risk and provide comprehensive risk summary blending both analyses. Focus on practical security impact to an enterprise network."}
@mcp.tool()
def enroll_certificate(subject, renewal_certificate_id=0, dns_sans=[]):
"""
Enroll a certificate in Keyfactor Command using configuration stored in MCP server. If the common name is a DNS hotname it should also be copied into a SAN.
Args:
subject: Distinguished name for the X.509 certificate
renewal_certificate_id: Optional integer id of the certificate being renewed.
dns_sans: Optional list of strings representing all DNS Subject Alternative Names. Multiple entries must be passed as a serialized list.
"""
template = os.environ.get('KEYFACTOR_TEMPLATE',"")
ca = os.environ.get('KEYFACTOR_CA',"")
timestamp = datetime.utcnow().isoformat(timespec="seconds")+"+00:00"
request = {
"subject":subject,
"template":template,
"timestamp":timestamp,
"certificate_authority":ca}
if int(renewal_certificate_id) > 0:
request["renewalCertificateId"] = int(renewal_certificate_id)
if len(dns_sans) > 0:
if isinstance(dns_sans,str):
san=dns_sans
dns_sans=[san]
request["saNs"] = {"dns":dns_sans}
response = keyfactor.enrollment_pfx(request)
response["client_directions"]="Do not display PFX password."
return response
@mcp.tool()
def enroll_csr(csr):
"""
Enroll a certificate from the user's CSR.
Args:
CSR: PEM-encoded CSR
"""
template = os.environ.get('KEYFACTOR_TEMPLATE',"")
timestamp = datetime.utcnow().isoformat(timespec="seconds")+"+00:00"
return keyfactor.enrollment_csr({"csr":csr,"template":template,"timestamp":timestamp})
@mcp.tool()
def revoke_certificate(certid, reason_code=0, comment="Revoked via MCP"):
"""
Revoke a certificate by id
Args:
certid: integer
reason_code: integer for revocation reason per X.509 spec.
comment: String describing the reason the certificate is being revoked per X.509 standard
"""
today = str(date.today())
try:
reason = KeyfactorPKIEnumsRevokeCode(int(reason_code))
except:
pass
return keyfactor.certificates_revoke({"CertificateIds":[certid],"Reason":reason,"Comment":comment,"EffectiveDate":f"{today}T00:00:00Z"})
@mcp.tool()
def getMetadataFields():
"""
Return a list of all metadata fields defined in the platform.
"""
return keyfactor.get_metadata_fields_()
#@mcp.tool()
def setMetadata(certid, field, value):
"""
Keyfactor Command stores metadata for each cert. Set certificate metadata field to given value for given certificate id.
Args:
certid: Integer certificate id
field: String which must be a metadata field name present in Command
value: Value to which the field should be set for this cert.
"""
return keyfactor.put_certificates_metadata({})
@mcp.tool()
def query_certificates(query, include_revoked=False, include_expired=False, verbose=0, sort_field=None, sort_order=True, return_limit=50):
"""
Query the Keyfactor Command platform for all X.509 certificates under management, with AND, OR, and parens supported
Args:
query: string in Keyfactor query language: <field> <comparison> <value>
Fields: CN, DN, IssuerDN, IssuedDate, ExpirationDate, Thumbprint, SerialNumber, CertId, SAN, KeyType, KeySize, any metadata field possibly including RiskScore and RiskIntelligenceResults, and more
Comparisons: -eq, -ne, -gt, -ge, -lt, -le. Sometimes -contains, -startswith, -endswith
Values: May include %ME%, %TODAY%, %TODAY+n%, and %TODAY-n% tokens, for integer n. Times in ISO 8601. Must be in double quotes.
include_revoked: Boolean
include_expired: Boolean
verbose: integer 0-3. 0 returns minimal info. 3 returns everything including risk info and other metadata. Initial queries should always start at 0 and increase in followup queries.
sort_field: subset of fields available for query. Includes RiskScore.
sort_order: Boolean. "true" to find highest risk score or newest date.
return_limit: Integer. Set to "1" if querying for the top certificate only for best performance.
Responses:
Empty response means no matches.
Results for sort should always be checked to ensure that the sort order is correct.
Risk data should always be blended with the MCP client's own assessment of issues with the certificate, security impact and relative severity.
Example query values:
Match by common name: CN -contains "example"
Expiring in the next week: ExpirationDate -gt "%TODAY%" AND ExpirationDate -lt "%TODAY+7%"
All certificates: CertId -gt "0"
"""
if sort_order == 'true':
sort_order = True
if sort_order == 'false':
sort_order = False
sort_ascending = KeyfactorCommonQueryableExtensionsSortOrder(sort_order)
return keyfactor.get_certificates_(query_string=query, verbose=verbose, include_revoked=include_revoked, include_expired=include_expired, sort_field=sort_field, sort_ascending=sort_ascending, return_limit=return_limit)
if __name__ == "__main__":
# Initialize and run the server
print("Initializing Command MCP",file=sys.stderr)
#print(revoke_certificate(3, reason_code=0))
#print(query_certificates('CertId -eq "8"'))
mcp.run(transport='stdio')