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.
Files changed (183) hide show
  1. osscodeiq/__init__.py +0 -0
  2. osscodeiq/analyzer.py +467 -0
  3. osscodeiq/cache/__init__.py +0 -0
  4. osscodeiq/cache/hasher.py +23 -0
  5. osscodeiq/cache/store.py +300 -0
  6. osscodeiq/classifiers/__init__.py +0 -0
  7. osscodeiq/classifiers/layer_classifier.py +69 -0
  8. osscodeiq/cli.py +721 -0
  9. osscodeiq/config.py +113 -0
  10. osscodeiq/detectors/__init__.py +0 -0
  11. osscodeiq/detectors/auth/__init__.py +0 -0
  12. osscodeiq/detectors/auth/certificate_auth.py +139 -0
  13. osscodeiq/detectors/auth/ldap_auth.py +89 -0
  14. osscodeiq/detectors/auth/session_header_auth.py +120 -0
  15. osscodeiq/detectors/base.py +41 -0
  16. osscodeiq/detectors/config/__init__.py +0 -0
  17. osscodeiq/detectors/config/batch_structure.py +128 -0
  18. osscodeiq/detectors/config/cloudformation.py +183 -0
  19. osscodeiq/detectors/config/docker_compose.py +179 -0
  20. osscodeiq/detectors/config/github_actions.py +150 -0
  21. osscodeiq/detectors/config/gitlab_ci.py +216 -0
  22. osscodeiq/detectors/config/helm_chart.py +187 -0
  23. osscodeiq/detectors/config/ini_structure.py +101 -0
  24. osscodeiq/detectors/config/json_structure.py +72 -0
  25. osscodeiq/detectors/config/kubernetes.py +305 -0
  26. osscodeiq/detectors/config/kubernetes_rbac.py +212 -0
  27. osscodeiq/detectors/config/openapi.py +194 -0
  28. osscodeiq/detectors/config/package_json.py +99 -0
  29. osscodeiq/detectors/config/properties_detector.py +108 -0
  30. osscodeiq/detectors/config/pyproject_toml.py +169 -0
  31. osscodeiq/detectors/config/sql_structure.py +155 -0
  32. osscodeiq/detectors/config/toml_structure.py +93 -0
  33. osscodeiq/detectors/config/tsconfig_json.py +105 -0
  34. osscodeiq/detectors/config/yaml_structure.py +82 -0
  35. osscodeiq/detectors/cpp/__init__.py +0 -0
  36. osscodeiq/detectors/cpp/cpp_structures.py +192 -0
  37. osscodeiq/detectors/csharp/__init__.py +0 -0
  38. osscodeiq/detectors/csharp/csharp_efcore.py +184 -0
  39. osscodeiq/detectors/csharp/csharp_minimal_apis.py +156 -0
  40. osscodeiq/detectors/csharp/csharp_structures.py +317 -0
  41. osscodeiq/detectors/docs/__init__.py +0 -0
  42. osscodeiq/detectors/docs/markdown_structure.py +117 -0
  43. osscodeiq/detectors/frontend/__init__.py +0 -0
  44. osscodeiq/detectors/frontend/angular_components.py +177 -0
  45. osscodeiq/detectors/frontend/frontend_routes.py +259 -0
  46. osscodeiq/detectors/frontend/react_components.py +148 -0
  47. osscodeiq/detectors/frontend/svelte_components.py +84 -0
  48. osscodeiq/detectors/frontend/vue_components.py +150 -0
  49. osscodeiq/detectors/generic/__init__.py +1 -0
  50. osscodeiq/detectors/generic/imports_detector.py +413 -0
  51. osscodeiq/detectors/go/__init__.py +0 -0
  52. osscodeiq/detectors/go/go_orm.py +202 -0
  53. osscodeiq/detectors/go/go_structures.py +162 -0
  54. osscodeiq/detectors/go/go_web.py +157 -0
  55. osscodeiq/detectors/iac/__init__.py +0 -0
  56. osscodeiq/detectors/iac/bicep.py +135 -0
  57. osscodeiq/detectors/iac/dockerfile.py +182 -0
  58. osscodeiq/detectors/iac/terraform.py +188 -0
  59. osscodeiq/detectors/java/__init__.py +0 -0
  60. osscodeiq/detectors/java/azure_functions.py +424 -0
  61. osscodeiq/detectors/java/azure_messaging.py +350 -0
  62. osscodeiq/detectors/java/class_hierarchy.py +349 -0
  63. osscodeiq/detectors/java/config_def.py +82 -0
  64. osscodeiq/detectors/java/cosmos_db.py +105 -0
  65. osscodeiq/detectors/java/graphql_resolver.py +188 -0
  66. osscodeiq/detectors/java/grpc_service.py +142 -0
  67. osscodeiq/detectors/java/ibm_mq.py +178 -0
  68. osscodeiq/detectors/java/jaxrs.py +160 -0
  69. osscodeiq/detectors/java/jdbc.py +196 -0
  70. osscodeiq/detectors/java/jms.py +116 -0
  71. osscodeiq/detectors/java/jpa_entity.py +143 -0
  72. osscodeiq/detectors/java/kafka.py +113 -0
  73. osscodeiq/detectors/java/kafka_protocol.py +70 -0
  74. osscodeiq/detectors/java/micronaut.py +248 -0
  75. osscodeiq/detectors/java/module_deps.py +191 -0
  76. osscodeiq/detectors/java/public_api.py +206 -0
  77. osscodeiq/detectors/java/quarkus.py +176 -0
  78. osscodeiq/detectors/java/rabbitmq.py +150 -0
  79. osscodeiq/detectors/java/raw_sql.py +136 -0
  80. osscodeiq/detectors/java/repository.py +131 -0
  81. osscodeiq/detectors/java/rmi.py +129 -0
  82. osscodeiq/detectors/java/spring_events.py +117 -0
  83. osscodeiq/detectors/java/spring_rest.py +168 -0
  84. osscodeiq/detectors/java/spring_security.py +212 -0
  85. osscodeiq/detectors/java/tibco_ems.py +193 -0
  86. osscodeiq/detectors/java/websocket.py +188 -0
  87. osscodeiq/detectors/kotlin/__init__.py +0 -0
  88. osscodeiq/detectors/kotlin/kotlin_structures.py +124 -0
  89. osscodeiq/detectors/kotlin/ktor_routes.py +163 -0
  90. osscodeiq/detectors/proto/__init__.py +0 -0
  91. osscodeiq/detectors/proto/proto_structure.py +153 -0
  92. osscodeiq/detectors/python/__init__.py +0 -0
  93. osscodeiq/detectors/python/celery_tasks.py +88 -0
  94. osscodeiq/detectors/python/django_auth.py +132 -0
  95. osscodeiq/detectors/python/django_models.py +157 -0
  96. osscodeiq/detectors/python/django_views.py +74 -0
  97. osscodeiq/detectors/python/fastapi_auth.py +143 -0
  98. osscodeiq/detectors/python/fastapi_routes.py +68 -0
  99. osscodeiq/detectors/python/flask_routes.py +67 -0
  100. osscodeiq/detectors/python/kafka_python.py +175 -0
  101. osscodeiq/detectors/python/pydantic_models.py +115 -0
  102. osscodeiq/detectors/python/python_structures.py +234 -0
  103. osscodeiq/detectors/python/sqlalchemy_models.py +82 -0
  104. osscodeiq/detectors/registry.py +100 -0
  105. osscodeiq/detectors/rust/__init__.py +0 -0
  106. osscodeiq/detectors/rust/actix_web.py +234 -0
  107. osscodeiq/detectors/rust/rust_structures.py +174 -0
  108. osscodeiq/detectors/scala/__init__.py +0 -0
  109. osscodeiq/detectors/scala/scala_structures.py +128 -0
  110. osscodeiq/detectors/shell/__init__.py +0 -0
  111. osscodeiq/detectors/shell/bash_detector.py +127 -0
  112. osscodeiq/detectors/shell/powershell_detector.py +118 -0
  113. osscodeiq/detectors/typescript/__init__.py +0 -0
  114. osscodeiq/detectors/typescript/express_routes.py +55 -0
  115. osscodeiq/detectors/typescript/fastify_routes.py +156 -0
  116. osscodeiq/detectors/typescript/graphql_resolvers.py +100 -0
  117. osscodeiq/detectors/typescript/kafka_js.py +164 -0
  118. osscodeiq/detectors/typescript/mongoose_orm.py +151 -0
  119. osscodeiq/detectors/typescript/nestjs_controllers.py +99 -0
  120. osscodeiq/detectors/typescript/nestjs_guards.py +138 -0
  121. osscodeiq/detectors/typescript/passport_jwt.py +133 -0
  122. osscodeiq/detectors/typescript/prisma_orm.py +96 -0
  123. osscodeiq/detectors/typescript/remix_routes.py +160 -0
  124. osscodeiq/detectors/typescript/sequelize_orm.py +136 -0
  125. osscodeiq/detectors/typescript/typeorm_entities.py +86 -0
  126. osscodeiq/detectors/typescript/typescript_structures.py +185 -0
  127. osscodeiq/detectors/utils.py +49 -0
  128. osscodeiq/discovery/__init__.py +11 -0
  129. osscodeiq/discovery/change_detector.py +97 -0
  130. osscodeiq/discovery/file_discovery.py +342 -0
  131. osscodeiq/flow/__init__.py +0 -0
  132. osscodeiq/flow/engine.py +78 -0
  133. osscodeiq/flow/models.py +72 -0
  134. osscodeiq/flow/renderer.py +127 -0
  135. osscodeiq/flow/templates/interactive.html +252 -0
  136. osscodeiq/flow/vendor/cytoscape-dagre.min.js +8 -0
  137. osscodeiq/flow/vendor/cytoscape.min.js +32 -0
  138. osscodeiq/flow/vendor/dagre.min.js +3809 -0
  139. osscodeiq/flow/views.py +357 -0
  140. osscodeiq/graph/__init__.py +0 -0
  141. osscodeiq/graph/backend.py +52 -0
  142. osscodeiq/graph/backends/__init__.py +23 -0
  143. osscodeiq/graph/backends/kuzu.py +576 -0
  144. osscodeiq/graph/backends/networkx.py +135 -0
  145. osscodeiq/graph/backends/sqlite_backend.py +406 -0
  146. osscodeiq/graph/builder.py +297 -0
  147. osscodeiq/graph/query.py +228 -0
  148. osscodeiq/graph/store.py +183 -0
  149. osscodeiq/graph/views.py +231 -0
  150. osscodeiq/models/__init__.py +17 -0
  151. osscodeiq/models/graph.py +116 -0
  152. osscodeiq/output/__init__.py +0 -0
  153. osscodeiq/output/dot.py +171 -0
  154. osscodeiq/output/mermaid.py +160 -0
  155. osscodeiq/output/safety.py +58 -0
  156. osscodeiq/output/serializers.py +42 -0
  157. osscodeiq/parsing/__init__.py +5 -0
  158. osscodeiq/parsing/languages/__init__.py +0 -0
  159. osscodeiq/parsing/languages/base.py +23 -0
  160. osscodeiq/parsing/languages/java.py +68 -0
  161. osscodeiq/parsing/languages/python.py +57 -0
  162. osscodeiq/parsing/languages/typescript.py +95 -0
  163. osscodeiq/parsing/parser_manager.py +125 -0
  164. osscodeiq/parsing/structured/__init__.py +0 -0
  165. osscodeiq/parsing/structured/gradle_parser.py +78 -0
  166. osscodeiq/parsing/structured/json_parser.py +24 -0
  167. osscodeiq/parsing/structured/properties_parser.py +56 -0
  168. osscodeiq/parsing/structured/sql_parser.py +54 -0
  169. osscodeiq/parsing/structured/xml_parser.py +148 -0
  170. osscodeiq/parsing/structured/yaml_parser.py +38 -0
  171. osscodeiq/server/__init__.py +7 -0
  172. osscodeiq/server/app.py +53 -0
  173. osscodeiq/server/mcp_server.py +174 -0
  174. osscodeiq/server/middleware.py +16 -0
  175. osscodeiq/server/routes.py +184 -0
  176. osscodeiq/server/service.py +445 -0
  177. osscodeiq/server/templates/welcome.html +56 -0
  178. osscodeiq-0.0.0.dist-info/METADATA +30 -0
  179. osscodeiq-0.0.0.dist-info/RECORD +183 -0
  180. osscodeiq-0.0.0.dist-info/WHEEL +5 -0
  181. osscodeiq-0.0.0.dist-info/entry_points.txt +2 -0
  182. osscodeiq-0.0.0.dist-info/licenses/LICENSE +21 -0
  183. osscodeiq-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,216 @@
1
+ """GitLab CI pipeline detector for .gitlab-ci.yml 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
+ _GITLAB_CI_KEYWORDS = frozenset({
17
+ "stages",
18
+ "variables",
19
+ "default",
20
+ "workflow",
21
+ "include",
22
+ "image",
23
+ "services",
24
+ "before_script",
25
+ "after_script",
26
+ "cache",
27
+ })
28
+
29
+ _TOOL_KEYWORDS = (
30
+ "docker",
31
+ "helm",
32
+ "kubectl",
33
+ "terraform",
34
+ "maven",
35
+ "gradle",
36
+ "npm",
37
+ "pip",
38
+ )
39
+
40
+
41
+ def _is_gitlab_ci_file(ctx: DetectorContext) -> bool:
42
+ """Check whether the file is a GitLab CI configuration file."""
43
+ return ctx.file_path.endswith(".gitlab-ci.yml")
44
+
45
+
46
+ def _detect_tools(scripts: list[Any]) -> list[str]:
47
+ """Scan script lines for known tool keywords."""
48
+ tools: list[str] = []
49
+ for line in scripts:
50
+ line_str = str(line)
51
+ for tool in _TOOL_KEYWORDS:
52
+ if tool in line_str and tool not in tools:
53
+ tools.append(tool)
54
+ return tools
55
+
56
+
57
+ class GitLabCIDetector:
58
+ """Detects stages, jobs, dependencies, and tool usage from GitLab CI YAML files."""
59
+
60
+ name: str = "gitlab_ci"
61
+ supported_languages: tuple[str, ...] = ("yaml",)
62
+
63
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
64
+ result = DetectorResult()
65
+
66
+ if not _is_gitlab_ci_file(ctx):
67
+ return result
68
+
69
+ if not ctx.parsed_data:
70
+ return result
71
+
72
+ data = ctx.parsed_data.get("data")
73
+ if not isinstance(data, dict):
74
+ return result
75
+
76
+ fp = ctx.file_path
77
+ pipeline_id = f"gitlab:{fp}:pipeline"
78
+
79
+ # Pipeline MODULE node
80
+ result.nodes.append(GraphNode(
81
+ id=pipeline_id,
82
+ kind=NodeKind.MODULE,
83
+ label=f"pipeline:{fp}",
84
+ fqn=pipeline_id,
85
+ module=ctx.module_name,
86
+ location=SourceLocation(file_path=fp),
87
+ properties={"pipeline_file": fp},
88
+ ))
89
+
90
+ # Stages
91
+ stages = data.get("stages")
92
+ if isinstance(stages, list):
93
+ for stage_name in stages:
94
+ stage_str = str(stage_name)
95
+ result.nodes.append(GraphNode(
96
+ id=f"gitlab:{fp}:stage:{stage_str}",
97
+ kind=NodeKind.CONFIG_KEY,
98
+ label=f"stage:{stage_str}",
99
+ module=ctx.module_name,
100
+ location=SourceLocation(file_path=fp),
101
+ properties={"stage": stage_str},
102
+ ))
103
+
104
+ # Include directives
105
+ includes = data.get("include")
106
+ if includes is not None:
107
+ if isinstance(includes, str):
108
+ includes = [includes]
109
+ if isinstance(includes, list):
110
+ for inc in includes:
111
+ if isinstance(inc, str):
112
+ target = inc
113
+ elif isinstance(inc, dict):
114
+ target = inc.get("local") or inc.get("file") or inc.get("template") or str(inc)
115
+ else:
116
+ target = str(inc)
117
+ result.edges.append(GraphEdge(
118
+ source=pipeline_id,
119
+ target=str(target),
120
+ kind=EdgeKind.IMPORTS,
121
+ label=f"includes {target}",
122
+ ))
123
+
124
+ # Collect job names first for edge resolution
125
+ job_names: list[str] = []
126
+ for key in data:
127
+ key_str = str(key)
128
+ if key_str in _GITLAB_CI_KEYWORDS:
129
+ continue
130
+ val = data[key]
131
+ if isinstance(val, dict):
132
+ job_names.append(key_str)
133
+
134
+ job_ids: dict[str, str] = {}
135
+ for name in job_names:
136
+ job_ids[name] = f"gitlab:{fp}:job:{name}"
137
+
138
+ # Process each job
139
+ for job_name in job_names:
140
+ job_def = data[job_name]
141
+ job_id = job_ids[job_name]
142
+
143
+ props: dict[str, Any] = {}
144
+
145
+ # Stage property
146
+ stage_val = job_def.get("stage")
147
+ if stage_val is not None:
148
+ props["stage"] = str(stage_val)
149
+
150
+ # Image property
151
+ image_val = job_def.get("image")
152
+ if image_val is not None:
153
+ props["image"] = str(image_val)
154
+
155
+ # Script tool detection
156
+ scripts = job_def.get("script")
157
+ if isinstance(scripts, list):
158
+ tools = _detect_tools(scripts)
159
+ if tools:
160
+ props["tools"] = tools
161
+
162
+ # Job METHOD node
163
+ result.nodes.append(GraphNode(
164
+ id=job_id,
165
+ kind=NodeKind.METHOD,
166
+ label=job_name,
167
+ fqn=job_id,
168
+ module=ctx.module_name,
169
+ location=SourceLocation(file_path=fp),
170
+ properties=props,
171
+ ))
172
+
173
+ # CONTAINS edge: pipeline -> job
174
+ result.edges.append(GraphEdge(
175
+ source=pipeline_id,
176
+ target=job_id,
177
+ kind=EdgeKind.CONTAINS,
178
+ label=f"pipeline contains job {job_name}",
179
+ ))
180
+
181
+ # needs: dependencies
182
+ needs = job_def.get("needs")
183
+ if isinstance(needs, str):
184
+ needs = [needs]
185
+ if isinstance(needs, list):
186
+ for dep in needs:
187
+ # needs can be a string or a dict with "job" key
188
+ if isinstance(dep, dict):
189
+ dep_str = str(dep.get("job", ""))
190
+ else:
191
+ dep_str = str(dep)
192
+ if dep_str and dep_str in job_ids:
193
+ result.edges.append(GraphEdge(
194
+ source=job_id,
195
+ target=job_ids[dep_str],
196
+ kind=EdgeKind.DEPENDS_ON,
197
+ label=f"job {job_name} needs {dep_str}",
198
+ ))
199
+
200
+ # extends: template inheritance
201
+ extends = job_def.get("extends")
202
+ if extends is not None:
203
+ if isinstance(extends, str):
204
+ extends = [extends]
205
+ if isinstance(extends, list):
206
+ for parent in extends:
207
+ parent_str = str(parent)
208
+ if parent_str in job_ids:
209
+ result.edges.append(GraphEdge(
210
+ source=job_id,
211
+ target=job_ids[parent_str],
212
+ kind=EdgeKind.EXTENDS,
213
+ label=f"job {job_name} extends {parent_str}",
214
+ ))
215
+
216
+ return result
@@ -0,0 +1,187 @@
1
+ """Helm chart detector for Kubernetes Helm chart patterns.
2
+
3
+ Detects Chart.yaml, values.yaml, and template references.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from typing import Any
10
+
11
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
12
+ from osscodeiq.detectors.utils import decode_text
13
+ from osscodeiq.models.graph import (
14
+ EdgeKind,
15
+ GraphEdge,
16
+ GraphNode,
17
+ NodeKind,
18
+ SourceLocation,
19
+ )
20
+
21
+ # Template value references: {{ .Values.key }}
22
+ _VALUES_REF_RE = re.compile(
23
+ r"\{\{\s*\.Values\.([a-zA-Z0-9_.]+)\s*\}\}", re.MULTILINE
24
+ )
25
+
26
+ # Include helper references: {{ include "helper" }}
27
+ _INCLUDE_RE = re.compile(
28
+ r'\{\{-?\s*include\s+["\']([^"\']+)["\']', re.MULTILINE
29
+ )
30
+
31
+
32
+ def _get_yaml_data(ctx: DetectorContext) -> dict[str, Any] | None:
33
+ """Extract YAML data from parsed_data."""
34
+ if not ctx.parsed_data:
35
+ return None
36
+
37
+ ptype = ctx.parsed_data.get("type")
38
+ if ptype == "yaml":
39
+ data = ctx.parsed_data.get("data")
40
+ if isinstance(data, dict):
41
+ return data
42
+ return None
43
+
44
+
45
+ class HelmChartDetector:
46
+ """Detects Helm chart patterns in Chart.yaml, values.yaml, and templates."""
47
+
48
+ name: str = "helm_chart"
49
+ supported_languages: tuple[str, ...] = ("yaml",)
50
+
51
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
52
+ result = DetectorResult()
53
+ fp = ctx.file_path
54
+
55
+ if fp.endswith("Chart.yaml"):
56
+ self._detect_chart_yaml(ctx, result)
57
+ elif fp.endswith("values.yaml") and ("charts/" in fp or "helm/" in fp):
58
+ self._detect_values_yaml(ctx, result)
59
+ elif "/templates/" in fp and fp.endswith(".yaml"):
60
+ self._detect_template(ctx, result)
61
+ else:
62
+ return result
63
+
64
+ return result
65
+
66
+ def _detect_chart_yaml(
67
+ self, ctx: DetectorContext, result: DetectorResult
68
+ ) -> None:
69
+ """Parse Chart.yaml and emit MODULE + DEPENDS_ON edges."""
70
+ fp = ctx.file_path
71
+ data = _get_yaml_data(ctx)
72
+ if not data:
73
+ return
74
+
75
+ chart_name = data.get("name", "unknown")
76
+ chart_version = data.get("version", "0.0.0")
77
+
78
+ chart_node_id = f"helm:{fp}:chart:{chart_name}"
79
+ result.nodes.append(GraphNode(
80
+ id=chart_node_id,
81
+ kind=NodeKind.MODULE,
82
+ label=f"helm:{chart_name}",
83
+ fqn=f"helm:{chart_name}:{chart_version}",
84
+ module=ctx.module_name,
85
+ location=SourceLocation(file_path=fp),
86
+ properties={
87
+ "chart_name": str(chart_name),
88
+ "chart_version": str(chart_version),
89
+ "type": "helm_chart",
90
+ },
91
+ ))
92
+
93
+ # Process dependencies
94
+ dependencies = data.get("dependencies")
95
+ if isinstance(dependencies, list):
96
+ for dep in dependencies:
97
+ if not isinstance(dep, dict):
98
+ continue
99
+ dep_name = dep.get("name", "")
100
+ dep_version = dep.get("version", "")
101
+ dep_repo = dep.get("repository", "")
102
+ if not dep_name:
103
+ continue
104
+
105
+ dep_node_id = f"helm:{fp}:dep:{dep_name}"
106
+ result.nodes.append(GraphNode(
107
+ id=dep_node_id,
108
+ kind=NodeKind.MODULE,
109
+ label=f"helm-dep:{dep_name}",
110
+ fqn=f"helm:{dep_name}:{dep_version}",
111
+ module=ctx.module_name,
112
+ location=SourceLocation(file_path=fp),
113
+ properties={
114
+ "chart_name": str(dep_name),
115
+ "chart_version": str(dep_version),
116
+ "repository": str(dep_repo),
117
+ "type": "helm_dependency",
118
+ },
119
+ ))
120
+
121
+ result.edges.append(GraphEdge(
122
+ source=chart_node_id,
123
+ target=dep_node_id,
124
+ kind=EdgeKind.DEPENDS_ON,
125
+ label=f"{chart_name} depends on {dep_name}",
126
+ properties={"version": str(dep_version)},
127
+ ))
128
+
129
+ def _detect_values_yaml(
130
+ self, ctx: DetectorContext, result: DetectorResult
131
+ ) -> None:
132
+ """Parse values.yaml and emit CONFIG_KEY nodes for top-level keys."""
133
+ fp = ctx.file_path
134
+ data = _get_yaml_data(ctx)
135
+ if not data:
136
+ return
137
+
138
+ for key in sorted(data.keys()):
139
+ result.nodes.append(GraphNode(
140
+ id=f"helm:{fp}:value:{key}",
141
+ kind=NodeKind.CONFIG_KEY,
142
+ label=f"helm-value:{key}",
143
+ module=ctx.module_name,
144
+ location=SourceLocation(file_path=fp),
145
+ properties={"helm_value": True, "key": str(key)},
146
+ ))
147
+
148
+ def _detect_template(
149
+ self, ctx: DetectorContext, result: DetectorResult
150
+ ) -> None:
151
+ """Parse template files for .Values references and include directives."""
152
+ fp = ctx.file_path
153
+ text = decode_text(ctx)
154
+ lines = text.split("\n")
155
+ file_node_id = f"helm:{fp}:template"
156
+
157
+ seen_values: set[str] = set()
158
+ seen_includes: set[str] = set()
159
+
160
+ for i, line in enumerate(lines):
161
+ lineno = i + 1
162
+
163
+ # Detect {{ .Values.key }}
164
+ for m in _VALUES_REF_RE.finditer(line):
165
+ key = m.group(1)
166
+ if key not in seen_values:
167
+ seen_values.add(key)
168
+ result.edges.append(GraphEdge(
169
+ source=file_node_id,
170
+ target=f"helm:values:{key}",
171
+ kind=EdgeKind.READS_CONFIG,
172
+ label=f"reads .Values.{key}",
173
+ properties={"key": key, "line": lineno},
174
+ ))
175
+
176
+ # Detect {{ include "helper" }}
177
+ for m in _INCLUDE_RE.finditer(line):
178
+ helper = m.group(1)
179
+ if helper not in seen_includes:
180
+ seen_includes.add(helper)
181
+ result.edges.append(GraphEdge(
182
+ source=file_node_id,
183
+ target=f"helm:helper:{helper}",
184
+ kind=EdgeKind.IMPORTS,
185
+ label=f"includes {helper}",
186
+ properties={"helper": helper, "line": lineno},
187
+ ))
@@ -0,0 +1,101 @@
1
+ """Generic INI/CFG/CONF structure detector for all .ini/.cfg/.conf files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import configparser
6
+
7
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
8
+ from osscodeiq.detectors.utils import decode_text
9
+ from osscodeiq.models.graph import (
10
+ EdgeKind,
11
+ GraphEdge,
12
+ GraphNode,
13
+ NodeKind,
14
+ SourceLocation,
15
+ )
16
+
17
+
18
+ class IniStructureDetector:
19
+ """Detects INI file structures: sections, keys, and file identity."""
20
+
21
+ name: str = "ini_structure"
22
+ supported_languages: tuple[str, ...] = ("ini",)
23
+
24
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
25
+ result = DetectorResult()
26
+
27
+ file_id = f"ini:{ctx.file_path}"
28
+
29
+ # Create CONFIG_FILE node for the file itself
30
+ result.nodes.append(GraphNode(
31
+ id=file_id,
32
+ kind=NodeKind.CONFIG_FILE,
33
+ label=ctx.file_path,
34
+ fqn=ctx.file_path,
35
+ module=ctx.module_name,
36
+ location=SourceLocation(
37
+ file_path=ctx.file_path,
38
+ line_start=1,
39
+ ),
40
+ properties={"format": "ini"},
41
+ ))
42
+
43
+ # Parse INI from raw content
44
+ try:
45
+ text = decode_text(ctx)
46
+ parser = configparser.ConfigParser()
47
+ parser.read_string(text)
48
+ except Exception:
49
+ return result
50
+
51
+ for section in parser.sections():
52
+ section_id = f"ini:{ctx.file_path}:{section}"
53
+
54
+ # Create CONFIG_KEY node for the section
55
+ result.nodes.append(GraphNode(
56
+ id=section_id,
57
+ kind=NodeKind.CONFIG_KEY,
58
+ label=section,
59
+ fqn=f"{ctx.file_path}:{section}",
60
+ module=ctx.module_name,
61
+ location=SourceLocation(
62
+ file_path=ctx.file_path,
63
+ ),
64
+ properties={"section": True},
65
+ ))
66
+
67
+ result.edges.append(GraphEdge(
68
+ source=file_id,
69
+ target=section_id,
70
+ kind=EdgeKind.CONTAINS,
71
+ label=f"{ctx.file_path} contains [{section}]",
72
+ ))
73
+
74
+ # Create CONFIG_KEY nodes for keys within the section
75
+ for key in parser.options(section):
76
+ # Skip keys inherited from DEFAULT
77
+ if section != "DEFAULT" and key in parser.defaults() and parser.get(section, key) == parser.defaults()[key]:
78
+ continue
79
+
80
+ key_id = f"ini:{ctx.file_path}:{section}:{key}"
81
+
82
+ result.nodes.append(GraphNode(
83
+ id=key_id,
84
+ kind=NodeKind.CONFIG_KEY,
85
+ label=key,
86
+ fqn=f"{ctx.file_path}:{section}:{key}",
87
+ module=ctx.module_name,
88
+ location=SourceLocation(
89
+ file_path=ctx.file_path,
90
+ ),
91
+ properties={"section": section},
92
+ ))
93
+
94
+ result.edges.append(GraphEdge(
95
+ source=section_id,
96
+ target=key_id,
97
+ kind=EdgeKind.CONTAINS,
98
+ label=f"[{section}] contains {key}",
99
+ ))
100
+
101
+ return result
@@ -0,0 +1,72 @@
1
+ """Generic JSON structure detector for all .json files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
6
+ from osscodeiq.models.graph import (
7
+ EdgeKind,
8
+ GraphEdge,
9
+ GraphNode,
10
+ NodeKind,
11
+ SourceLocation,
12
+ )
13
+
14
+
15
+ class JsonStructureDetector:
16
+ """Detects JSON file structures: top-level keys and file identity."""
17
+
18
+ name: str = "json_structure"
19
+ supported_languages: tuple[str, ...] = ("json",)
20
+
21
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
22
+ result = DetectorResult()
23
+
24
+ file_id = f"json:{ctx.file_path}"
25
+
26
+ # Create CONFIG_FILE node for the file itself
27
+ result.nodes.append(GraphNode(
28
+ id=file_id,
29
+ kind=NodeKind.CONFIG_FILE,
30
+ label=ctx.file_path,
31
+ fqn=ctx.file_path,
32
+ module=ctx.module_name,
33
+ location=SourceLocation(
34
+ file_path=ctx.file_path,
35
+ line_start=1,
36
+ ),
37
+ properties={"format": "json"},
38
+ ))
39
+
40
+ # Extract data from parsed_data
41
+ data = None
42
+ if isinstance(ctx.parsed_data, dict):
43
+ data = ctx.parsed_data.get("data")
44
+
45
+ if data is None:
46
+ return result
47
+
48
+ # Only extract top-level keys from dicts
49
+ if isinstance(data, dict):
50
+ for key in data:
51
+ key_str = str(key)
52
+ key_id = f"json:{ctx.file_path}:{key_str}"
53
+
54
+ result.nodes.append(GraphNode(
55
+ id=key_id,
56
+ kind=NodeKind.CONFIG_KEY,
57
+ label=key_str,
58
+ fqn=f"{ctx.file_path}:{key_str}",
59
+ module=ctx.module_name,
60
+ location=SourceLocation(
61
+ file_path=ctx.file_path,
62
+ ),
63
+ ))
64
+
65
+ result.edges.append(GraphEdge(
66
+ source=file_id,
67
+ target=key_id,
68
+ kind=EdgeKind.CONTAINS,
69
+ label=f"{ctx.file_path} contains {key_str}",
70
+ ))
71
+
72
+ return result