qualys-mcp 2.1.0__py3-none-any.whl → 2.1.2__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.
- {qualys_mcp-2.1.0.dist-info → qualys_mcp-2.1.2.dist-info}/METADATA +1 -1
- qualys_mcp-2.1.2.dist-info/RECORD +6 -0
- qualys_mcp.py +51 -4
- qualys_mcp-2.1.0.dist-info/RECORD +0 -6
- {qualys_mcp-2.1.0.dist-info → qualys_mcp-2.1.2.dist-info}/WHEEL +0 -0
- {qualys_mcp-2.1.0.dist-info → qualys_mcp-2.1.2.dist-info}/entry_points.txt +0 -0
- {qualys_mcp-2.1.0.dist-info → qualys_mcp-2.1.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: qualys-mcp
|
|
3
|
-
Version: 2.1.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: MCP server for Qualys security APIs - natural language interaction with vulnerability, asset, and cloud security data
|
|
5
5
|
Project-URL: Homepage, https://github.com/nelssec/qualys-mcp
|
|
6
6
|
Project-URL: Repository, https://github.com/nelssec/qualys-mcp
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
qualys_mcp.py,sha256=DYP5SOqd9_E9XhHwwTsvIJxNSDec0NSJOV4fSYkw710,29479
|
|
2
|
+
qualys_mcp-2.1.2.dist-info/METADATA,sha256=Ii3I5JnPtgCKWiBFWtLej4sjq--SWuWNrGepJUTRAU4,3295
|
|
3
|
+
qualys_mcp-2.1.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
4
|
+
qualys_mcp-2.1.2.dist-info/entry_points.txt,sha256=Dc8X0AhJDjGaZOJ0SNpWDWjEX4sYzrYa9FZEbggX0Rs,47
|
|
5
|
+
qualys_mcp-2.1.2.dist-info/licenses/LICENSE,sha256=dW3nC4AX_VbxPAgneSDR-miZPiHgAYw5JhPtdbUEt_E,1091
|
|
6
|
+
qualys_mcp-2.1.2.dist-info/RECORD,,
|
qualys_mcp.py
CHANGED
|
@@ -14,8 +14,15 @@ mcp = FastMCP("qualys-mcp")
|
|
|
14
14
|
|
|
15
15
|
USERNAME = os.environ.get('QUALYS_USERNAME', '')
|
|
16
16
|
PASSWORD = os.environ.get('QUALYS_PASSWORD', '')
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
|
|
18
|
+
def normalize_url(url):
|
|
19
|
+
url = url.strip().rstrip('/')
|
|
20
|
+
if url and not url.startswith('http'):
|
|
21
|
+
url = f"https://{url}"
|
|
22
|
+
return url
|
|
23
|
+
|
|
24
|
+
BASE_URL = normalize_url(os.environ.get('QUALYS_BASE_URL', ''))
|
|
25
|
+
GATEWAY_URL = normalize_url(os.environ.get('QUALYS_GATEWAY_URL', ''))
|
|
19
26
|
BASIC_AUTH = base64.b64encode(f"{USERNAME}:{PASSWORD}".encode()).decode()
|
|
20
27
|
BEARER_TOKEN = None
|
|
21
28
|
|
|
@@ -105,6 +112,46 @@ def get_assets(limit=100, qql=None):
|
|
|
105
112
|
return []
|
|
106
113
|
|
|
107
114
|
|
|
115
|
+
def get_eol_assets(stage_filter="EOL,EOL/EOS", limit=500):
|
|
116
|
+
url = f"{GATEWAY_URL}/rest/2.0/search/am/asset?pageSize={limit}"
|
|
117
|
+
token = get_bearer_token()
|
|
118
|
+
|
|
119
|
+
filter_body = json.dumps({
|
|
120
|
+
"filters": [
|
|
121
|
+
{"field": "operatingSystem.lifecycle.stage", "operator": "IN", "value": stage_filter}
|
|
122
|
+
]
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
req = Request(url, data=filter_body.encode(), method='POST')
|
|
126
|
+
req.add_header('Authorization', f'Bearer {token}' if token else f'Basic {BASIC_AUTH}')
|
|
127
|
+
req.add_header('Content-Type', 'application/json')
|
|
128
|
+
req.add_header('X-Requested-With', 'qualys-mcp')
|
|
129
|
+
|
|
130
|
+
try:
|
|
131
|
+
with urlopen(req, timeout=60) as resp:
|
|
132
|
+
data = json.loads(resp.read())
|
|
133
|
+
assets = []
|
|
134
|
+
for a in data.get('assetListData', {}).get('asset', []):
|
|
135
|
+
os_info = a.get('operatingSystem', {})
|
|
136
|
+
lifecycle = os_info.get('lifecycle', {})
|
|
137
|
+
assets.append({
|
|
138
|
+
'assetId': a.get('assetId'),
|
|
139
|
+
'address': a.get('address', ''),
|
|
140
|
+
'dnsName': a.get('dnsHostName', ''),
|
|
141
|
+
'operatingSystem': {
|
|
142
|
+
'osName': f"{os_info.get('name', '')} {os_info.get('version', '')}".strip(),
|
|
143
|
+
'lifecycle': {
|
|
144
|
+
'stage': lifecycle.get('stage', ''),
|
|
145
|
+
'eolDate': lifecycle.get('eolDate', ''),
|
|
146
|
+
'eosDate': lifecycle.get('eosDate', '')
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
return assets
|
|
151
|
+
except Exception as e:
|
|
152
|
+
return []
|
|
153
|
+
|
|
154
|
+
|
|
108
155
|
def get_images(limit=100, severity=None):
|
|
109
156
|
url = f"{GATEWAY_URL}/csapi/v1.3/images?pageSize={limit}"
|
|
110
157
|
if severity:
|
|
@@ -477,7 +524,7 @@ def get_tech_debt(days_until_eol: int = 0) -> dict:
|
|
|
477
524
|
'byOS': []
|
|
478
525
|
}
|
|
479
526
|
|
|
480
|
-
assets =
|
|
527
|
+
assets = get_eol_assets("EOL,EOL/EOS,EOS", 500)
|
|
481
528
|
result['stats']['total'] = len(assets)
|
|
482
529
|
|
|
483
530
|
today = datetime.utcnow().date()
|
|
@@ -516,7 +563,7 @@ def get_tech_debt(days_until_eol: int = 0) -> dict:
|
|
|
516
563
|
os_data[os_name]['eol'] += 1
|
|
517
564
|
if len(result['currentEOL']) < 20:
|
|
518
565
|
result['currentEOL'].append(asset_info)
|
|
519
|
-
elif stage
|
|
566
|
+
elif stage in ('EOS', 'EOL/EOS'):
|
|
520
567
|
result['stats']['currentEOS'] += 1
|
|
521
568
|
os_data[os_name]['eos'] += 1
|
|
522
569
|
if len(result['currentEOS']) < 20:
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
qualys_mcp.py,sha256=6V8ulDBKwLD7elgDT3Ow4GWUv1HEMHUrT-Xeer4yPrY,27677
|
|
2
|
-
qualys_mcp-2.1.0.dist-info/METADATA,sha256=-QohxatoOIOGnUwLISk4GJ7-QGp3xIyCVVUY3gAUwV4,3295
|
|
3
|
-
qualys_mcp-2.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
4
|
-
qualys_mcp-2.1.0.dist-info/entry_points.txt,sha256=Dc8X0AhJDjGaZOJ0SNpWDWjEX4sYzrYa9FZEbggX0Rs,47
|
|
5
|
-
qualys_mcp-2.1.0.dist-info/licenses/LICENSE,sha256=dW3nC4AX_VbxPAgneSDR-miZPiHgAYw5JhPtdbUEt_E,1091
|
|
6
|
-
qualys_mcp-2.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|