@raghulm/aegis-mcp 1.0.2 → 1.0.3

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.
@@ -0,0 +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
File without changes
@@ -0,0 +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
@@ -0,0 +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
+ ]
File without changes
@@ -0,0 +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
+ }
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ import socket
4
+ from typing import Any
5
+
6
+ _COMMON_PORTS: dict[int, str] = {
7
+ 21: "FTP",
8
+ 22: "SSH",
9
+ 23: "Telnet",
10
+ 25: "SMTP",
11
+ 53: "DNS",
12
+ 80: "HTTP",
13
+ 110: "POP3",
14
+ 143: "IMAP",
15
+ 443: "HTTPS",
16
+ 445: "SMB",
17
+ 993: "IMAPS",
18
+ 995: "POP3S",
19
+ 3306: "MySQL",
20
+ 3389: "RDP",
21
+ 5432: "PostgreSQL",
22
+ 6379: "Redis",
23
+ 8080: "HTTP-Alt",
24
+ 8443: "HTTPS-Alt",
25
+ 27017: "MongoDB",
26
+ }
27
+
28
+
29
+ def port_scan(host: str, ports: str = "") -> list[dict[str, Any]]:
30
+ """Perform a basic TCP connect scan on a host.
31
+
32
+ Args:
33
+ host: Target hostname or IP address.
34
+ ports: Comma-separated port numbers. If empty, scans common service ports.
35
+
36
+ Returns:
37
+ List of port results with port number, status, and service name.
38
+ """
39
+ try:
40
+ resolved_ip = socket.gethostbyname(host)
41
+ except socket.gaierror as exc:
42
+ raise RuntimeError(f"Cannot resolve hostname '{host}': {exc}") from exc
43
+
44
+ if ports:
45
+ try:
46
+ port_list = [int(p.strip()) for p in ports.split(",") if p.strip()]
47
+ except ValueError as exc:
48
+ raise RuntimeError(f"Invalid port specification: {exc}") from exc
49
+ else:
50
+ port_list = list(_COMMON_PORTS.keys())
51
+
52
+ results: list[dict[str, Any]] = []
53
+ for port in sorted(port_list):
54
+ try:
55
+ with socket.create_connection((host, port), timeout=2):
56
+ status = "open"
57
+ except (socket.timeout, ConnectionRefusedError, OSError):
58
+ status = "closed"
59
+
60
+ results.append({
61
+ "port": port,
62
+ "status": status,
63
+ "service": _COMMON_PORTS.get(port, "unknown"),
64
+ })
65
+
66
+ return results
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ import socket
4
+ import ssl
5
+ from datetime import datetime, timezone
6
+ from typing import Any
7
+
8
+
9
+ def check_ssl_certificate(hostname: str, port: int = 443) -> dict[str, Any]:
10
+ """Check the SSL/TLS certificate for a given hostname.
11
+
12
+ Args:
13
+ hostname: Domain name to check.
14
+ port: TCP port (default 443).
15
+
16
+ Returns:
17
+ Certificate details including subject, issuer, validity, and expiry info.
18
+ """
19
+ context = ssl.create_default_context()
20
+ try:
21
+ with socket.create_connection((hostname, port), timeout=10) as sock:
22
+ with context.wrap_socket(sock, server_hostname=hostname) as ssock:
23
+ cert = ssock.getpeercert()
24
+ except socket.gaierror as exc:
25
+ raise RuntimeError(f"DNS resolution failed for '{hostname}': {exc}") from exc
26
+ except socket.timeout as exc:
27
+ raise RuntimeError(f"Connection to '{hostname}:{port}' timed out") from exc
28
+ except ssl.SSLError as exc:
29
+ raise RuntimeError(f"SSL error for '{hostname}:{port}': {exc}") from exc
30
+ except OSError as exc:
31
+ raise RuntimeError(f"Connection failed to '{hostname}:{port}': {exc}") from exc
32
+
33
+ if not cert:
34
+ raise RuntimeError(f"No certificate returned by '{hostname}:{port}'")
35
+
36
+ def _format_name(name_tuples: tuple) -> str:
37
+ parts = []
38
+ for attr_group in name_tuples:
39
+ for key, value in attr_group:
40
+ parts.append(f"{key}={value}")
41
+ return ", ".join(parts)
42
+
43
+ not_before = datetime.strptime(cert["notBefore"], "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc)
44
+ not_after = datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc)
45
+ now = datetime.now(tz=timezone.utc)
46
+ days_until_expiry = (not_after - now).days
47
+
48
+ san_entries = []
49
+ for san_type, san_value in cert.get("subjectAltName", ()):
50
+ san_entries.append(f"{san_type}:{san_value}")
51
+
52
+ return {
53
+ "hostname": hostname,
54
+ "port": port,
55
+ "subject": _format_name(cert.get("subject", ())),
56
+ "issuer": _format_name(cert.get("issuer", ())),
57
+ "serial_number": cert.get("serialNumber", ""),
58
+ "version": cert.get("version", ""),
59
+ "not_before": not_before.isoformat(),
60
+ "not_after": not_after.isoformat(),
61
+ "days_until_expiry": days_until_expiry,
62
+ "expired": days_until_expiry < 0,
63
+ "subject_alt_names": san_entries,
64
+ "protocol": "TLSv1.2+",
65
+ }
File without changes
@@ -0,0 +1,103 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ from typing import Any
7
+
8
+ import requests
9
+
10
+
11
+ def _parse_requirements_txt(file_path: str) -> list[dict[str, str]]:
12
+ packages: list[dict[str, str]] = []
13
+ with open(file_path, "r", encoding="utf-8") as fh:
14
+ for line in fh:
15
+ line = line.strip()
16
+ if not line or line.startswith("#") or line.startswith("-"):
17
+ continue
18
+ match = re.match(r"^([A-Za-z0-9_\-\.]+)\s*(?:[=<>!~]+\s*(.+))?", line)
19
+ if match:
20
+ name = match.group(1)
21
+ version = match.group(2).strip() if match.group(2) else ""
22
+ packages.append({"name": name, "version": version})
23
+ return packages
24
+
25
+
26
+ def _parse_package_json(file_path: str) -> list[dict[str, str]]:
27
+ with open(file_path, "r", encoding="utf-8") as fh:
28
+ data = json.load(fh)
29
+ packages: list[dict[str, str]] = []
30
+ for dep_key in ("dependencies", "devDependencies"):
31
+ for name, version in data.get(dep_key, {}).items():
32
+ clean_version = re.sub(r"^[\^~>=<]", "", version)
33
+ packages.append({"name": name, "version": clean_version})
34
+ return packages
35
+
36
+
37
+ def _query_osv(ecosystem: str, package_name: str, version: str) -> list[dict[str, Any]]:
38
+ """Query the OSV.dev API for known vulnerabilities."""
39
+ payload: dict[str, Any] = {
40
+ "package": {"name": package_name, "ecosystem": ecosystem},
41
+ }
42
+ if version:
43
+ payload["version"] = version
44
+
45
+ try:
46
+ resp = requests.post(
47
+ "https://api.osv.dev/v1/query",
48
+ json=payload,
49
+ timeout=10,
50
+ )
51
+ resp.raise_for_status()
52
+ except requests.RequestException:
53
+ return []
54
+
55
+ vulns = resp.json().get("vulns", [])
56
+ results: list[dict[str, Any]] = []
57
+ for vuln in vulns:
58
+ aliases = vuln.get("aliases", [])
59
+ cve = next((a for a in aliases if a.startswith("CVE-")), aliases[0] if aliases else vuln.get("id", ""))
60
+ severity_list = vuln.get("severity", [])
61
+ severity = severity_list[0].get("score", "unknown") if severity_list else "unknown"
62
+ results.append({
63
+ "id": vuln.get("id", ""),
64
+ "cve": cve,
65
+ "summary": vuln.get("summary", "No summary available"),
66
+ "severity": severity,
67
+ })
68
+ return results
69
+
70
+
71
+ def check_dependencies(file_path: str) -> list[dict[str, Any]]:
72
+ """Scan a dependency file for known vulnerabilities via OSV.dev.
73
+
74
+ Args:
75
+ file_path: Path to requirements.txt or package.json.
76
+
77
+ Returns:
78
+ List of packages with their vulnerability status.
79
+ """
80
+ if not os.path.exists(file_path):
81
+ raise RuntimeError(f"File does not exist: {file_path}")
82
+
83
+ basename = os.path.basename(file_path).lower()
84
+ if basename == "requirements.txt":
85
+ packages = _parse_requirements_txt(file_path)
86
+ ecosystem = "PyPI"
87
+ elif basename == "package.json":
88
+ packages = _parse_package_json(file_path)
89
+ ecosystem = "npm"
90
+ else:
91
+ raise RuntimeError(f"Unsupported dependency file: {basename}. Use requirements.txt or package.json.")
92
+
93
+ results: list[dict[str, Any]] = []
94
+ for pkg in packages:
95
+ vulns = _query_osv(ecosystem, pkg["name"], pkg["version"])
96
+ results.append({
97
+ "package": pkg["name"],
98
+ "version": pkg["version"] or "unspecified",
99
+ "vulnerabilities_found": len(vulns),
100
+ "vulnerabilities": vulns,
101
+ })
102
+
103
+ return results
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ from typing import Any
6
+
7
+ # Common secret patterns with descriptive names
8
+ _PATTERNS: list[tuple[str, re.Pattern[str]]] = [
9
+ ("AWS Access Key", re.compile(r"(?:^|[^A-Z0-9])(?:AKIA[0-9A-Z]{16})(?:[^A-Z0-9]|$)")),
10
+ ("AWS Secret Key", re.compile(
11
+ r"""(?:aws_secret_access_key|secret_access_key|aws_secret)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?""",
12
+ re.IGNORECASE,
13
+ )),
14
+ ("Generic API Key", re.compile(
15
+ r"""(?:api[_-]?key|apikey|api[_-]?secret)\s*[=:]\s*['"]?([A-Za-z0-9_\-]{20,60})['"]?""",
16
+ re.IGNORECASE,
17
+ )),
18
+ ("Generic Token", re.compile(
19
+ r"""(?:token|auth[_-]?token|access[_-]?token|bearer)\s*[=:]\s*['"]?([A-Za-z0-9_\-\.]{20,200})['"]?""",
20
+ re.IGNORECASE,
21
+ )),
22
+ ("Generic Password", re.compile(
23
+ r"""(?:password|passwd|pwd|secret)\s*[=:]\s*['"]?([^\s'"]{8,})['"]?""",
24
+ re.IGNORECASE,
25
+ )),
26
+ ("Private Key", re.compile(r"-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----")),
27
+ ("GitHub Token", re.compile(r"(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}")),
28
+ ("Slack Token", re.compile(r"xox[bporas]-[A-Za-z0-9\-]{10,}")),
29
+ ("Stripe Key", re.compile(r"(?:sk|pk)_(?:test|live)_[A-Za-z0-9]{20,}")),
30
+ ("SendGrid Key", re.compile(r"SG\.[A-Za-z0-9_\-]{22,}\.[A-Za-z0-9_\-]{43,}")),
31
+ ]
32
+
33
+ _SKIP_DIRS = {".git", "__pycache__", "node_modules", ".venv", "venv", ".env", ".tox", "dist", "build"}
34
+ _SKIP_EXTENSIONS = {".pyc", ".pyo", ".so", ".dll", ".exe", ".bin", ".jpg", ".png", ".gif", ".ico", ".woff", ".ttf"}
35
+ _MAX_FILE_SIZE = 1_048_576 # 1 MB
36
+
37
+
38
+ def _redact(match_text: str, keep: int = 6) -> str:
39
+ if len(match_text) <= keep:
40
+ return "***REDACTED***"
41
+ return match_text[:keep] + "***REDACTED***"
42
+
43
+
44
+ def _scan_file(file_path: str) -> list[dict[str, Any]]:
45
+ findings: list[dict[str, Any]] = []
46
+ try:
47
+ if os.path.getsize(file_path) > _MAX_FILE_SIZE:
48
+ return findings
49
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as fh:
50
+ for line_no, line in enumerate(fh, start=1):
51
+ for pattern_name, pattern in _PATTERNS:
52
+ match = pattern.search(line)
53
+ if match:
54
+ matched_text = match.group(0).strip()
55
+ findings.append({
56
+ "file": file_path,
57
+ "line": line_no,
58
+ "pattern": pattern_name,
59
+ "match": _redact(matched_text),
60
+ })
61
+ except (OSError, UnicodeDecodeError):
62
+ pass
63
+ return findings
64
+
65
+
66
+ def scan_secrets(path: str) -> list[dict[str, Any]]:
67
+ """Scan a file or directory for exposed secrets using regex patterns.
68
+
69
+ Args:
70
+ path: Path to a file or directory to scan.
71
+
72
+ Returns:
73
+ List of findings, each with file, line, pattern name, and redacted match.
74
+ """
75
+ if not os.path.exists(path):
76
+ raise RuntimeError(f"Path does not exist: {path}")
77
+
78
+ findings: list[dict[str, Any]] = []
79
+ if os.path.isfile(path):
80
+ return _scan_file(path)
81
+
82
+ for root, dirs, files in os.walk(path):
83
+ dirs[:] = [d for d in dirs if d not in _SKIP_DIRS]
84
+ for fname in files:
85
+ ext = os.path.splitext(fname)[1].lower()
86
+ if ext in _SKIP_EXTENSIONS:
87
+ continue
88
+ full_path = os.path.join(root, fname)
89
+ findings.extend(_scan_file(full_path))
90
+
91
+ return findings