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
osscodeiq/flow/views.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""Flow view generators — each produces a small, clean FlowDiagram from the full graph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from osscodeiq.flow.models import FlowDiagram, FlowEdge, FlowNode, FlowSubgraph
|
|
6
|
+
from osscodeiq.graph.store import GraphStore
|
|
7
|
+
from osscodeiq.models.graph import EdgeKind, NodeKind
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_GITLAB_PREFIX = "gitlab:"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_overview(store: GraphStore) -> FlowDiagram:
|
|
14
|
+
"""High-level overview with 4 subgraphs: CI, Infrastructure, Application, Security."""
|
|
15
|
+
subgraphs = []
|
|
16
|
+
edges = []
|
|
17
|
+
|
|
18
|
+
# CI/CD subgraph
|
|
19
|
+
ci_nodes = []
|
|
20
|
+
workflows = [n for n in store.all_nodes() if n.kind == NodeKind.MODULE and any(
|
|
21
|
+
p in n.id for p in ("gha:", _GITLAB_PREFIX)
|
|
22
|
+
)]
|
|
23
|
+
ci_jobs = [n for n in store.all_nodes() if n.kind == NodeKind.METHOD and any(
|
|
24
|
+
p in n.id for p in ("gha:", _GITLAB_PREFIX)
|
|
25
|
+
)]
|
|
26
|
+
if workflows or ci_jobs:
|
|
27
|
+
ci_nodes.append(FlowNode(id="ci_pipelines", label=f"Pipelines x{len(workflows)}", kind="pipeline",
|
|
28
|
+
properties={"count": len(workflows)}))
|
|
29
|
+
if ci_jobs:
|
|
30
|
+
ci_nodes.append(FlowNode(id="ci_jobs", label=f"Jobs x{len(ci_jobs)}", kind="job",
|
|
31
|
+
properties={"count": len(ci_jobs)}))
|
|
32
|
+
edges.append(FlowEdge(source="ci_pipelines", target="ci_jobs"))
|
|
33
|
+
subgraphs.append(FlowSubgraph(id="ci", label="CI/CD Pipeline", nodes=ci_nodes, drill_down_view="ci"))
|
|
34
|
+
|
|
35
|
+
# Infrastructure subgraph
|
|
36
|
+
infra_nodes_raw = store.nodes_by_kind(NodeKind.INFRA_RESOURCE) + store.nodes_by_kind(NodeKind.AZURE_RESOURCE)
|
|
37
|
+
if infra_nodes_raw:
|
|
38
|
+
# Group by type from properties or id prefix
|
|
39
|
+
k8s = [n for n in infra_nodes_raw if "k8s:" in n.id]
|
|
40
|
+
docker = [n for n in infra_nodes_raw if "compose:" in n.id or "dockerfile" in n.id.lower()]
|
|
41
|
+
terraform = [n for n in infra_nodes_raw if "tf:" in n.id]
|
|
42
|
+
other_infra = [n for n in infra_nodes_raw if n not in k8s and n not in docker and n not in terraform]
|
|
43
|
+
|
|
44
|
+
infra_flow_nodes = []
|
|
45
|
+
if k8s:
|
|
46
|
+
infra_flow_nodes.append(FlowNode(id="infra_k8s", label=f"K8s Resources x{len(k8s)}", kind="k8s",
|
|
47
|
+
properties={"count": len(k8s)}))
|
|
48
|
+
if docker:
|
|
49
|
+
infra_flow_nodes.append(FlowNode(id="infra_docker", label=f"Docker x{len(docker)}", kind="docker",
|
|
50
|
+
properties={"count": len(docker)}))
|
|
51
|
+
if terraform:
|
|
52
|
+
infra_flow_nodes.append(FlowNode(id="infra_tf", label=f"Terraform x{len(terraform)}", kind="terraform",
|
|
53
|
+
properties={"count": len(terraform)}))
|
|
54
|
+
if other_infra:
|
|
55
|
+
infra_flow_nodes.append(FlowNode(id="infra_other", label=f"Infra x{len(other_infra)}", kind="infra",
|
|
56
|
+
properties={"count": len(other_infra)}))
|
|
57
|
+
if infra_flow_nodes:
|
|
58
|
+
subgraphs.append(FlowSubgraph(id="infra", label="Infrastructure", nodes=infra_flow_nodes, drill_down_view="deploy"))
|
|
59
|
+
|
|
60
|
+
# Application subgraph
|
|
61
|
+
endpoints = store.nodes_by_kind(NodeKind.ENDPOINT)
|
|
62
|
+
entities = store.nodes_by_kind(NodeKind.ENTITY)
|
|
63
|
+
classes = store.nodes_by_kind(NodeKind.CLASS)
|
|
64
|
+
methods = store.nodes_by_kind(NodeKind.METHOD)
|
|
65
|
+
# Exclude CI methods from method count
|
|
66
|
+
app_methods = [m for m in methods if not any(p in m.id for p in ("gha:", _GITLAB_PREFIX))]
|
|
67
|
+
components = store.nodes_by_kind(NodeKind.COMPONENT)
|
|
68
|
+
topics = store.nodes_by_kind(NodeKind.TOPIC) + store.nodes_by_kind(NodeKind.QUEUE)
|
|
69
|
+
db_conns = store.nodes_by_kind(NodeKind.DATABASE_CONNECTION)
|
|
70
|
+
|
|
71
|
+
app_nodes = []
|
|
72
|
+
if endpoints:
|
|
73
|
+
app_nodes.append(FlowNode(id="app_endpoints", label=f"Endpoints x{len(endpoints)}", kind="endpoint",
|
|
74
|
+
properties={"count": len(endpoints)}))
|
|
75
|
+
if entities:
|
|
76
|
+
app_nodes.append(FlowNode(id="app_entities", label=f"Entities x{len(entities)}", kind="entity",
|
|
77
|
+
properties={"count": len(entities)}))
|
|
78
|
+
if components:
|
|
79
|
+
app_nodes.append(FlowNode(id="app_components", label=f"Components x{len(components)}", kind="component",
|
|
80
|
+
properties={"count": len(components)}))
|
|
81
|
+
if topics:
|
|
82
|
+
app_nodes.append(FlowNode(id="app_messaging", label=f"Topics/Queues x{len(topics)}", kind="messaging",
|
|
83
|
+
properties={"count": len(topics)}))
|
|
84
|
+
if db_conns:
|
|
85
|
+
app_nodes.append(FlowNode(id="app_database", label=f"DB Connections x{len(db_conns)}", kind="database",
|
|
86
|
+
properties={"count": len(db_conns)}))
|
|
87
|
+
if not app_nodes and (classes or app_methods):
|
|
88
|
+
app_nodes.append(FlowNode(id="app_code", label=f"Classes x{len(classes)}, Methods x{len(app_methods)}", kind="code",
|
|
89
|
+
properties={"classes": len(classes), "methods": len(app_methods)}))
|
|
90
|
+
if app_nodes:
|
|
91
|
+
subgraphs.append(FlowSubgraph(id="app", label="Application", nodes=app_nodes, drill_down_view="runtime"))
|
|
92
|
+
# Add internal edges
|
|
93
|
+
if endpoints and entities:
|
|
94
|
+
edges.append(FlowEdge(source="app_endpoints", target="app_entities", label="queries"))
|
|
95
|
+
if endpoints and any(n.id == "app_messaging" for n in app_nodes):
|
|
96
|
+
edges.append(FlowEdge(source="app_endpoints", target="app_messaging", style="dotted"))
|
|
97
|
+
|
|
98
|
+
# Security subgraph
|
|
99
|
+
guards = store.nodes_by_kind(NodeKind.GUARD)
|
|
100
|
+
middleware = store.nodes_by_kind(NodeKind.MIDDLEWARE)
|
|
101
|
+
if guards or middleware:
|
|
102
|
+
sec_nodes = []
|
|
103
|
+
if guards:
|
|
104
|
+
sec_nodes.append(FlowNode(id="sec_guards", label=f"Auth Guards x{len(guards)}", kind="guard",
|
|
105
|
+
properties={"count": len(guards)}))
|
|
106
|
+
if middleware:
|
|
107
|
+
sec_nodes.append(FlowNode(id="sec_middleware", label=f"Middleware x{len(middleware)}", kind="middleware",
|
|
108
|
+
properties={"count": len(middleware)}))
|
|
109
|
+
subgraphs.append(FlowSubgraph(id="security", label="Security", nodes=sec_nodes, drill_down_view="auth"))
|
|
110
|
+
# Guards protect endpoints
|
|
111
|
+
if guards and endpoints:
|
|
112
|
+
edges.append(FlowEdge(source="sec_guards", target="app_endpoints", label="protects", style="thick"))
|
|
113
|
+
|
|
114
|
+
# Cross-subgraph edges
|
|
115
|
+
if ci_nodes and infra_nodes_raw:
|
|
116
|
+
infra_flow_nodes_local = [sg for sg in subgraphs if sg.id == "infra"]
|
|
117
|
+
if infra_flow_nodes_local and infra_flow_nodes_local[0].nodes:
|
|
118
|
+
first_infra = infra_flow_nodes_local[0].nodes[0].id
|
|
119
|
+
edges.append(FlowEdge(source="ci_jobs" if ci_jobs else "ci_pipelines", target=first_infra, label="deploys"))
|
|
120
|
+
if infra_nodes_raw and app_nodes:
|
|
121
|
+
infra_flow_nodes_local = [sg for sg in subgraphs if sg.id == "infra"]
|
|
122
|
+
if infra_flow_nodes_local and infra_flow_nodes_local[0].nodes:
|
|
123
|
+
first_infra = infra_flow_nodes_local[0].nodes[0].id
|
|
124
|
+
edges.append(FlowEdge(source=first_infra, target=app_nodes[0].id, label="hosts"))
|
|
125
|
+
|
|
126
|
+
stats = {
|
|
127
|
+
"total_nodes": store.node_count,
|
|
128
|
+
"total_edges": store.edge_count,
|
|
129
|
+
"endpoints": len(endpoints),
|
|
130
|
+
"entities": len(entities),
|
|
131
|
+
"guards": len(guards),
|
|
132
|
+
"components": len(components),
|
|
133
|
+
"infra_resources": len(infra_nodes_raw),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return FlowDiagram(title="Architecture Overview", view="overview", subgraphs=subgraphs, edges=edges, stats=stats)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def build_ci_view(store: GraphStore) -> FlowDiagram:
|
|
140
|
+
"""CI/CD pipeline detail -- shows workflows, jobs, dependencies."""
|
|
141
|
+
subgraphs = []
|
|
142
|
+
edges = []
|
|
143
|
+
|
|
144
|
+
# Find CI-related nodes
|
|
145
|
+
all_nodes = store.all_nodes()
|
|
146
|
+
workflows = sorted([n for n in all_nodes if n.kind == NodeKind.MODULE and any(p in n.id for p in ("gha:", _GITLAB_PREFIX))], key=lambda n: n.id)
|
|
147
|
+
jobs = sorted([n for n in all_nodes if n.kind == NodeKind.METHOD and any(p in n.id for p in ("gha:", _GITLAB_PREFIX))], key=lambda n: n.id)
|
|
148
|
+
triggers = sorted([n for n in all_nodes if n.kind == NodeKind.CONFIG_KEY and any(p in n.id for p in ("gha:", _GITLAB_PREFIX))], key=lambda n: n.id)
|
|
149
|
+
|
|
150
|
+
# Trigger nodes
|
|
151
|
+
if triggers:
|
|
152
|
+
trigger_flow = [FlowNode(id=f"trigger_{i}", label=t.label, kind="trigger",
|
|
153
|
+
properties={"source_id": t.id}) for i, t in enumerate(triggers[:10])]
|
|
154
|
+
subgraphs.append(FlowSubgraph(id="triggers", label="Triggers", nodes=trigger_flow))
|
|
155
|
+
|
|
156
|
+
# Group jobs by workflow
|
|
157
|
+
jobs_by_workflow: dict[str, list] = {}
|
|
158
|
+
for job in jobs:
|
|
159
|
+
# Determine workflow from job's module or id prefix
|
|
160
|
+
wf_id = job.module or (job.id.rsplit(":job:", 1)[0] if ":job:" in job.id else "unknown")
|
|
161
|
+
jobs_by_workflow.setdefault(wf_id, []).append(job)
|
|
162
|
+
|
|
163
|
+
for wf in workflows:
|
|
164
|
+
wf_jobs = jobs_by_workflow.get(wf.id, [])
|
|
165
|
+
job_nodes = [FlowNode(id=f"job_{j.id.replace(':', '_')}", label=j.label, kind="job",
|
|
166
|
+
properties={k: v for k, v in j.properties.items() if k in ("stage", "runs_on", "image")})
|
|
167
|
+
for j in wf_jobs[:20]]
|
|
168
|
+
subgraphs.append(FlowSubgraph(id=f"wf_{wf.id.replace(':', '_')}", label=wf.label, nodes=job_nodes))
|
|
169
|
+
|
|
170
|
+
# Job dependency edges
|
|
171
|
+
ci_edges = [e for e in store.all_edges() if e.kind == EdgeKind.DEPENDS_ON and any(p in e.source for p in ("gha:", _GITLAB_PREFIX))]
|
|
172
|
+
for e in sorted(ci_edges, key=lambda x: (x.source, x.target)):
|
|
173
|
+
edges.append(FlowEdge(source=f"job_{e.source.replace(':', '_')}", target=f"job_{e.target.replace(':', '_')}", label="needs"))
|
|
174
|
+
|
|
175
|
+
# Trigger -> workflow edges
|
|
176
|
+
if triggers and workflows:
|
|
177
|
+
for wf in workflows:
|
|
178
|
+
edges.append(FlowEdge(source="trigger_0", target=f"wf_{wf.id.replace(':', '_')}", style="dotted"))
|
|
179
|
+
|
|
180
|
+
return FlowDiagram(title="CI/CD Pipeline", view="ci", direction="TD", subgraphs=subgraphs, edges=edges,
|
|
181
|
+
stats={"workflows": len(workflows), "jobs": len(jobs), "triggers": len(triggers)})
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def build_deploy_view(store: GraphStore) -> FlowDiagram:
|
|
185
|
+
"""Deployment topology -- K8s, Docker, Terraform resources."""
|
|
186
|
+
subgraphs = []
|
|
187
|
+
edges = []
|
|
188
|
+
|
|
189
|
+
all_nodes = store.all_nodes()
|
|
190
|
+
all_edges = store.all_edges()
|
|
191
|
+
infra = sorted([n for n in all_nodes if n.kind in (NodeKind.INFRA_RESOURCE, NodeKind.AZURE_RESOURCE)], key=lambda n: n.id)
|
|
192
|
+
|
|
193
|
+
# Group by technology
|
|
194
|
+
k8s = [n for n in infra if "k8s:" in n.id]
|
|
195
|
+
compose = [n for n in infra if "compose:" in n.id]
|
|
196
|
+
tf = [n for n in infra if "tf:" in n.id]
|
|
197
|
+
docker = [n for n in infra if "dockerfile" in n.id.lower() or n.id.startswith("docker:")]
|
|
198
|
+
other = [n for n in infra if n not in k8s and n not in compose and n not in tf and n not in docker]
|
|
199
|
+
|
|
200
|
+
def _make_nodes(nodes, prefix, max_nodes=20):
|
|
201
|
+
return [FlowNode(id=f"{prefix}_{i}", label=n.label, kind=prefix,
|
|
202
|
+
properties={k: v for k, v in n.properties.items() if k in ("kind", "namespace", "image", "resource_type", "provider")})
|
|
203
|
+
for i, n in enumerate(nodes[:max_nodes])]
|
|
204
|
+
|
|
205
|
+
if k8s:
|
|
206
|
+
subgraphs.append(FlowSubgraph(id="k8s", label=f"Kubernetes ({len(k8s)} resources)", nodes=_make_nodes(k8s, "k8s")))
|
|
207
|
+
if compose:
|
|
208
|
+
subgraphs.append(FlowSubgraph(id="compose", label=f"Docker Compose ({len(compose)} services)", nodes=_make_nodes(compose, "compose")))
|
|
209
|
+
if tf:
|
|
210
|
+
subgraphs.append(FlowSubgraph(id="terraform", label=f"Terraform ({len(tf)} resources)", nodes=_make_nodes(tf, "tf")))
|
|
211
|
+
if docker:
|
|
212
|
+
subgraphs.append(FlowSubgraph(id="docker", label=f"Docker ({len(docker)} images)", nodes=_make_nodes(docker, "docker")))
|
|
213
|
+
if other:
|
|
214
|
+
subgraphs.append(FlowSubgraph(id="other_infra", label=f"Other ({len(other)})", nodes=_make_nodes(other, "other")))
|
|
215
|
+
|
|
216
|
+
# Add CONNECTS_TO and DEPENDS_ON edges between infra nodes
|
|
217
|
+
infra_ids = {n.id for n in infra}
|
|
218
|
+
for e in sorted(all_edges, key=lambda x: (x.source, x.target)):
|
|
219
|
+
if e.source in infra_ids and e.target in infra_ids and e.kind in (EdgeKind.CONNECTS_TO, EdgeKind.DEPENDS_ON):
|
|
220
|
+
# Map to flow node IDs
|
|
221
|
+
src_idx = next((i for i, n in enumerate(infra) if n.id == e.source), None)
|
|
222
|
+
tgt_idx = next((i for i, n in enumerate(infra) if n.id == e.target), None)
|
|
223
|
+
if src_idx is not None and tgt_idx is not None:
|
|
224
|
+
src_node = infra[src_idx]
|
|
225
|
+
tgt_node = infra[tgt_idx]
|
|
226
|
+
# Determine prefix and local index from group membership
|
|
227
|
+
src_prefix, src_local = _resolve_group_index(src_node, k8s, compose, tf, docker, other)
|
|
228
|
+
tgt_prefix, tgt_local = _resolve_group_index(tgt_node, k8s, compose, tf, docker, other)
|
|
229
|
+
edges.append(FlowEdge(source=f"{src_prefix}_{src_local}", target=f"{tgt_prefix}_{tgt_local}"))
|
|
230
|
+
|
|
231
|
+
return FlowDiagram(title="Deployment Topology", view="deploy", direction="TD", subgraphs=subgraphs, edges=edges,
|
|
232
|
+
stats={"k8s": len(k8s), "compose": len(compose), "terraform": len(tf), "docker": len(docker)})
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _resolve_group_index(node, k8s, compose, tf, docker, other):
|
|
236
|
+
"""Return (prefix, local_index) for a node within its technology group."""
|
|
237
|
+
if node in k8s:
|
|
238
|
+
return "k8s", k8s.index(node)
|
|
239
|
+
if node in compose:
|
|
240
|
+
return "compose", compose.index(node)
|
|
241
|
+
if node in tf:
|
|
242
|
+
return "tf", tf.index(node)
|
|
243
|
+
if node in docker:
|
|
244
|
+
return "docker", docker.index(node)
|
|
245
|
+
return "other", other.index(node)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def build_runtime_view(store: GraphStore) -> FlowDiagram:
|
|
249
|
+
"""Runtime architecture -- modules, endpoints, entities, messaging, grouped by layer."""
|
|
250
|
+
subgraphs = []
|
|
251
|
+
edges = []
|
|
252
|
+
|
|
253
|
+
endpoints = store.nodes_by_kind(NodeKind.ENDPOINT)
|
|
254
|
+
entities = store.nodes_by_kind(NodeKind.ENTITY)
|
|
255
|
+
topics = store.nodes_by_kind(NodeKind.TOPIC) + store.nodes_by_kind(NodeKind.QUEUE)
|
|
256
|
+
db_conns = store.nodes_by_kind(NodeKind.DATABASE_CONNECTION)
|
|
257
|
+
|
|
258
|
+
# Group by layer
|
|
259
|
+
frontend_nodes = []
|
|
260
|
+
backend_nodes = []
|
|
261
|
+
data_nodes = []
|
|
262
|
+
|
|
263
|
+
if endpoints:
|
|
264
|
+
# Group endpoints by layer
|
|
265
|
+
fe_ep = [e for e in endpoints if e.properties.get("layer") == "frontend"]
|
|
266
|
+
be_ep = [e for e in endpoints if e.properties.get("layer") != "frontend"]
|
|
267
|
+
if fe_ep:
|
|
268
|
+
frontend_nodes.append(FlowNode(id="rt_fe_endpoints", label=f"Frontend Routes x{len(fe_ep)}", kind="endpoint"))
|
|
269
|
+
if be_ep:
|
|
270
|
+
backend_nodes.append(FlowNode(id="rt_be_endpoints", label=f"API Endpoints x{len(be_ep)}", kind="endpoint",
|
|
271
|
+
properties={"count": len(be_ep)}))
|
|
272
|
+
|
|
273
|
+
components = store.nodes_by_kind(NodeKind.COMPONENT)
|
|
274
|
+
if components:
|
|
275
|
+
frontend_nodes.append(FlowNode(id="rt_components", label=f"Components x{len(components)}", kind="component"))
|
|
276
|
+
|
|
277
|
+
if entities:
|
|
278
|
+
data_nodes.append(FlowNode(id="rt_entities", label=f"Entities x{len(entities)}", kind="entity"))
|
|
279
|
+
if db_conns:
|
|
280
|
+
data_nodes.append(FlowNode(id="rt_database", label=f"DB Connections x{len(db_conns)}", kind="database"))
|
|
281
|
+
if topics:
|
|
282
|
+
backend_nodes.append(FlowNode(id="rt_messaging", label=f"Messaging x{len(topics)}", kind="messaging"))
|
|
283
|
+
|
|
284
|
+
if frontend_nodes:
|
|
285
|
+
subgraphs.append(FlowSubgraph(id="frontend", label="Frontend", nodes=frontend_nodes))
|
|
286
|
+
if backend_nodes:
|
|
287
|
+
subgraphs.append(FlowSubgraph(id="backend", label="Backend", nodes=backend_nodes))
|
|
288
|
+
if data_nodes:
|
|
289
|
+
subgraphs.append(FlowSubgraph(id="data", label="Data Layer", nodes=data_nodes))
|
|
290
|
+
|
|
291
|
+
# Edges
|
|
292
|
+
if frontend_nodes and backend_nodes:
|
|
293
|
+
fe_id = frontend_nodes[0].id
|
|
294
|
+
be_id = backend_nodes[0].id
|
|
295
|
+
edges.append(FlowEdge(source=fe_id, target=be_id, label="calls"))
|
|
296
|
+
if backend_nodes and data_nodes:
|
|
297
|
+
be_id = backend_nodes[0].id
|
|
298
|
+
dt_id = data_nodes[0].id
|
|
299
|
+
edges.append(FlowEdge(source=be_id, target=dt_id, label="queries"))
|
|
300
|
+
|
|
301
|
+
return FlowDiagram(title="Runtime Architecture", view="runtime", subgraphs=subgraphs, edges=edges,
|
|
302
|
+
stats={"endpoints": len(endpoints), "entities": len(entities), "components": len(components),
|
|
303
|
+
"topics": len(topics), "db_connections": len(db_conns)})
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def build_auth_view(store: GraphStore) -> FlowDiagram:
|
|
307
|
+
"""Auth overview -- guards, endpoints, protection coverage."""
|
|
308
|
+
subgraphs = []
|
|
309
|
+
edges = []
|
|
310
|
+
|
|
311
|
+
guards = sorted(store.nodes_by_kind(NodeKind.GUARD), key=lambda n: n.id)
|
|
312
|
+
middleware = sorted(store.nodes_by_kind(NodeKind.MIDDLEWARE), key=lambda n: n.id)
|
|
313
|
+
endpoints = sorted(store.nodes_by_kind(NodeKind.ENDPOINT), key=lambda n: n.id)
|
|
314
|
+
protects_edges = store.edges_by_kind(EdgeKind.PROTECTS)
|
|
315
|
+
|
|
316
|
+
protected_ids = {e.target for e in protects_edges}
|
|
317
|
+
protected_endpoints = [e for e in endpoints if e.id in protected_ids]
|
|
318
|
+
unprotected_endpoints = [e for e in endpoints if e.id not in protected_ids]
|
|
319
|
+
|
|
320
|
+
# Group guards by auth_type
|
|
321
|
+
guards_by_type: dict[str, list] = {}
|
|
322
|
+
for g in guards:
|
|
323
|
+
auth_type = g.properties.get("auth_type", "unknown")
|
|
324
|
+
guards_by_type.setdefault(auth_type, []).append(g)
|
|
325
|
+
|
|
326
|
+
guard_nodes = []
|
|
327
|
+
for auth_type, type_guards in sorted(guards_by_type.items()):
|
|
328
|
+
guard_nodes.append(FlowNode(id=f"auth_{auth_type}", label=f"{auth_type} x{len(type_guards)}", kind="guard",
|
|
329
|
+
properties={"auth_type": auth_type, "count": len(type_guards)}))
|
|
330
|
+
if middleware:
|
|
331
|
+
guard_nodes.append(FlowNode(id="auth_middleware", label=f"Middleware x{len(middleware)}", kind="middleware",
|
|
332
|
+
properties={"count": len(middleware)}))
|
|
333
|
+
if guard_nodes:
|
|
334
|
+
subgraphs.append(FlowSubgraph(id="guards", label="Auth Guards", nodes=guard_nodes))
|
|
335
|
+
|
|
336
|
+
# Endpoint coverage
|
|
337
|
+
ep_nodes = []
|
|
338
|
+
if protected_endpoints:
|
|
339
|
+
ep_nodes.append(FlowNode(id="ep_protected", label=f"Protected x{len(protected_endpoints)}", kind="endpoint",
|
|
340
|
+
style="success", properties={"count": len(protected_endpoints)}))
|
|
341
|
+
if unprotected_endpoints:
|
|
342
|
+
ep_nodes.append(FlowNode(id="ep_unprotected", label=f"Unprotected x{len(unprotected_endpoints)}", kind="endpoint",
|
|
343
|
+
style="danger", properties={"count": len(unprotected_endpoints)}))
|
|
344
|
+
if ep_nodes:
|
|
345
|
+
subgraphs.append(FlowSubgraph(id="endpoints", label="Endpoints", nodes=ep_nodes))
|
|
346
|
+
|
|
347
|
+
# Edges: guards -> protected
|
|
348
|
+
for gn in guard_nodes:
|
|
349
|
+
if any(n.id == "ep_protected" for n in ep_nodes):
|
|
350
|
+
edges.append(FlowEdge(source=gn.id, target="ep_protected", label="protects", style="thick"))
|
|
351
|
+
|
|
352
|
+
coverage = len(protected_endpoints) / len(endpoints) * 100 if endpoints else 0
|
|
353
|
+
|
|
354
|
+
return FlowDiagram(title="Auth & Security", view="auth", subgraphs=subgraphs, edges=edges,
|
|
355
|
+
stats={"guards": len(guards), "middleware": len(middleware),
|
|
356
|
+
"protected": len(protected_endpoints), "unprotected": len(unprotected_endpoints),
|
|
357
|
+
"coverage_pct": round(coverage, 1)})
|
|
File without changes
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Protocol definitions for graph storage backends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from osscodeiq.models.graph import (
|
|
8
|
+
EdgeKind,
|
|
9
|
+
GraphEdge,
|
|
10
|
+
GraphNode,
|
|
11
|
+
NodeKind,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@runtime_checkable
|
|
16
|
+
class GraphBackend(Protocol):
|
|
17
|
+
"""Contract that every graph storage backend must satisfy."""
|
|
18
|
+
|
|
19
|
+
def add_node(self, node: GraphNode) -> None: ...
|
|
20
|
+
def add_edge(self, edge: GraphEdge) -> None: ...
|
|
21
|
+
def clear(self) -> None: ...
|
|
22
|
+
def get_node(self, node_id: str) -> GraphNode | None: ...
|
|
23
|
+
def has_node(self, node_id: str) -> bool: ...
|
|
24
|
+
def get_edges_between(self, source: str, target: str) -> list[GraphEdge]: ...
|
|
25
|
+
def all_nodes(self) -> list[GraphNode]: ...
|
|
26
|
+
def all_edges(self) -> list[GraphEdge]: ...
|
|
27
|
+
def nodes_by_kind(self, kind: NodeKind) -> list[GraphNode]: ...
|
|
28
|
+
def edges_by_kind(self, kind: EdgeKind) -> list[GraphEdge]: ...
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def node_count(self) -> int: ...
|
|
32
|
+
@property
|
|
33
|
+
def edge_count(self) -> int: ...
|
|
34
|
+
|
|
35
|
+
def neighbors(
|
|
36
|
+
self, node_id: str,
|
|
37
|
+
edge_kinds: set[EdgeKind] | None = None,
|
|
38
|
+
direction: str = "both",
|
|
39
|
+
) -> list[str]: ...
|
|
40
|
+
|
|
41
|
+
def find_cycles(self, limit: int = 100) -> list[list[str]]: ...
|
|
42
|
+
def shortest_path(self, source: str, target: str) -> list[str] | None: ...
|
|
43
|
+
def subgraph(self, node_ids: set[str]) -> GraphBackend: ...
|
|
44
|
+
def update_node_properties(self, node_id: str, properties: dict[str, Any]) -> None: ...
|
|
45
|
+
def close(self) -> None: ...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@runtime_checkable
|
|
49
|
+
class CypherBackend(Protocol):
|
|
50
|
+
"""Optional capability for backends supporting Cypher queries."""
|
|
51
|
+
|
|
52
|
+
def query_cypher(self, cypher: str, params: dict[str, Any] | None = None) -> list[dict[str, Any]]: ...
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Graph backend factory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from osscodeiq.graph.backend import GraphBackend
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def create_backend(backend_name: str = "networkx", **kwargs) -> GraphBackend:
|
|
12
|
+
"""Create a graph backend by name."""
|
|
13
|
+
if backend_name == "networkx":
|
|
14
|
+
from osscodeiq.graph.backends.networkx import NetworkXBackend
|
|
15
|
+
return NetworkXBackend()
|
|
16
|
+
elif backend_name == "kuzu":
|
|
17
|
+
from osscodeiq.graph.backends.kuzu import KuzuBackend
|
|
18
|
+
return KuzuBackend(db_path=kwargs.get("path", ".code-intelligence/graph.kuzu"))
|
|
19
|
+
elif backend_name == "sqlite":
|
|
20
|
+
from osscodeiq.graph.backends.sqlite_backend import SqliteGraphBackend
|
|
21
|
+
return SqliteGraphBackend(db_path=kwargs.get("path", ".code-intelligence/graph.db"))
|
|
22
|
+
else:
|
|
23
|
+
raise ValueError(f"Unknown graph backend: {backend_name}")
|