osscodeiq 0.0.0__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.
- osscodeiq/__init__.py +0 -0
- osscodeiq/analyzer.py +467 -0
- osscodeiq/cache/__init__.py +0 -0
- osscodeiq/cache/hasher.py +23 -0
- osscodeiq/cache/store.py +300 -0
- osscodeiq/classifiers/__init__.py +0 -0
- osscodeiq/classifiers/layer_classifier.py +69 -0
- osscodeiq/cli.py +721 -0
- osscodeiq/config.py +113 -0
- osscodeiq/detectors/__init__.py +0 -0
- osscodeiq/detectors/auth/__init__.py +0 -0
- osscodeiq/detectors/auth/certificate_auth.py +139 -0
- osscodeiq/detectors/auth/ldap_auth.py +89 -0
- osscodeiq/detectors/auth/session_header_auth.py +120 -0
- osscodeiq/detectors/base.py +41 -0
- osscodeiq/detectors/config/__init__.py +0 -0
- osscodeiq/detectors/config/batch_structure.py +128 -0
- osscodeiq/detectors/config/cloudformation.py +183 -0
- osscodeiq/detectors/config/docker_compose.py +179 -0
- osscodeiq/detectors/config/github_actions.py +150 -0
- osscodeiq/detectors/config/gitlab_ci.py +216 -0
- osscodeiq/detectors/config/helm_chart.py +187 -0
- osscodeiq/detectors/config/ini_structure.py +101 -0
- osscodeiq/detectors/config/json_structure.py +72 -0
- osscodeiq/detectors/config/kubernetes.py +305 -0
- osscodeiq/detectors/config/kubernetes_rbac.py +212 -0
- osscodeiq/detectors/config/openapi.py +194 -0
- osscodeiq/detectors/config/package_json.py +99 -0
- osscodeiq/detectors/config/properties_detector.py +108 -0
- osscodeiq/detectors/config/pyproject_toml.py +169 -0
- osscodeiq/detectors/config/sql_structure.py +155 -0
- osscodeiq/detectors/config/toml_structure.py +93 -0
- osscodeiq/detectors/config/tsconfig_json.py +105 -0
- osscodeiq/detectors/config/yaml_structure.py +82 -0
- osscodeiq/detectors/cpp/__init__.py +0 -0
- osscodeiq/detectors/cpp/cpp_structures.py +192 -0
- osscodeiq/detectors/csharp/__init__.py +0 -0
- osscodeiq/detectors/csharp/csharp_efcore.py +184 -0
- osscodeiq/detectors/csharp/csharp_minimal_apis.py +156 -0
- osscodeiq/detectors/csharp/csharp_structures.py +317 -0
- osscodeiq/detectors/docs/__init__.py +0 -0
- osscodeiq/detectors/docs/markdown_structure.py +117 -0
- osscodeiq/detectors/frontend/__init__.py +0 -0
- osscodeiq/detectors/frontend/angular_components.py +177 -0
- osscodeiq/detectors/frontend/frontend_routes.py +259 -0
- osscodeiq/detectors/frontend/react_components.py +148 -0
- osscodeiq/detectors/frontend/svelte_components.py +84 -0
- osscodeiq/detectors/frontend/vue_components.py +150 -0
- osscodeiq/detectors/generic/__init__.py +1 -0
- osscodeiq/detectors/generic/imports_detector.py +413 -0
- osscodeiq/detectors/go/__init__.py +0 -0
- osscodeiq/detectors/go/go_orm.py +202 -0
- osscodeiq/detectors/go/go_structures.py +162 -0
- osscodeiq/detectors/go/go_web.py +157 -0
- osscodeiq/detectors/iac/__init__.py +0 -0
- osscodeiq/detectors/iac/bicep.py +135 -0
- osscodeiq/detectors/iac/dockerfile.py +182 -0
- osscodeiq/detectors/iac/terraform.py +188 -0
- osscodeiq/detectors/java/__init__.py +0 -0
- osscodeiq/detectors/java/azure_functions.py +424 -0
- osscodeiq/detectors/java/azure_messaging.py +350 -0
- osscodeiq/detectors/java/class_hierarchy.py +349 -0
- osscodeiq/detectors/java/config_def.py +82 -0
- osscodeiq/detectors/java/cosmos_db.py +105 -0
- osscodeiq/detectors/java/graphql_resolver.py +188 -0
- osscodeiq/detectors/java/grpc_service.py +142 -0
- osscodeiq/detectors/java/ibm_mq.py +178 -0
- osscodeiq/detectors/java/jaxrs.py +160 -0
- osscodeiq/detectors/java/jdbc.py +196 -0
- osscodeiq/detectors/java/jms.py +116 -0
- osscodeiq/detectors/java/jpa_entity.py +143 -0
- osscodeiq/detectors/java/kafka.py +113 -0
- osscodeiq/detectors/java/kafka_protocol.py +70 -0
- osscodeiq/detectors/java/micronaut.py +248 -0
- osscodeiq/detectors/java/module_deps.py +191 -0
- osscodeiq/detectors/java/public_api.py +206 -0
- osscodeiq/detectors/java/quarkus.py +176 -0
- osscodeiq/detectors/java/rabbitmq.py +150 -0
- osscodeiq/detectors/java/raw_sql.py +136 -0
- osscodeiq/detectors/java/repository.py +131 -0
- osscodeiq/detectors/java/rmi.py +129 -0
- osscodeiq/detectors/java/spring_events.py +117 -0
- osscodeiq/detectors/java/spring_rest.py +168 -0
- osscodeiq/detectors/java/spring_security.py +212 -0
- osscodeiq/detectors/java/tibco_ems.py +193 -0
- osscodeiq/detectors/java/websocket.py +188 -0
- osscodeiq/detectors/kotlin/__init__.py +0 -0
- osscodeiq/detectors/kotlin/kotlin_structures.py +124 -0
- osscodeiq/detectors/kotlin/ktor_routes.py +163 -0
- osscodeiq/detectors/proto/__init__.py +0 -0
- osscodeiq/detectors/proto/proto_structure.py +153 -0
- osscodeiq/detectors/python/__init__.py +0 -0
- osscodeiq/detectors/python/celery_tasks.py +88 -0
- osscodeiq/detectors/python/django_auth.py +132 -0
- osscodeiq/detectors/python/django_models.py +157 -0
- osscodeiq/detectors/python/django_views.py +74 -0
- osscodeiq/detectors/python/fastapi_auth.py +143 -0
- osscodeiq/detectors/python/fastapi_routes.py +68 -0
- osscodeiq/detectors/python/flask_routes.py +67 -0
- osscodeiq/detectors/python/kafka_python.py +175 -0
- osscodeiq/detectors/python/pydantic_models.py +115 -0
- osscodeiq/detectors/python/python_structures.py +234 -0
- osscodeiq/detectors/python/sqlalchemy_models.py +82 -0
- osscodeiq/detectors/registry.py +100 -0
- osscodeiq/detectors/rust/__init__.py +0 -0
- osscodeiq/detectors/rust/actix_web.py +234 -0
- osscodeiq/detectors/rust/rust_structures.py +174 -0
- osscodeiq/detectors/scala/__init__.py +0 -0
- osscodeiq/detectors/scala/scala_structures.py +128 -0
- osscodeiq/detectors/shell/__init__.py +0 -0
- osscodeiq/detectors/shell/bash_detector.py +127 -0
- osscodeiq/detectors/shell/powershell_detector.py +118 -0
- osscodeiq/detectors/typescript/__init__.py +0 -0
- osscodeiq/detectors/typescript/express_routes.py +55 -0
- osscodeiq/detectors/typescript/fastify_routes.py +156 -0
- osscodeiq/detectors/typescript/graphql_resolvers.py +100 -0
- osscodeiq/detectors/typescript/kafka_js.py +164 -0
- osscodeiq/detectors/typescript/mongoose_orm.py +151 -0
- osscodeiq/detectors/typescript/nestjs_controllers.py +99 -0
- osscodeiq/detectors/typescript/nestjs_guards.py +138 -0
- osscodeiq/detectors/typescript/passport_jwt.py +133 -0
- osscodeiq/detectors/typescript/prisma_orm.py +96 -0
- osscodeiq/detectors/typescript/remix_routes.py +160 -0
- osscodeiq/detectors/typescript/sequelize_orm.py +136 -0
- osscodeiq/detectors/typescript/typeorm_entities.py +86 -0
- osscodeiq/detectors/typescript/typescript_structures.py +185 -0
- osscodeiq/detectors/utils.py +49 -0
- osscodeiq/discovery/__init__.py +11 -0
- osscodeiq/discovery/change_detector.py +97 -0
- osscodeiq/discovery/file_discovery.py +342 -0
- osscodeiq/flow/__init__.py +0 -0
- osscodeiq/flow/engine.py +78 -0
- osscodeiq/flow/models.py +72 -0
- osscodeiq/flow/renderer.py +127 -0
- osscodeiq/flow/templates/interactive.html +252 -0
- osscodeiq/flow/vendor/cytoscape-dagre.min.js +8 -0
- osscodeiq/flow/vendor/cytoscape.min.js +32 -0
- osscodeiq/flow/vendor/dagre.min.js +3809 -0
- osscodeiq/flow/views.py +357 -0
- osscodeiq/graph/__init__.py +0 -0
- osscodeiq/graph/backend.py +52 -0
- osscodeiq/graph/backends/__init__.py +23 -0
- osscodeiq/graph/backends/kuzu.py +576 -0
- osscodeiq/graph/backends/networkx.py +135 -0
- osscodeiq/graph/backends/sqlite_backend.py +406 -0
- osscodeiq/graph/builder.py +297 -0
- osscodeiq/graph/query.py +228 -0
- osscodeiq/graph/store.py +183 -0
- osscodeiq/graph/views.py +231 -0
- osscodeiq/models/__init__.py +17 -0
- osscodeiq/models/graph.py +116 -0
- osscodeiq/output/__init__.py +0 -0
- osscodeiq/output/dot.py +171 -0
- osscodeiq/output/mermaid.py +160 -0
- osscodeiq/output/safety.py +58 -0
- osscodeiq/output/serializers.py +42 -0
- osscodeiq/parsing/__init__.py +5 -0
- osscodeiq/parsing/languages/__init__.py +0 -0
- osscodeiq/parsing/languages/base.py +23 -0
- osscodeiq/parsing/languages/java.py +68 -0
- osscodeiq/parsing/languages/python.py +57 -0
- osscodeiq/parsing/languages/typescript.py +95 -0
- osscodeiq/parsing/parser_manager.py +125 -0
- osscodeiq/parsing/structured/__init__.py +0 -0
- osscodeiq/parsing/structured/gradle_parser.py +78 -0
- osscodeiq/parsing/structured/json_parser.py +24 -0
- osscodeiq/parsing/structured/properties_parser.py +56 -0
- osscodeiq/parsing/structured/sql_parser.py +54 -0
- osscodeiq/parsing/structured/xml_parser.py +148 -0
- osscodeiq/parsing/structured/yaml_parser.py +38 -0
- osscodeiq/server/__init__.py +7 -0
- osscodeiq/server/app.py +53 -0
- osscodeiq/server/mcp_server.py +174 -0
- osscodeiq/server/middleware.py +16 -0
- osscodeiq/server/routes.py +184 -0
- osscodeiq/server/service.py +445 -0
- osscodeiq/server/templates/welcome.html +56 -0
- osscodeiq-0.0.0.dist-info/METADATA +30 -0
- osscodeiq-0.0.0.dist-info/RECORD +183 -0
- osscodeiq-0.0.0.dist-info/WHEEL +5 -0
- osscodeiq-0.0.0.dist-info/entry_points.txt +2 -0
- osscodeiq-0.0.0.dist-info/licenses/LICENSE +21 -0
- osscodeiq-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Kubernetes manifest detector for container orchestration resource definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
8
|
+
from osscodeiq.models.graph import (
|
|
9
|
+
EdgeKind,
|
|
10
|
+
GraphEdge,
|
|
11
|
+
GraphNode,
|
|
12
|
+
NodeKind,
|
|
13
|
+
SourceLocation,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
_K8S_KINDS: frozenset[str] = frozenset({
|
|
17
|
+
"Deployment",
|
|
18
|
+
"Service",
|
|
19
|
+
"ConfigMap",
|
|
20
|
+
"Secret",
|
|
21
|
+
"Ingress",
|
|
22
|
+
"Pod",
|
|
23
|
+
"StatefulSet",
|
|
24
|
+
"DaemonSet",
|
|
25
|
+
"Job",
|
|
26
|
+
"CronJob",
|
|
27
|
+
"Namespace",
|
|
28
|
+
"PersistentVolumeClaim",
|
|
29
|
+
"ServiceAccount",
|
|
30
|
+
"Role",
|
|
31
|
+
"RoleBinding",
|
|
32
|
+
"ClusterRole",
|
|
33
|
+
"ClusterRoleBinding",
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _is_k8s_doc(doc: Any) -> bool:
|
|
38
|
+
"""Check whether a parsed YAML document looks like a Kubernetes resource."""
|
|
39
|
+
return isinstance(doc, dict) and doc.get("kind") in _K8S_KINDS
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _get_documents(ctx: DetectorContext) -> list[dict[str, Any]]:
|
|
43
|
+
"""Extract Kubernetes documents from parsed data."""
|
|
44
|
+
if not ctx.parsed_data:
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
ptype = ctx.parsed_data.get("type")
|
|
48
|
+
|
|
49
|
+
if ptype == "yaml_multi":
|
|
50
|
+
docs = ctx.parsed_data.get("documents", [])
|
|
51
|
+
if isinstance(docs, list):
|
|
52
|
+
return [d for d in docs if _is_k8s_doc(d)]
|
|
53
|
+
return []
|
|
54
|
+
|
|
55
|
+
if ptype == "yaml":
|
|
56
|
+
data = ctx.parsed_data.get("data")
|
|
57
|
+
if _is_k8s_doc(data):
|
|
58
|
+
return [data]
|
|
59
|
+
return []
|
|
60
|
+
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _safe_str(val: Any) -> str:
|
|
65
|
+
"""Safely convert a value to string."""
|
|
66
|
+
if val is None:
|
|
67
|
+
return ""
|
|
68
|
+
return str(val)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class KubernetesDetector:
|
|
72
|
+
"""Detects Kubernetes resources, container specs, and cross-resource relationships."""
|
|
73
|
+
|
|
74
|
+
name: str = "kubernetes"
|
|
75
|
+
supported_languages: tuple[str, ...] = ("yaml",)
|
|
76
|
+
|
|
77
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
78
|
+
result = DetectorResult()
|
|
79
|
+
|
|
80
|
+
documents = _get_documents(ctx)
|
|
81
|
+
if not documents:
|
|
82
|
+
return result
|
|
83
|
+
|
|
84
|
+
fp = ctx.file_path
|
|
85
|
+
|
|
86
|
+
# Track deployments by their match labels for service selector resolution
|
|
87
|
+
deployment_labels: dict[str, str] = {} # "label_key=label_val" -> node_id
|
|
88
|
+
# Track services with selectors
|
|
89
|
+
service_selectors: list[tuple[str, dict[str, str]]] = [] # (node_id, selector)
|
|
90
|
+
# Track ingress backends
|
|
91
|
+
ingress_backends: list[tuple[str, str]] = [] # (ingress_node_id, service_name)
|
|
92
|
+
|
|
93
|
+
for doc in documents:
|
|
94
|
+
kind = doc.get("kind", "")
|
|
95
|
+
metadata = doc.get("metadata") or {}
|
|
96
|
+
if not isinstance(metadata, dict):
|
|
97
|
+
metadata = {}
|
|
98
|
+
|
|
99
|
+
name = _safe_str(metadata.get("name", "unknown"))
|
|
100
|
+
namespace = _safe_str(metadata.get("namespace", "default")) or "default"
|
|
101
|
+
labels = metadata.get("labels")
|
|
102
|
+
annotations = metadata.get("annotations")
|
|
103
|
+
|
|
104
|
+
node_id = f"k8s:{fp}:{kind}:{namespace}/{name}"
|
|
105
|
+
|
|
106
|
+
props: dict[str, Any] = {"kind": kind, "namespace": namespace}
|
|
107
|
+
if isinstance(labels, dict):
|
|
108
|
+
props["labels"] = labels
|
|
109
|
+
if isinstance(annotations, dict):
|
|
110
|
+
props["annotations"] = annotations
|
|
111
|
+
|
|
112
|
+
# INFRA_RESOURCE node for the resource
|
|
113
|
+
result.nodes.append(GraphNode(
|
|
114
|
+
id=node_id,
|
|
115
|
+
kind=NodeKind.INFRA_RESOURCE,
|
|
116
|
+
label=f"{kind}/{name}",
|
|
117
|
+
fqn=f"k8s:{kind}:{namespace}/{name}",
|
|
118
|
+
module=ctx.module_name,
|
|
119
|
+
location=SourceLocation(file_path=fp),
|
|
120
|
+
properties=props,
|
|
121
|
+
))
|
|
122
|
+
|
|
123
|
+
spec = doc.get("spec") or {}
|
|
124
|
+
if not isinstance(spec, dict):
|
|
125
|
+
spec = {}
|
|
126
|
+
|
|
127
|
+
# Extract container specs from workload resources
|
|
128
|
+
if kind in ("Deployment", "StatefulSet", "DaemonSet", "Job", "CronJob", "Pod"):
|
|
129
|
+
containers = self._extract_containers(spec, kind)
|
|
130
|
+
for container in containers:
|
|
131
|
+
c_name = _safe_str(container.get("name", "unnamed"))
|
|
132
|
+
c_props: dict[str, Any] = {}
|
|
133
|
+
|
|
134
|
+
image = container.get("image")
|
|
135
|
+
if image:
|
|
136
|
+
c_props["image"] = str(image)
|
|
137
|
+
|
|
138
|
+
# Ports
|
|
139
|
+
c_ports = container.get("ports")
|
|
140
|
+
if isinstance(c_ports, list):
|
|
141
|
+
port_strs = []
|
|
142
|
+
for p in c_ports:
|
|
143
|
+
if isinstance(p, dict):
|
|
144
|
+
port_strs.append(
|
|
145
|
+
f"{p.get('containerPort', '?')}/{p.get('protocol', 'TCP')}"
|
|
146
|
+
)
|
|
147
|
+
if port_strs:
|
|
148
|
+
c_props["ports"] = port_strs
|
|
149
|
+
|
|
150
|
+
# Environment variables
|
|
151
|
+
env_vars = container.get("env")
|
|
152
|
+
if isinstance(env_vars, list):
|
|
153
|
+
env_names = []
|
|
154
|
+
for e in env_vars:
|
|
155
|
+
if isinstance(e, dict) and "name" in e:
|
|
156
|
+
env_names.append(str(e["name"]))
|
|
157
|
+
if env_names:
|
|
158
|
+
c_props["env_vars"] = env_names
|
|
159
|
+
|
|
160
|
+
result.nodes.append(GraphNode(
|
|
161
|
+
id=f"{node_id}:container:{c_name}",
|
|
162
|
+
kind=NodeKind.CONFIG_KEY,
|
|
163
|
+
label=f"{name}/{c_name}",
|
|
164
|
+
module=ctx.module_name,
|
|
165
|
+
location=SourceLocation(file_path=fp),
|
|
166
|
+
properties=c_props,
|
|
167
|
+
))
|
|
168
|
+
|
|
169
|
+
# Track deployment match labels for service selector linking
|
|
170
|
+
if kind in ("Deployment", "StatefulSet", "DaemonSet"):
|
|
171
|
+
template = spec.get("template") or {}
|
|
172
|
+
if isinstance(template, dict):
|
|
173
|
+
tmpl_meta = template.get("metadata") or {}
|
|
174
|
+
if isinstance(tmpl_meta, dict):
|
|
175
|
+
tmpl_labels = tmpl_meta.get("labels")
|
|
176
|
+
if isinstance(tmpl_labels, dict):
|
|
177
|
+
for lk, lv in tmpl_labels.items():
|
|
178
|
+
deployment_labels[f"{lk}={lv}"] = node_id
|
|
179
|
+
|
|
180
|
+
# Also use spec.selector.matchLabels
|
|
181
|
+
selector = spec.get("selector") or {}
|
|
182
|
+
if isinstance(selector, dict):
|
|
183
|
+
match_labels = selector.get("matchLabels")
|
|
184
|
+
if isinstance(match_labels, dict):
|
|
185
|
+
for lk, lv in match_labels.items():
|
|
186
|
+
deployment_labels[f"{lk}={lv}"] = node_id
|
|
187
|
+
|
|
188
|
+
# Track service selectors
|
|
189
|
+
if kind == "Service":
|
|
190
|
+
svc_selector = spec.get("selector")
|
|
191
|
+
if isinstance(svc_selector, dict):
|
|
192
|
+
service_selectors.append((node_id, svc_selector))
|
|
193
|
+
|
|
194
|
+
# Track ingress backends
|
|
195
|
+
if kind == "Ingress":
|
|
196
|
+
self._collect_ingress_backends(spec, node_id, ingress_backends)
|
|
197
|
+
|
|
198
|
+
# Resolve service selector -> deployment edges
|
|
199
|
+
for svc_node_id, selector in service_selectors:
|
|
200
|
+
for sel_key, sel_val in selector.items():
|
|
201
|
+
label_tag = f"{sel_key}={sel_val}"
|
|
202
|
+
if label_tag in deployment_labels:
|
|
203
|
+
result.edges.append(GraphEdge(
|
|
204
|
+
source=svc_node_id,
|
|
205
|
+
target=deployment_labels[label_tag],
|
|
206
|
+
kind=EdgeKind.DEPENDS_ON,
|
|
207
|
+
label=f"service selects {label_tag}",
|
|
208
|
+
properties={"selector": label_tag},
|
|
209
|
+
))
|
|
210
|
+
|
|
211
|
+
# Resolve ingress -> service edges
|
|
212
|
+
# Build a lookup of service names to their node IDs
|
|
213
|
+
service_name_to_id: dict[str, str] = {}
|
|
214
|
+
for doc in documents:
|
|
215
|
+
if doc.get("kind") != "Service":
|
|
216
|
+
continue
|
|
217
|
+
meta = doc.get("metadata") or {}
|
|
218
|
+
if isinstance(meta, dict):
|
|
219
|
+
svc_name = _safe_str(meta.get("name", ""))
|
|
220
|
+
ns = _safe_str(meta.get("namespace", "default")) or "default"
|
|
221
|
+
svc_nid = f"k8s:{fp}:Service:{ns}/{svc_name}"
|
|
222
|
+
service_name_to_id[svc_name] = svc_nid
|
|
223
|
+
|
|
224
|
+
for ingress_nid, backend_svc in ingress_backends:
|
|
225
|
+
target_id = service_name_to_id.get(backend_svc)
|
|
226
|
+
if target_id:
|
|
227
|
+
result.edges.append(GraphEdge(
|
|
228
|
+
source=ingress_nid,
|
|
229
|
+
target=target_id,
|
|
230
|
+
kind=EdgeKind.CONNECTS_TO,
|
|
231
|
+
label=f"ingress routes to {backend_svc}",
|
|
232
|
+
))
|
|
233
|
+
|
|
234
|
+
return result
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _extract_containers(spec: dict[str, Any], kind: str) -> list[dict[str, Any]]:
|
|
238
|
+
"""Extract container definitions from a workload spec, navigating nested templates."""
|
|
239
|
+
containers: list[dict[str, Any]] = []
|
|
240
|
+
|
|
241
|
+
if kind == "Pod":
|
|
242
|
+
cs = spec.get("containers")
|
|
243
|
+
if isinstance(cs, list):
|
|
244
|
+
containers.extend(c for c in cs if isinstance(c, dict))
|
|
245
|
+
return containers
|
|
246
|
+
|
|
247
|
+
if kind == "CronJob":
|
|
248
|
+
job_template = spec.get("jobTemplate") or {}
|
|
249
|
+
if isinstance(job_template, dict):
|
|
250
|
+
spec = job_template.get("spec") or {}
|
|
251
|
+
if not isinstance(spec, dict):
|
|
252
|
+
return containers
|
|
253
|
+
|
|
254
|
+
template = spec.get("template") or {}
|
|
255
|
+
if isinstance(template, dict):
|
|
256
|
+
pod_spec = template.get("spec") or {}
|
|
257
|
+
if isinstance(pod_spec, dict):
|
|
258
|
+
cs = pod_spec.get("containers")
|
|
259
|
+
if isinstance(cs, list):
|
|
260
|
+
containers.extend(c for c in cs if isinstance(c, dict))
|
|
261
|
+
init_cs = pod_spec.get("initContainers")
|
|
262
|
+
if isinstance(init_cs, list):
|
|
263
|
+
containers.extend(c for c in init_cs if isinstance(c, dict))
|
|
264
|
+
|
|
265
|
+
return containers
|
|
266
|
+
|
|
267
|
+
@staticmethod
|
|
268
|
+
def _collect_ingress_backends(
|
|
269
|
+
spec: dict[str, Any],
|
|
270
|
+
ingress_node_id: str,
|
|
271
|
+
out: list[tuple[str, str]],
|
|
272
|
+
) -> None:
|
|
273
|
+
"""Collect backend service references from an Ingress spec."""
|
|
274
|
+
# Default backend
|
|
275
|
+
default_backend = spec.get("defaultBackend") or spec.get("backend")
|
|
276
|
+
if isinstance(default_backend, dict):
|
|
277
|
+
svc = default_backend.get("service") or default_backend
|
|
278
|
+
if isinstance(svc, dict):
|
|
279
|
+
svc_name = svc.get("name") or svc.get("serviceName")
|
|
280
|
+
if svc_name:
|
|
281
|
+
out.append((ingress_node_id, str(svc_name)))
|
|
282
|
+
|
|
283
|
+
# Rules
|
|
284
|
+
rules = spec.get("rules")
|
|
285
|
+
if isinstance(rules, list):
|
|
286
|
+
for rule in rules:
|
|
287
|
+
if not isinstance(rule, dict):
|
|
288
|
+
continue
|
|
289
|
+
http = rule.get("http") or {}
|
|
290
|
+
if not isinstance(http, dict):
|
|
291
|
+
continue
|
|
292
|
+
paths = http.get("paths")
|
|
293
|
+
if not isinstance(paths, list):
|
|
294
|
+
continue
|
|
295
|
+
for path_entry in paths:
|
|
296
|
+
if not isinstance(path_entry, dict):
|
|
297
|
+
continue
|
|
298
|
+
backend = path_entry.get("backend")
|
|
299
|
+
if not isinstance(backend, dict):
|
|
300
|
+
continue
|
|
301
|
+
svc = backend.get("service") or backend
|
|
302
|
+
if isinstance(svc, dict):
|
|
303
|
+
svc_name = svc.get("name") or svc.get("serviceName")
|
|
304
|
+
if svc_name:
|
|
305
|
+
out.append((ingress_node_id, str(svc_name)))
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Kubernetes RBAC (Role-Based Access Control) detector."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
8
|
+
from osscodeiq.models.graph import (
|
|
9
|
+
EdgeKind,
|
|
10
|
+
GraphEdge,
|
|
11
|
+
GraphNode,
|
|
12
|
+
NodeKind,
|
|
13
|
+
SourceLocation,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
_RBAC_KINDS: frozenset[str] = frozenset({
|
|
17
|
+
"Role",
|
|
18
|
+
"ClusterRole",
|
|
19
|
+
"RoleBinding",
|
|
20
|
+
"ClusterRoleBinding",
|
|
21
|
+
"ServiceAccount",
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _safe_str(val: Any) -> str:
|
|
26
|
+
"""Safely convert a value to string."""
|
|
27
|
+
if val is None:
|
|
28
|
+
return ""
|
|
29
|
+
return str(val)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _get_documents(ctx: DetectorContext) -> list[dict[str, Any]]:
|
|
33
|
+
"""Extract RBAC-related Kubernetes documents from parsed data."""
|
|
34
|
+
if not ctx.parsed_data:
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
ptype = ctx.parsed_data.get("type")
|
|
38
|
+
|
|
39
|
+
if ptype == "yaml_multi":
|
|
40
|
+
docs = ctx.parsed_data.get("documents", [])
|
|
41
|
+
if isinstance(docs, list):
|
|
42
|
+
return [
|
|
43
|
+
d for d in docs
|
|
44
|
+
if isinstance(d, dict) and d.get("kind") in _RBAC_KINDS
|
|
45
|
+
]
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
if ptype == "yaml":
|
|
49
|
+
data = ctx.parsed_data.get("data")
|
|
50
|
+
if isinstance(data, dict) and data.get("kind") in _RBAC_KINDS:
|
|
51
|
+
return [data]
|
|
52
|
+
return []
|
|
53
|
+
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _make_node_id(file_path: str, kind: str, namespace: str, name: str) -> str:
|
|
58
|
+
"""Build deterministic node ID for a K8s RBAC resource."""
|
|
59
|
+
return f"k8s_rbac:{file_path}:{kind}:{namespace}/{name}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class KubernetesRBACDetector:
|
|
63
|
+
"""Detects Kubernetes RBAC resources and produces GUARD nodes and PROTECTS edges."""
|
|
64
|
+
|
|
65
|
+
name: str = "config.kubernetes_rbac"
|
|
66
|
+
supported_languages: tuple[str, ...] = ("yaml",)
|
|
67
|
+
|
|
68
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
69
|
+
result = DetectorResult()
|
|
70
|
+
|
|
71
|
+
documents = _get_documents(ctx)
|
|
72
|
+
if not documents:
|
|
73
|
+
return result
|
|
74
|
+
|
|
75
|
+
fp = ctx.file_path
|
|
76
|
+
|
|
77
|
+
# Collect nodes first so we can resolve bindings
|
|
78
|
+
role_nodes: dict[str, str] = {} # "kind:namespace/name" -> node_id
|
|
79
|
+
sa_nodes: dict[str, str] = {} # "namespace/name" -> node_id
|
|
80
|
+
bindings: list[dict[str, Any]] = []
|
|
81
|
+
|
|
82
|
+
for doc in documents:
|
|
83
|
+
kind = doc.get("kind", "")
|
|
84
|
+
metadata = doc.get("metadata") or {}
|
|
85
|
+
if not isinstance(metadata, dict):
|
|
86
|
+
metadata = {}
|
|
87
|
+
|
|
88
|
+
name = _safe_str(metadata.get("name", "unknown"))
|
|
89
|
+
namespace = _safe_str(metadata.get("namespace", "default")) or "default"
|
|
90
|
+
|
|
91
|
+
node_id = _make_node_id(fp, kind, namespace, name)
|
|
92
|
+
|
|
93
|
+
if kind in ("Role", "ClusterRole"):
|
|
94
|
+
rules = doc.get("rules")
|
|
95
|
+
if not isinstance(rules, list):
|
|
96
|
+
rules = []
|
|
97
|
+
serialized_rules = []
|
|
98
|
+
for rule in rules:
|
|
99
|
+
if isinstance(rule, dict):
|
|
100
|
+
serialized_rules.append({
|
|
101
|
+
"apiGroups": rule.get("apiGroups", []),
|
|
102
|
+
"resources": rule.get("resources", []),
|
|
103
|
+
"verbs": rule.get("verbs", []),
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
result.nodes.append(GraphNode(
|
|
107
|
+
id=node_id,
|
|
108
|
+
kind=NodeKind.GUARD,
|
|
109
|
+
label=f"{kind}/{name}",
|
|
110
|
+
fqn=f"k8s:{kind}:{namespace}/{name}",
|
|
111
|
+
module=ctx.module_name,
|
|
112
|
+
location=SourceLocation(file_path=fp),
|
|
113
|
+
properties={
|
|
114
|
+
"auth_type": "k8s_rbac",
|
|
115
|
+
"k8s_kind": kind,
|
|
116
|
+
"namespace": namespace,
|
|
117
|
+
"rules": serialized_rules,
|
|
118
|
+
},
|
|
119
|
+
))
|
|
120
|
+
role_key = f"{kind}:{namespace}/{name}"
|
|
121
|
+
# ClusterRoles are cluster-scoped; store with "cluster-wide" marker
|
|
122
|
+
if kind == "ClusterRole":
|
|
123
|
+
role_key = f"ClusterRole:cluster-wide/{name}"
|
|
124
|
+
role_nodes[role_key] = node_id
|
|
125
|
+
|
|
126
|
+
elif kind == "ServiceAccount":
|
|
127
|
+
result.nodes.append(GraphNode(
|
|
128
|
+
id=node_id,
|
|
129
|
+
kind=NodeKind.GUARD,
|
|
130
|
+
label=f"ServiceAccount/{name}",
|
|
131
|
+
fqn=f"k8s:ServiceAccount:{namespace}/{name}",
|
|
132
|
+
module=ctx.module_name,
|
|
133
|
+
location=SourceLocation(file_path=fp),
|
|
134
|
+
properties={
|
|
135
|
+
"auth_type": "k8s_rbac",
|
|
136
|
+
"k8s_kind": "ServiceAccount",
|
|
137
|
+
"namespace": namespace,
|
|
138
|
+
"rules": [],
|
|
139
|
+
},
|
|
140
|
+
))
|
|
141
|
+
sa_nodes[f"{namespace}/{name}"] = node_id
|
|
142
|
+
|
|
143
|
+
elif kind in ("RoleBinding", "ClusterRoleBinding"):
|
|
144
|
+
result.nodes.append(GraphNode(
|
|
145
|
+
id=node_id,
|
|
146
|
+
kind=NodeKind.GUARD,
|
|
147
|
+
label=f"{kind}/{name}",
|
|
148
|
+
fqn=f"k8s:{kind}:{namespace}/{name}",
|
|
149
|
+
module=ctx.module_name,
|
|
150
|
+
location=SourceLocation(file_path=fp),
|
|
151
|
+
properties={
|
|
152
|
+
"auth_type": "k8s_rbac",
|
|
153
|
+
"k8s_kind": kind,
|
|
154
|
+
"namespace": namespace,
|
|
155
|
+
"rules": [],
|
|
156
|
+
},
|
|
157
|
+
))
|
|
158
|
+
bindings.append(doc)
|
|
159
|
+
|
|
160
|
+
# Resolve RoleBinding/ClusterRoleBinding -> PROTECTS edges
|
|
161
|
+
for doc in bindings:
|
|
162
|
+
kind = doc.get("kind", "")
|
|
163
|
+
metadata = doc.get("metadata") or {}
|
|
164
|
+
if not isinstance(metadata, dict):
|
|
165
|
+
metadata = {}
|
|
166
|
+
binding_namespace = _safe_str(metadata.get("namespace", "default")) or "default"
|
|
167
|
+
|
|
168
|
+
role_ref = doc.get("roleRef")
|
|
169
|
+
if not isinstance(role_ref, dict):
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
ref_kind = _safe_str(role_ref.get("kind", ""))
|
|
173
|
+
ref_name = _safe_str(role_ref.get("name", ""))
|
|
174
|
+
|
|
175
|
+
# Resolve the role node
|
|
176
|
+
if ref_kind == "ClusterRole":
|
|
177
|
+
role_key = f"ClusterRole:cluster-wide/{ref_name}"
|
|
178
|
+
else:
|
|
179
|
+
role_key = f"{ref_kind}:{binding_namespace}/{ref_name}"
|
|
180
|
+
|
|
181
|
+
role_nid = role_nodes.get(role_key)
|
|
182
|
+
if not role_nid:
|
|
183
|
+
continue
|
|
184
|
+
|
|
185
|
+
subjects = doc.get("subjects")
|
|
186
|
+
if not isinstance(subjects, list):
|
|
187
|
+
continue
|
|
188
|
+
|
|
189
|
+
for subject in subjects:
|
|
190
|
+
if not isinstance(subject, dict):
|
|
191
|
+
continue
|
|
192
|
+
subj_kind = _safe_str(subject.get("kind", ""))
|
|
193
|
+
subj_name = _safe_str(subject.get("name", ""))
|
|
194
|
+
subj_namespace = _safe_str(
|
|
195
|
+
subject.get("namespace", binding_namespace)
|
|
196
|
+
) or binding_namespace
|
|
197
|
+
|
|
198
|
+
if subj_kind == "ServiceAccount":
|
|
199
|
+
sa_key = f"{subj_namespace}/{subj_name}"
|
|
200
|
+
sa_nid = sa_nodes.get(sa_key)
|
|
201
|
+
if sa_nid:
|
|
202
|
+
result.edges.append(GraphEdge(
|
|
203
|
+
source=role_nid,
|
|
204
|
+
target=sa_nid,
|
|
205
|
+
kind=EdgeKind.PROTECTS,
|
|
206
|
+
label=f"{ref_kind}/{ref_name} -> ServiceAccount/{subj_name}",
|
|
207
|
+
properties={
|
|
208
|
+
"binding_kind": kind,
|
|
209
|
+
},
|
|
210
|
+
))
|
|
211
|
+
|
|
212
|
+
return result
|