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,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
|