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,183 @@
1
+ """AWS CloudFormation template detector.
2
+
3
+ Detects CloudFormation resources, parameters, outputs, and cross-resource 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.models.graph import (
13
+ EdgeKind,
14
+ GraphEdge,
15
+ GraphNode,
16
+ NodeKind,
17
+ SourceLocation,
18
+ )
19
+
20
+ # Pattern for !GetAtt in text-based detection
21
+ _GETATT_RE = re.compile(r"!GetAtt\s+(\w+)\.", re.MULTILINE)
22
+ _REF_RE = re.compile(r"!Ref\s+(\w+)", re.MULTILINE)
23
+
24
+
25
+ def _is_cfn_template(data: dict[str, Any]) -> bool:
26
+ """Check whether parsed data looks like a CloudFormation template."""
27
+ if "AWSTemplateFormatVersion" in data:
28
+ return True
29
+ resources = data.get("Resources")
30
+ if isinstance(resources, dict):
31
+ for _key, val in resources.items():
32
+ if isinstance(val, dict):
33
+ rtype = val.get("Type", "")
34
+ if isinstance(rtype, str) and rtype.startswith("AWS::"):
35
+ return True
36
+ return False
37
+
38
+
39
+ def _get_data(ctx: DetectorContext) -> dict[str, Any] | None:
40
+ """Extract data from parsed_data for YAML or JSON types."""
41
+ if not ctx.parsed_data:
42
+ return None
43
+
44
+ ptype = ctx.parsed_data.get("type")
45
+ if ptype in ("yaml", "json"):
46
+ data = ctx.parsed_data.get("data")
47
+ if isinstance(data, dict) and _is_cfn_template(data):
48
+ return data
49
+ return None
50
+
51
+
52
+ def _collect_refs(value: Any, refs: set[str]) -> None:
53
+ """Recursively collect Ref and Fn::GetAtt references from a value tree."""
54
+ if isinstance(value, dict):
55
+ if "Ref" in value:
56
+ ref = value["Ref"]
57
+ if isinstance(ref, str):
58
+ refs.add(ref)
59
+ if "Fn::GetAtt" in value:
60
+ getatt = value["Fn::GetAtt"]
61
+ if isinstance(getatt, list) and len(getatt) >= 1:
62
+ refs.add(str(getatt[0]))
63
+ elif isinstance(getatt, str) and "." in getatt:
64
+ refs.add(getatt.split(".")[0])
65
+ for v in value.values():
66
+ _collect_refs(v, refs)
67
+ elif isinstance(value, list):
68
+ for item in value:
69
+ _collect_refs(item, refs)
70
+
71
+
72
+ class CloudFormationDetector:
73
+ """Detects AWS CloudFormation resources, parameters, outputs, and dependencies."""
74
+
75
+ name: str = "cloudformation"
76
+ supported_languages: tuple[str, ...] = ("yaml", "json")
77
+
78
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
79
+ result = DetectorResult()
80
+ fp = ctx.file_path
81
+
82
+ data = _get_data(ctx)
83
+ if not data:
84
+ return result
85
+
86
+ resource_ids: set[str] = set()
87
+
88
+ # Process Resources
89
+ resources = data.get("Resources")
90
+ if isinstance(resources, dict):
91
+ for logical_id, resource in sorted(resources.items()):
92
+ if not isinstance(resource, dict):
93
+ continue
94
+
95
+ resource_type = resource.get("Type", "unknown")
96
+ node_id = f"cfn:{fp}:resource:{logical_id}"
97
+ resource_ids.add(logical_id)
98
+
99
+ result.nodes.append(GraphNode(
100
+ id=node_id,
101
+ kind=NodeKind.INFRA_RESOURCE,
102
+ label=f"{logical_id} ({resource_type})",
103
+ fqn=f"cfn:{logical_id}",
104
+ module=ctx.module_name,
105
+ location=SourceLocation(file_path=fp),
106
+ properties={
107
+ "logical_id": str(logical_id),
108
+ "resource_type": str(resource_type),
109
+ },
110
+ ))
111
+
112
+ # Collect Ref and Fn::GetAtt references within this resource
113
+ refs: set[str] = set()
114
+ _collect_refs(resource, refs)
115
+ # Remove self-reference
116
+ refs.discard(logical_id)
117
+
118
+ for ref in sorted(refs):
119
+ result.edges.append(GraphEdge(
120
+ source=node_id,
121
+ target=f"cfn:{fp}:resource:{ref}",
122
+ kind=EdgeKind.DEPENDS_ON,
123
+ label=f"{logical_id} -> {ref}",
124
+ properties={"ref_type": "Ref/GetAtt"},
125
+ ))
126
+
127
+ # Process Parameters
128
+ parameters = data.get("Parameters")
129
+ if isinstance(parameters, dict):
130
+ for param_name, param_def in sorted(parameters.items()):
131
+ if not isinstance(param_def, dict):
132
+ continue
133
+
134
+ param_type = param_def.get("Type", "String")
135
+ default = param_def.get("Default")
136
+ description = param_def.get("Description", "")
137
+
138
+ props: dict[str, Any] = {
139
+ "param_type": str(param_type),
140
+ "cfn_type": "parameter",
141
+ }
142
+ if default is not None:
143
+ props["default"] = str(default)
144
+ if description:
145
+ props["description"] = str(description)
146
+
147
+ result.nodes.append(GraphNode(
148
+ id=f"cfn:{fp}:parameter:{param_name}",
149
+ kind=NodeKind.CONFIG_DEFINITION,
150
+ label=f"param:{param_name}",
151
+ fqn=f"cfn:param:{param_name}",
152
+ module=ctx.module_name,
153
+ location=SourceLocation(file_path=fp),
154
+ properties=props,
155
+ ))
156
+
157
+ # Process Outputs
158
+ outputs = data.get("Outputs")
159
+ if isinstance(outputs, dict):
160
+ for output_name, output_def in sorted(outputs.items()):
161
+ if not isinstance(output_def, dict):
162
+ continue
163
+
164
+ description = output_def.get("Description", "")
165
+ props_out: dict[str, Any] = {"cfn_type": "output"}
166
+ if description:
167
+ props_out["description"] = str(description)
168
+
169
+ export = output_def.get("Export")
170
+ if isinstance(export, dict) and "Name" in export:
171
+ props_out["export_name"] = str(export["Name"])
172
+
173
+ result.nodes.append(GraphNode(
174
+ id=f"cfn:{fp}:output:{output_name}",
175
+ kind=NodeKind.CONFIG_DEFINITION,
176
+ label=f"output:{output_name}",
177
+ fqn=f"cfn:output:{output_name}",
178
+ module=ctx.module_name,
179
+ location=SourceLocation(file_path=fp),
180
+ properties=props_out,
181
+ ))
182
+
183
+ return result
@@ -0,0 +1,179 @@
1
+ """Docker Compose detector for container orchestration definitions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ from typing import Any
8
+
9
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
10
+ from osscodeiq.models.graph import (
11
+ EdgeKind,
12
+ GraphEdge,
13
+ GraphNode,
14
+ NodeKind,
15
+ SourceLocation,
16
+ )
17
+
18
+ _COMPOSE_FILENAME_RE = re.compile(
19
+ r"^(docker-compose|compose).*\.(yml|yaml)$", re.IGNORECASE
20
+ )
21
+
22
+
23
+ def _is_compose_file(ctx: DetectorContext) -> bool:
24
+ """Check whether the file is a Docker Compose file."""
25
+ basename = os.path.basename(ctx.file_path)
26
+ if _COMPOSE_FILENAME_RE.match(basename):
27
+ return True
28
+ # Fallback: check parsed data for compose-like structure
29
+ if ctx.parsed_data and ctx.parsed_data.get("type") == "yaml":
30
+ data = ctx.parsed_data.get("data")
31
+ if isinstance(data, dict) and "services" in data:
32
+ return True
33
+ return False
34
+
35
+
36
+ class DockerComposeDetector:
37
+ """Detects services, ports, volumes, networks, and dependencies from Docker Compose files."""
38
+
39
+ name: str = "docker_compose"
40
+ supported_languages: tuple[str, ...] = ("yaml",)
41
+
42
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
43
+ result = DetectorResult()
44
+
45
+ if not _is_compose_file(ctx):
46
+ return result
47
+
48
+ if not ctx.parsed_data:
49
+ return result
50
+
51
+ data = ctx.parsed_data.get("data")
52
+ if not isinstance(data, dict):
53
+ return result
54
+
55
+ services = data.get("services")
56
+ if not isinstance(services, dict):
57
+ return result
58
+
59
+ fp = ctx.file_path
60
+
61
+ # Build a set of known service IDs for edge resolution
62
+ service_ids: dict[str, str] = {}
63
+ for svc_name in services:
64
+ service_ids[svc_name] = f"compose:{fp}:service:{svc_name}"
65
+
66
+ for svc_name, svc_def in services.items():
67
+ if not isinstance(svc_def, dict):
68
+ continue
69
+
70
+ svc_id = service_ids[svc_name]
71
+
72
+ # Properties for the service node
73
+ props: dict[str, Any] = {}
74
+ if "image" in svc_def:
75
+ props["image"] = str(svc_def["image"])
76
+ build = svc_def.get("build")
77
+ if isinstance(build, str):
78
+ props["build_context"] = build
79
+ elif isinstance(build, dict) and "context" in build:
80
+ props["build_context"] = str(build["context"])
81
+
82
+ # INFRA_RESOURCE node for the service
83
+ result.nodes.append(GraphNode(
84
+ id=svc_id,
85
+ kind=NodeKind.INFRA_RESOURCE,
86
+ label=svc_name,
87
+ fqn=f"compose:{svc_name}",
88
+ module=ctx.module_name,
89
+ location=SourceLocation(file_path=fp),
90
+ properties=props,
91
+ ))
92
+
93
+ # Ports
94
+ ports = svc_def.get("ports")
95
+ if isinstance(ports, list):
96
+ for port_entry in ports:
97
+ port_str = str(port_entry)
98
+ result.nodes.append(GraphNode(
99
+ id=f"compose:{fp}:service:{svc_name}:port:{port_str}",
100
+ kind=NodeKind.CONFIG_KEY,
101
+ label=f"{svc_name} port {port_str}",
102
+ module=ctx.module_name,
103
+ location=SourceLocation(file_path=fp),
104
+ properties={"port": port_str},
105
+ ))
106
+
107
+ # depends_on
108
+ depends_on = svc_def.get("depends_on")
109
+ if isinstance(depends_on, list):
110
+ for dep in depends_on:
111
+ dep_str = str(dep)
112
+ if dep_str in service_ids:
113
+ result.edges.append(GraphEdge(
114
+ source=svc_id,
115
+ target=service_ids[dep_str],
116
+ kind=EdgeKind.DEPENDS_ON,
117
+ label=f"{svc_name} depends on {dep_str}",
118
+ ))
119
+ elif isinstance(depends_on, dict):
120
+ for dep_str in depends_on:
121
+ if dep_str in service_ids:
122
+ result.edges.append(GraphEdge(
123
+ source=svc_id,
124
+ target=service_ids[dep_str],
125
+ kind=EdgeKind.DEPENDS_ON,
126
+ label=f"{svc_name} depends on {dep_str}",
127
+ ))
128
+
129
+ # links
130
+ links = svc_def.get("links")
131
+ if isinstance(links, list):
132
+ for link in links:
133
+ link_name = str(link).split(":")[0]
134
+ if link_name in service_ids:
135
+ result.edges.append(GraphEdge(
136
+ source=svc_id,
137
+ target=service_ids[link_name],
138
+ kind=EdgeKind.CONNECTS_TO,
139
+ label=f"{svc_name} links to {link_name}",
140
+ ))
141
+
142
+ # Volumes
143
+ volumes = svc_def.get("volumes")
144
+ if isinstance(volumes, list):
145
+ for vol_entry in volumes:
146
+ vol_str = str(vol_entry) if not isinstance(vol_entry, dict) else vol_entry.get("source", str(vol_entry))
147
+ result.nodes.append(GraphNode(
148
+ id=f"compose:{fp}:service:{svc_name}:volume:{vol_str}",
149
+ kind=NodeKind.CONFIG_KEY,
150
+ label=f"{svc_name} volume {vol_str}",
151
+ module=ctx.module_name,
152
+ location=SourceLocation(file_path=fp),
153
+ properties={"volume": vol_str},
154
+ ))
155
+
156
+ # Networks
157
+ networks = svc_def.get("networks")
158
+ if isinstance(networks, list):
159
+ for net in networks:
160
+ result.nodes.append(GraphNode(
161
+ id=f"compose:{fp}:service:{svc_name}:network:{net}",
162
+ kind=NodeKind.CONFIG_KEY,
163
+ label=f"{svc_name} network {net}",
164
+ module=ctx.module_name,
165
+ location=SourceLocation(file_path=fp),
166
+ properties={"network": str(net)},
167
+ ))
168
+ elif isinstance(networks, dict):
169
+ for net_name in networks:
170
+ result.nodes.append(GraphNode(
171
+ id=f"compose:{fp}:service:{svc_name}:network:{net_name}",
172
+ kind=NodeKind.CONFIG_KEY,
173
+ label=f"{svc_name} network {net_name}",
174
+ module=ctx.module_name,
175
+ location=SourceLocation(file_path=fp),
176
+ properties={"network": str(net_name)},
177
+ ))
178
+
179
+ return result
@@ -0,0 +1,150 @@
1
+ """GitHub Actions workflow detector for CI/CD pipeline 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
+
17
+ def _is_github_actions_file(ctx: DetectorContext) -> bool:
18
+ """Check whether the file is a GitHub Actions workflow."""
19
+ return ".github/workflows/" in ctx.file_path
20
+
21
+
22
+ class GitHubActionsDetector:
23
+ """Detects workflows, jobs, triggers, and job dependencies from GitHub Actions YAML files."""
24
+
25
+ name: str = "github_actions"
26
+ supported_languages: tuple[str, ...] = ("yaml",)
27
+
28
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
29
+ result = DetectorResult()
30
+
31
+ if not _is_github_actions_file(ctx):
32
+ return result
33
+
34
+ if not ctx.parsed_data:
35
+ return result
36
+
37
+ data = ctx.parsed_data.get("data")
38
+ if not isinstance(data, dict):
39
+ return result
40
+
41
+ fp = ctx.file_path
42
+ workflow_id = f"gha:{fp}"
43
+
44
+ # Workflow MODULE node
45
+ workflow_name = data.get("name", fp)
46
+ result.nodes.append(GraphNode(
47
+ id=workflow_id,
48
+ kind=NodeKind.MODULE,
49
+ label=str(workflow_name),
50
+ fqn=workflow_id,
51
+ module=ctx.module_name,
52
+ location=SourceLocation(file_path=fp),
53
+ properties={"workflow_file": fp},
54
+ ))
55
+
56
+ # Trigger events from "on:" key
57
+ on_triggers = data.get("on") or data.get(True) # YAML parses bare "on" as True
58
+ if on_triggers is not None:
59
+ if isinstance(on_triggers, str):
60
+ # Simple form: on: push
61
+ result.nodes.append(GraphNode(
62
+ id=f"gha:{fp}:trigger:{on_triggers}",
63
+ kind=NodeKind.CONFIG_KEY,
64
+ label=f"trigger: {on_triggers}",
65
+ module=ctx.module_name,
66
+ location=SourceLocation(file_path=fp),
67
+ properties={"event": on_triggers},
68
+ ))
69
+ elif isinstance(on_triggers, list):
70
+ # List form: on: [push, pull_request]
71
+ for event in on_triggers:
72
+ event_str = str(event)
73
+ result.nodes.append(GraphNode(
74
+ id=f"gha:{fp}:trigger:{event_str}",
75
+ kind=NodeKind.CONFIG_KEY,
76
+ label=f"trigger: {event_str}",
77
+ module=ctx.module_name,
78
+ location=SourceLocation(file_path=fp),
79
+ properties={"event": event_str},
80
+ ))
81
+ elif isinstance(on_triggers, dict):
82
+ # Dict form: on: { push: {branches: [main]}, ... }
83
+ for event_name, event_config in on_triggers.items():
84
+ event_str = str(event_name)
85
+ props: dict[str, Any] = {"event": event_str}
86
+ if isinstance(event_config, dict):
87
+ props["config"] = event_config
88
+ result.nodes.append(GraphNode(
89
+ id=f"gha:{fp}:trigger:{event_str}",
90
+ kind=NodeKind.CONFIG_KEY,
91
+ label=f"trigger: {event_str}",
92
+ module=ctx.module_name,
93
+ location=SourceLocation(file_path=fp),
94
+ properties=props,
95
+ ))
96
+
97
+ # Jobs
98
+ jobs = data.get("jobs")
99
+ if not isinstance(jobs, dict):
100
+ return result
101
+
102
+ job_ids: dict[str, str] = {}
103
+ for job_name in jobs:
104
+ job_ids[job_name] = f"gha:{fp}:job:{job_name}"
105
+
106
+ for job_name, job_def in jobs.items():
107
+ if not isinstance(job_def, dict):
108
+ continue
109
+
110
+ job_id = job_ids[job_name]
111
+
112
+ props = {}
113
+ runs_on = job_def.get("runs-on")
114
+ if runs_on is not None:
115
+ props["runs_on"] = runs_on if isinstance(runs_on, str) else str(runs_on)
116
+
117
+ result.nodes.append(GraphNode(
118
+ id=job_id,
119
+ kind=NodeKind.METHOD,
120
+ label=job_def.get("name", job_name),
121
+ fqn=job_id,
122
+ module=ctx.module_name,
123
+ location=SourceLocation(file_path=fp),
124
+ properties=props,
125
+ ))
126
+
127
+ # CONTAINS edge: workflow -> job
128
+ result.edges.append(GraphEdge(
129
+ source=workflow_id,
130
+ target=job_id,
131
+ kind=EdgeKind.CONTAINS,
132
+ label=f"workflow contains job {job_name}",
133
+ ))
134
+
135
+ # Job dependencies via "needs"
136
+ needs = job_def.get("needs")
137
+ if isinstance(needs, str):
138
+ needs = [needs]
139
+ if isinstance(needs, list):
140
+ for dep in needs:
141
+ dep_str = str(dep)
142
+ if dep_str in job_ids:
143
+ result.edges.append(GraphEdge(
144
+ source=job_id,
145
+ target=job_ids[dep_str],
146
+ kind=EdgeKind.DEPENDS_ON,
147
+ label=f"job {job_name} needs {dep_str}",
148
+ ))
149
+
150
+ return result