@raghulm/aegis-mcp 1.0.5 → 1.0.9

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,33 +1,33 @@
1
- from __future__ import annotations
2
-
3
- import requests
4
-
5
-
6
- def pipeline_status(base_url: str, pipeline_id: str, api_token: str) -> dict:
7
- """Fetch the status of a CI/CD pipeline by its ID.
8
-
9
- Args:
10
- base_url: Base URL of the CI/CD service API.
11
- pipeline_id: Unique identifier of the pipeline.
12
- api_token: API token for authenticating with the CI/CD service.
13
-
14
- Returns:
15
- Pipeline status as a JSON-compatible dict.
16
- """
17
- try:
18
- response = requests.get(
19
- f"{base_url.rstrip('/')}/pipelines/{pipeline_id}",
20
- headers={"Authorization": f"Bearer {api_token}"},
21
- timeout=15,
22
- )
23
- response.raise_for_status()
24
- except requests.ConnectionError as exc:
25
- raise RuntimeError(f"Cannot connect to CI/CD service at '{base_url}': {exc}") from exc
26
- except requests.HTTPError as exc:
27
- raise RuntimeError(
28
- f"CI/CD API error for pipeline '{pipeline_id}': {exc.response.status_code}"
29
- ) from exc
30
- except requests.Timeout as exc:
31
- raise RuntimeError(f"CI/CD API request timed out for pipeline '{pipeline_id}'") from exc
32
-
33
- return response.json()
1
+ from __future__ import annotations
2
+
3
+ import requests
4
+
5
+
6
+ def pipeline_status(base_url: str, pipeline_id: str, api_token: str) -> dict:
7
+ """Fetch the status of a CI/CD pipeline by its ID.
8
+
9
+ Args:
10
+ base_url: Base URL of the CI/CD service API.
11
+ pipeline_id: Unique identifier of the pipeline.
12
+ api_token: API token for authenticating with the CI/CD service.
13
+
14
+ Returns:
15
+ Pipeline status as a JSON-compatible dict.
16
+ """
17
+ try:
18
+ response = requests.get(
19
+ f"{base_url.rstrip('/')}/pipelines/{pipeline_id}",
20
+ headers={"Authorization": f"Bearer {api_token}"},
21
+ timeout=15,
22
+ )
23
+ response.raise_for_status()
24
+ except requests.ConnectionError as exc:
25
+ raise RuntimeError(f"Cannot connect to CI/CD service at '{base_url}': {exc}") from exc
26
+ except requests.HTTPError as exc:
27
+ raise RuntimeError(
28
+ f"CI/CD API error for pipeline '{pipeline_id}': {exc.response.status_code}"
29
+ ) from exc
30
+ except requests.Timeout as exc:
31
+ raise RuntimeError(f"CI/CD API request timed out for pipeline '{pipeline_id}'") from exc
32
+
33
+ return response.json()
package/tools/git/repo.py CHANGED
@@ -1,22 +1,22 @@
1
- from __future__ import annotations
2
-
3
- import subprocess
4
-
5
-
6
- def get_recent_commits(limit: int = 10) -> list[dict[str, str]]:
7
- try:
8
- raw = subprocess.check_output(
9
- ["git", "log", f"-{limit}", "--pretty=format:%H|%an|%s"],
10
- text=True,
11
- )
12
- except FileNotFoundError as exc:
13
- raise RuntimeError("git is not installed or not on PATH") from exc
14
- except subprocess.CalledProcessError as exc:
15
- raise RuntimeError(f"git log failed: {exc.output}") from exc
16
-
17
- commits = []
18
- for line in raw.splitlines():
19
- parts = line.split("|", maxsplit=2)
20
- if len(parts) == 3:
21
- commits.append({"hash": parts[0], "author": parts[1], "subject": parts[2]})
22
- return commits
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+
5
+
6
+ def get_recent_commits(limit: int = 10) -> list[dict[str, str]]:
7
+ try:
8
+ raw = subprocess.check_output(
9
+ ["git", "log", f"-{limit}", "--pretty=format:%H|%an|%s"],
10
+ text=True,
11
+ )
12
+ except FileNotFoundError as exc:
13
+ raise RuntimeError("git is not installed or not on PATH") from exc
14
+ except subprocess.CalledProcessError as exc:
15
+ raise RuntimeError(f"git log failed: {exc.output}") from exc
16
+
17
+ commits = []
18
+ for line in raw.splitlines():
19
+ parts = line.split("|", maxsplit=2)
20
+ if len(parts) == 3:
21
+ commits.append({"hash": parts[0], "author": parts[1], "subject": parts[2]})
22
+ return commits
@@ -1,108 +1,108 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import subprocess
5
- from typing import Any
6
-
7
-
8
- def k8s_security_audit(namespace: str = "") -> list[dict[str, Any]]:
9
- findings = []
10
-
11
- cmd_base = ["kubectl", "get"]
12
- if namespace:
13
- ns_args = ["-n", namespace]
14
- else:
15
- ns_args = ["-A"]
16
-
17
- try:
18
- pods_result = subprocess.check_output(
19
- cmd_base + ["pods"] + ns_args + ["-o", "json"],
20
- stderr=subprocess.STDOUT,
21
- text=True,
22
- )
23
- pods_payload = json.loads(pods_result)
24
- except Exception as exc:
25
- print(f"Warning: Failed to get pods: {exc}")
26
- pods_payload = {"items": []}
27
-
28
- try:
29
- services_result = subprocess.check_output(
30
- cmd_base + ["svc"] + ns_args + ["-o", "json"],
31
- stderr=subprocess.STDOUT,
32
- text=True,
33
- )
34
- svcs_payload = json.loads(services_result)
35
- except Exception as exc:
36
- print(f"Warning: Failed to get services: {exc}")
37
- svcs_payload = {"items": []}
38
-
39
- try:
40
- roles_result = subprocess.check_output(
41
- cmd_base + ["clusterrolebindings", "-o", "json"],
42
- stderr=subprocess.STDOUT,
43
- text=True,
44
- )
45
- crb_payload = json.loads(roles_result)
46
- except Exception as exc:
47
- print(f"Warning: Failed to get clusterrolebindings: {exc}")
48
- crb_payload = {"items": []}
49
-
50
- # Parse pods
51
- for pod in pods_payload.get("items", []):
52
- metadata = pod.get("metadata", {})
53
- pod_name = metadata.get("name", "unknown")
54
- pod_ns = metadata.get("namespace", "unknown")
55
- spec = pod.get("spec", {})
56
-
57
- # Check hostNetwork
58
- if spec.get("hostNetwork") is True:
59
- findings.append({
60
- "type": "hostNetwork",
61
- "severity": "HIGH",
62
- "resource": f"Pod/{pod_ns}/{pod_name}",
63
- "message": "Pod is using host network."
64
- })
65
-
66
- # Check privileged containers
67
- for container in spec.get("containers", []):
68
- sec_ctx = container.get("securityContext", {})
69
- if sec_ctx.get("privileged") is True:
70
- findings.append({
71
- "type": "privileged_container",
72
- "severity": "CRITICAL",
73
- "resource": f"Pod/{pod_ns}/{pod_name}",
74
- "message": f"Container '{container.get('name')}' is running as privileged.",
75
- })
76
-
77
- # Parse services
78
- for svc in svcs_payload.get("items", []):
79
- metadata = svc.get("metadata", {})
80
- svc_name = metadata.get("name", "unknown")
81
- svc_ns = metadata.get("namespace", "unknown")
82
- spec = svc.get("spec", {})
83
-
84
- if spec.get("type") == "NodePort":
85
- findings.append({
86
- "type": "exposed_nodeport",
87
- "severity": "MEDIUM",
88
- "resource": f"Service/{svc_ns}/{svc_name}",
89
- "message": "Service is exposed via NodePort."
90
- })
91
-
92
- # Parse cluster role bindings
93
- for crb in crb_payload.get("items", []):
94
- metadata = crb.get("metadata", {})
95
- crb_name = metadata.get("name", "unknown")
96
- role_ref = crb.get("roleRef", {})
97
-
98
- if role_ref.get("name") == "cluster-admin":
99
- for subj in crb.get("subjects", []):
100
- if subj.get("kind") == "ServiceAccount":
101
- findings.append({
102
- "type": "cluster_admin_sa",
103
- "severity": "CRITICAL",
104
- "resource": f"ClusterRoleBinding/{crb_name}",
105
- "message": f"ServiceAccount '{subj.get('namespace')}/{subj.get('name')}' is bound to cluster-admin."
106
- })
107
-
108
- return findings
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+ from typing import Any
6
+
7
+
8
+ def k8s_security_audit(namespace: str = "") -> list[dict[str, Any]]:
9
+ findings = []
10
+
11
+ cmd_base = ["kubectl", "get"]
12
+ if namespace:
13
+ ns_args = ["-n", namespace]
14
+ else:
15
+ ns_args = ["-A"]
16
+
17
+ try:
18
+ pods_result = subprocess.check_output(
19
+ cmd_base + ["pods"] + ns_args + ["-o", "json"],
20
+ stderr=subprocess.STDOUT,
21
+ text=True,
22
+ )
23
+ pods_payload = json.loads(pods_result)
24
+ except Exception as exc:
25
+ print(f"Warning: Failed to get pods: {exc}")
26
+ pods_payload = {"items": []}
27
+
28
+ try:
29
+ services_result = subprocess.check_output(
30
+ cmd_base + ["svc"] + ns_args + ["-o", "json"],
31
+ stderr=subprocess.STDOUT,
32
+ text=True,
33
+ )
34
+ svcs_payload = json.loads(services_result)
35
+ except Exception as exc:
36
+ print(f"Warning: Failed to get services: {exc}")
37
+ svcs_payload = {"items": []}
38
+
39
+ try:
40
+ roles_result = subprocess.check_output(
41
+ cmd_base + ["clusterrolebindings", "-o", "json"],
42
+ stderr=subprocess.STDOUT,
43
+ text=True,
44
+ )
45
+ crb_payload = json.loads(roles_result)
46
+ except Exception as exc:
47
+ print(f"Warning: Failed to get clusterrolebindings: {exc}")
48
+ crb_payload = {"items": []}
49
+
50
+ # Parse pods
51
+ for pod in pods_payload.get("items", []):
52
+ metadata = pod.get("metadata", {})
53
+ pod_name = metadata.get("name", "unknown")
54
+ pod_ns = metadata.get("namespace", "unknown")
55
+ spec = pod.get("spec", {})
56
+
57
+ # Check hostNetwork
58
+ if spec.get("hostNetwork") is True:
59
+ findings.append({
60
+ "type": "hostNetwork",
61
+ "severity": "HIGH",
62
+ "resource": f"Pod/{pod_ns}/{pod_name}",
63
+ "message": "Pod is using host network."
64
+ })
65
+
66
+ # Check privileged containers
67
+ for container in spec.get("containers", []):
68
+ sec_ctx = container.get("securityContext", {})
69
+ if sec_ctx.get("privileged") is True:
70
+ findings.append({
71
+ "type": "privileged_container",
72
+ "severity": "CRITICAL",
73
+ "resource": f"Pod/{pod_ns}/{pod_name}",
74
+ "message": f"Container '{container.get('name')}' is running as privileged.",
75
+ })
76
+
77
+ # Parse services
78
+ for svc in svcs_payload.get("items", []):
79
+ metadata = svc.get("metadata", {})
80
+ svc_name = metadata.get("name", "unknown")
81
+ svc_ns = metadata.get("namespace", "unknown")
82
+ spec = svc.get("spec", {})
83
+
84
+ if spec.get("type") == "NodePort":
85
+ findings.append({
86
+ "type": "exposed_nodeport",
87
+ "severity": "MEDIUM",
88
+ "resource": f"Service/{svc_ns}/{svc_name}",
89
+ "message": "Service is exposed via NodePort."
90
+ })
91
+
92
+ # Parse cluster role bindings
93
+ for crb in crb_payload.get("items", []):
94
+ metadata = crb.get("metadata", {})
95
+ crb_name = metadata.get("name", "unknown")
96
+ role_ref = crb.get("roleRef", {})
97
+
98
+ if role_ref.get("name") == "cluster-admin":
99
+ for subj in crb.get("subjects", []):
100
+ if subj.get("kind") == "ServiceAccount":
101
+ findings.append({
102
+ "type": "cluster_admin_sa",
103
+ "severity": "CRITICAL",
104
+ "resource": f"ClusterRoleBinding/{crb_name}",
105
+ "message": f"ServiceAccount '{subj.get('namespace')}/{subj.get('name')}' is bound to cluster-admin."
106
+ })
107
+
108
+ return findings
@@ -1,27 +1,27 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import subprocess
5
- from typing import Any
6
-
7
-
8
- def list_pods(namespace: str = "default") -> list[dict[str, Any]]:
9
- try:
10
- result = subprocess.check_output(
11
- ["kubectl", "get", "pods", "-n", namespace, "-o", "json"],
12
- stderr=subprocess.STDOUT,
13
- text=True,
14
- )
15
- except FileNotFoundError as exc:
16
- raise RuntimeError("kubectl is not installed or not on PATH") from exc
17
- except subprocess.CalledProcessError as exc:
18
- raise RuntimeError(f"kubectl error (namespace '{namespace}'): {exc.output}") from exc
19
-
20
- payload = json.loads(result)
21
- return [
22
- {
23
- "name": item["metadata"]["name"],
24
- "phase": item["status"].get("phase"),
25
- }
26
- for item in payload.get("items", [])
27
- ]
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+ from typing import Any
6
+
7
+
8
+ def list_pods(namespace: str = "default") -> list[dict[str, Any]]:
9
+ try:
10
+ result = subprocess.check_output(
11
+ ["kubectl", "get", "pods", "-n", namespace, "-o", "json"],
12
+ stderr=subprocess.STDOUT,
13
+ text=True,
14
+ )
15
+ except FileNotFoundError as exc:
16
+ raise RuntimeError("kubectl is not installed or not on PATH") from exc
17
+ except subprocess.CalledProcessError as exc:
18
+ raise RuntimeError(f"kubectl error (namespace '{namespace}'): {exc.output}") from exc
19
+
20
+ payload = json.loads(result)
21
+ return [
22
+ {
23
+ "name": item["metadata"]["name"],
24
+ "phase": item["status"].get("phase"),
25
+ }
26
+ for item in payload.get("items", [])
27
+ ]
@@ -1,99 +1,99 @@
1
- from __future__ import annotations
2
-
3
- from typing import Any
4
-
5
- import requests
6
-
7
- _SECURITY_HEADERS = {
8
- "Strict-Transport-Security": {
9
- "description": "Enforces HTTPS connections",
10
- "recommended": True,
11
- },
12
- "Content-Security-Policy": {
13
- "description": "Controls resources the browser is allowed to load",
14
- "recommended": True,
15
- },
16
- "X-Content-Type-Options": {
17
- "description": "Prevents MIME-type sniffing",
18
- "recommended": True,
19
- "expected_value": "nosniff",
20
- },
21
- "X-Frame-Options": {
22
- "description": "Controls whether the page can be embedded in iframes",
23
- "recommended": True,
24
- },
25
- "Referrer-Policy": {
26
- "description": "Controls how much referrer information is sent",
27
- "recommended": True,
28
- },
29
- "Permissions-Policy": {
30
- "description": "Controls browser features available to the page",
31
- "recommended": True,
32
- },
33
- "X-XSS-Protection": {
34
- "description": "Legacy XSS filter (mostly deprecated but still checked)",
35
- "recommended": False,
36
- },
37
- "Cache-Control": {
38
- "description": "Controls caching behavior for sensitive data",
39
- "recommended": True,
40
- },
41
- }
42
-
43
-
44
- def check_http_headers(url: str) -> dict[str, Any]:
45
- """Audit HTTP security headers for a given URL.
46
-
47
- Args:
48
- url: The target URL to check (must include scheme, e.g. https://example.com).
49
-
50
- Returns:
51
- Dict with header analysis, score, and recommendations.
52
- """
53
- if not url.startswith(("http://", "https://")):
54
- url = "https://" + url
55
-
56
- try:
57
- resp = requests.head(url, timeout=10, allow_redirects=True)
58
- except requests.ConnectionError as exc:
59
- raise RuntimeError(f"Cannot connect to '{url}': {exc}") from exc
60
- except requests.Timeout as exc:
61
- raise RuntimeError(f"Request to '{url}' timed out") from exc
62
-
63
- headers_lower = {k.lower(): v for k, v in resp.headers.items()}
64
- total_recommended = sum(1 for h in _SECURITY_HEADERS.values() if h["recommended"])
65
- present_recommended = 0
66
-
67
- results: list[dict[str, Any]] = []
68
- for header_name, info in _SECURITY_HEADERS.items():
69
- value = headers_lower.get(header_name.lower())
70
- is_present = value is not None
71
- if is_present and info["recommended"]:
72
- present_recommended += 1
73
-
74
- entry: dict[str, Any] = {
75
- "header": header_name,
76
- "present": is_present,
77
- "value": value or "",
78
- "description": info["description"],
79
- }
80
-
81
- expected = info.get("expected_value")
82
- if expected and is_present and value != expected:
83
- entry["warning"] = f"Expected '{expected}', got '{value}'"
84
-
85
- results.append(entry)
86
-
87
- score = round((present_recommended / total_recommended) * 100) if total_recommended else 0
88
- grade = "A" if score >= 85 else "B" if score >= 70 else "C" if score >= 50 else "D" if score >= 30 else "F"
89
-
90
- missing = [r["header"] for r in results if not r["present"] and _SECURITY_HEADERS[r["header"]]["recommended"]]
91
-
92
- return {
93
- "url": url,
94
- "status_code": resp.status_code,
95
- "score": score,
96
- "grade": grade,
97
- "headers": results,
98
- "missing_recommended": missing,
99
- }
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import requests
6
+
7
+ _SECURITY_HEADERS = {
8
+ "Strict-Transport-Security": {
9
+ "description": "Enforces HTTPS connections",
10
+ "recommended": True,
11
+ },
12
+ "Content-Security-Policy": {
13
+ "description": "Controls resources the browser is allowed to load",
14
+ "recommended": True,
15
+ },
16
+ "X-Content-Type-Options": {
17
+ "description": "Prevents MIME-type sniffing",
18
+ "recommended": True,
19
+ "expected_value": "nosniff",
20
+ },
21
+ "X-Frame-Options": {
22
+ "description": "Controls whether the page can be embedded in iframes",
23
+ "recommended": True,
24
+ },
25
+ "Referrer-Policy": {
26
+ "description": "Controls how much referrer information is sent",
27
+ "recommended": True,
28
+ },
29
+ "Permissions-Policy": {
30
+ "description": "Controls browser features available to the page",
31
+ "recommended": True,
32
+ },
33
+ "X-XSS-Protection": {
34
+ "description": "Legacy XSS filter (mostly deprecated but still checked)",
35
+ "recommended": False,
36
+ },
37
+ "Cache-Control": {
38
+ "description": "Controls caching behavior for sensitive data",
39
+ "recommended": True,
40
+ },
41
+ }
42
+
43
+
44
+ def check_http_headers(url: str) -> dict[str, Any]:
45
+ """Audit HTTP security headers for a given URL.
46
+
47
+ Args:
48
+ url: The target URL to check (must include scheme, e.g. https://example.com).
49
+
50
+ Returns:
51
+ Dict with header analysis, score, and recommendations.
52
+ """
53
+ if not url.startswith(("http://", "https://")):
54
+ url = "https://" + url
55
+
56
+ try:
57
+ resp = requests.head(url, timeout=10, allow_redirects=True)
58
+ except requests.ConnectionError as exc:
59
+ raise RuntimeError(f"Cannot connect to '{url}': {exc}") from exc
60
+ except requests.Timeout as exc:
61
+ raise RuntimeError(f"Request to '{url}' timed out") from exc
62
+
63
+ headers_lower = {k.lower(): v for k, v in resp.headers.items()}
64
+ total_recommended = sum(1 for h in _SECURITY_HEADERS.values() if h["recommended"])
65
+ present_recommended = 0
66
+
67
+ results: list[dict[str, Any]] = []
68
+ for header_name, info in _SECURITY_HEADERS.items():
69
+ value = headers_lower.get(header_name.lower())
70
+ is_present = value is not None
71
+ if is_present and info["recommended"]:
72
+ present_recommended += 1
73
+
74
+ entry: dict[str, Any] = {
75
+ "header": header_name,
76
+ "present": is_present,
77
+ "value": value or "",
78
+ "description": info["description"],
79
+ }
80
+
81
+ expected = info.get("expected_value")
82
+ if expected and is_present and value != expected:
83
+ entry["warning"] = f"Expected '{expected}', got '{value}'"
84
+
85
+ results.append(entry)
86
+
87
+ score = round((present_recommended / total_recommended) * 100) if total_recommended else 0
88
+ grade = "A" if score >= 85 else "B" if score >= 70 else "C" if score >= 50 else "D" if score >= 30 else "F"
89
+
90
+ missing = [r["header"] for r in results if not r["present"] and _SECURITY_HEADERS[r["header"]]["recommended"]]
91
+
92
+ return {
93
+ "url": url,
94
+ "status_code": resp.status_code,
95
+ "score": score,
96
+ "grade": grade,
97
+ "headers": results,
98
+ "missing_recommended": missing,
99
+ }