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