qualys-mcp 2.1.3__py3-none-any.whl → 2.1.5__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: qualys-mcp
3
- Version: 2.1.3
3
+ Version: 2.1.5
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=mWx-ygYB1S1cDlO4bkb8nCyTOaSBFwpUwy8r-dhF3Yg,32079
2
+ qualys_mcp-2.1.5.dist-info/METADATA,sha256=3hHgBza5aNzc7isX-18NtFThXnSyUyy7EeX-X18ZHFs,3295
3
+ qualys_mcp-2.1.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
4
+ qualys_mcp-2.1.5.dist-info/entry_points.txt,sha256=Dc8X0AhJDjGaZOJ0SNpWDWjEX4sYzrYa9FZEbggX0Rs,47
5
+ qualys_mcp-2.1.5.dist-info/licenses/LICENSE,sha256=dW3nC4AX_VbxPAgneSDR-miZPiHgAYw5JhPtdbUEt_E,1091
6
+ qualys_mcp-2.1.5.dist-info/RECORD,,
qualys_mcp.py CHANGED
@@ -42,7 +42,7 @@ def get_bearer_token():
42
42
  return None
43
43
 
44
44
 
45
- def api_get(url, gateway=False):
45
+ def api_get(url, gateway=False, timeout=30):
46
46
  req = Request(url)
47
47
  if gateway:
48
48
  token = get_bearer_token()
@@ -51,7 +51,7 @@ def api_get(url, gateway=False):
51
51
  req.add_header('Authorization', f'Basic {BASIC_AUTH}')
52
52
  req.add_header('X-Requested-With', 'qualys-mcp')
53
53
  try:
54
- with urlopen(req, timeout=60) as resp:
54
+ with urlopen(req, timeout=timeout) as resp:
55
55
  return resp.read()
56
56
  except:
57
57
  return None
@@ -381,34 +381,49 @@ def get_security_posture() -> dict:
381
381
  health = 100
382
382
  result = {'healthScore': 0, 'assets': {'total': 0, 'highRisk': 0},
383
383
  'vulns': {'critical': 0, 'high': 0}, 'containers': {'total': 0, 'atRisk': 0},
384
- 'cloud': {'accounts': 0, 'failedControls': 0}}
384
+ 'cloud': {'accounts': 0, 'failedControls': 0}, 'errors': []}
385
385
 
386
- assets = get_assets(500)
387
- result['assets']['total'] = len(assets)
388
- result['assets']['highRisk'] = len([a for a in assets if a.get('assetRiskScore', 0) >= 700])
389
- if assets:
390
- health -= int(result['assets']['highRisk'] / len(assets) * 50)
391
-
392
- result['vulns']['critical'] = len(get_detections(5, 200))
393
- result['vulns']['high'] = len(get_detections(4, 200))
394
- if result['vulns']['critical'] > 50:
395
- health -= 20
396
- elif result['vulns']['critical'] > 10:
397
- health -= 10
398
-
399
- imgs = get_images(500)
400
- result['containers']['total'] = len(imgs)
401
- vuln_ids = {i.get('imageId') for i in get_images(100, 5)}
402
- result['containers']['atRisk'] = len([c for c in get_containers(500) if c.get('imageId') in vuln_ids])
386
+ try:
387
+ assets = get_assets(100)
388
+ result['assets']['total'] = len(assets)
389
+ result['assets']['highRisk'] = len([a for a in assets if a.get('assetRiskScore', 0) >= 700])
390
+ if assets:
391
+ health -= int(result['assets']['highRisk'] / len(assets) * 50)
392
+ except:
393
+ result['errors'].append('assets')
403
394
 
404
- for p in ['aws', 'azure', 'gcp']:
405
- conns = get_connectors(p, 20)
406
- result['cloud']['accounts'] += len(conns)
407
- if conns:
408
- acc = conns[0].get('awsAccountId') or conns[0].get('azureSubscriptionId') or conns[0].get('gcpProjectId')
409
- if acc:
410
- result['cloud']['failedControls'] += len([e for e in get_evaluations(acc, p, 500) if e.get('result') in ['FAIL', 'FAILED']])
395
+ try:
396
+ result['vulns']['critical'] = len(get_detections(5, 100))
397
+ result['vulns']['high'] = len(get_detections(4, 100))
398
+ if result['vulns']['critical'] > 50:
399
+ health -= 20
400
+ elif result['vulns']['critical'] > 10:
401
+ health -= 10
402
+ except:
403
+ result['errors'].append('vulns')
404
+
405
+ try:
406
+ imgs = get_images(100)
407
+ result['containers']['total'] = len(imgs)
408
+ vuln_ids = {i.get('imageId') for i in get_images(50, 5)}
409
+ result['containers']['atRisk'] = len([c for c in get_containers(100) if c.get('imageId') in vuln_ids])
410
+ except:
411
+ result['errors'].append('containers')
412
+
413
+ try:
414
+ for p in ['aws']:
415
+ conns = get_connectors(p, 10)
416
+ result['cloud']['accounts'] += len(conns)
417
+ if conns:
418
+ acc = conns[0].get('awsAccountId') or conns[0].get('azureSubscriptionId') or conns[0].get('gcpProjectId')
419
+ if acc:
420
+ result['cloud']['failedControls'] += len([e for e in get_evaluations(acc, p, 100) if e.get('result') in ['FAIL', 'FAILED']])
421
+ break
422
+ except:
423
+ result['errors'].append('cloud')
411
424
 
425
+ if not result['errors']:
426
+ del result['errors']
412
427
  result['healthScore'] = max(0, health)
413
428
  return result
414
429
 
@@ -795,6 +810,57 @@ def get_webapp_vulns(severity: int = 4, limit: int = 50) -> dict:
795
810
  return result
796
811
 
797
812
 
813
+ @mcp.tool()
814
+ def debug_api(endpoint: str = "eol") -> dict:
815
+ """Debug API connectivity. Use endpoint='eol' to test EOL query, 'assets' for basic assets, 'auth' for auth test."""
816
+ result = {'endpoint': endpoint, 'gateway_url': GATEWAY_URL, 'base_url': BASE_URL}
817
+
818
+ if endpoint == 'auth':
819
+ token = get_bearer_token()
820
+ result['token_obtained'] = bool(token)
821
+ result['token_preview'] = token[:20] + '...' if token else None
822
+ return result
823
+
824
+ if endpoint == 'assets':
825
+ assets = get_assets(5)
826
+ result['count'] = len(assets)
827
+ result['sample'] = assets[:2] if assets else []
828
+ return result
829
+
830
+ if endpoint == 'eol':
831
+ url = f"{GATEWAY_URL}/rest/2.0/search/am/asset?pageSize=5"
832
+ token = get_bearer_token()
833
+ result['token_obtained'] = bool(token)
834
+
835
+ filter_body = json.dumps({
836
+ "filters": [
837
+ {"field": "operatingSystem.lifecycle.stage", "operator": "IN", "value": "EOL,EOL/EOS,EOS"}
838
+ ]
839
+ })
840
+ result['request_url'] = url
841
+ result['request_body'] = filter_body
842
+
843
+ req = Request(url, data=filter_body.encode(), method='POST')
844
+ req.add_header('Authorization', f'Bearer {token}' if token else f'Basic {BASIC_AUTH}')
845
+ req.add_header('Content-Type', 'application/json')
846
+ req.add_header('X-Requested-With', 'qualys-mcp')
847
+
848
+ try:
849
+ with urlopen(req, timeout=60) as resp:
850
+ raw = resp.read()
851
+ result['response_code'] = resp.status
852
+ result['response_length'] = len(raw)
853
+ data = json.loads(raw)
854
+ result['has_assetListData'] = 'assetListData' in data
855
+ result['asset_count'] = len(data.get('assetListData', {}).get('asset', []))
856
+ if result['asset_count'] > 0:
857
+ result['sample_asset'] = data['assetListData']['asset'][0]
858
+ except Exception as e:
859
+ result['error'] = str(e)
860
+
861
+ return result
862
+
863
+
798
864
  def main():
799
865
  mcp.run()
800
866
 
@@ -1,6 +0,0 @@
1
- qualys_mcp.py,sha256=u7jd98XTJ0RLTyv2_gHaMN2xDrf8zo1YCWYUl3M2GIk,29645
2
- qualys_mcp-2.1.3.dist-info/METADATA,sha256=wltiuNxoh0_2g9C-VY_GW18El8bqk0Pb4GLBkaONDAc,3295
3
- qualys_mcp-2.1.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
4
- qualys_mcp-2.1.3.dist-info/entry_points.txt,sha256=Dc8X0AhJDjGaZOJ0SNpWDWjEX4sYzrYa9FZEbggX0Rs,47
5
- qualys_mcp-2.1.3.dist-info/licenses/LICENSE,sha256=dW3nC4AX_VbxPAgneSDR-miZPiHgAYw5JhPtdbUEt_E,1091
6
- qualys_mcp-2.1.3.dist-info/RECORD,,