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,124 @@
1
+ """Regex-based Kotlin structures detector for Kotlin source files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
8
+ from osscodeiq.detectors.utils import decode_text, find_line_number
9
+ from osscodeiq.models.graph import (
10
+ EdgeKind,
11
+ GraphEdge,
12
+ GraphNode,
13
+ NodeKind,
14
+ SourceLocation,
15
+ )
16
+
17
+ # FIX: Added sealed, enum, annotation, value, inline modifiers (original only
18
+ # had data, open, abstract).
19
+ _KOTLIN_IMPORT_RE = re.compile(r'^\s*import\s+([\w.]+)', re.MULTILINE)
20
+ _KOTLIN_CLASS_RE = re.compile(
21
+ r'^\s*(?:(?:data|open|abstract|sealed|enum|annotation|value|inline)\s+)*class\s+(\w+)'
22
+ r'(?:\s*(?:\(.*?\))?\s*:\s*([\w\s,.<>]+))?',
23
+ re.MULTILINE,
24
+ )
25
+ _KOTLIN_INTERFACE_RE = re.compile(r'^\s*interface\s+(\w+)', re.MULTILINE)
26
+ # FIX: Added inline fun and override fun matching (original missed inline fun).
27
+ _KOTLIN_FUN_RE = re.compile(
28
+ r'^\s*(?:(?:override|inline|private|protected|internal|public)\s+)*(?:fun|suspend\s+fun)\s+(\w+)\s*\(',
29
+ re.MULTILINE,
30
+ )
31
+ _KOTLIN_OBJECT_RE = re.compile(r'^\s*object\s+(\w+)', re.MULTILINE)
32
+
33
+
34
+
35
+ class KotlinStructuresDetector:
36
+ """Detects Kotlin imports, classes, interfaces, objects, and functions."""
37
+
38
+ name: str = "kotlin_structures"
39
+ supported_languages: tuple[str, ...] = ("kotlin",)
40
+
41
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
42
+ result = DetectorResult()
43
+ text = decode_text(ctx)
44
+ file_node_id = ctx.file_path
45
+
46
+ for m in _KOTLIN_IMPORT_RE.finditer(text):
47
+ target = m.group(1)
48
+ result.edges.append(GraphEdge(
49
+ source=file_node_id,
50
+ target=target,
51
+ kind=EdgeKind.IMPORTS,
52
+ label=f"{ctx.file_path} imports {target}",
53
+ ))
54
+
55
+ for m in _KOTLIN_CLASS_RE.finditer(text):
56
+ class_name = m.group(1)
57
+ supertypes_str = m.group(2)
58
+ node_id = f"{ctx.file_path}:{class_name}"
59
+ result.nodes.append(GraphNode(
60
+ id=node_id,
61
+ kind=NodeKind.CLASS,
62
+ label=class_name,
63
+ fqn=class_name,
64
+ module=ctx.module_name,
65
+ location=SourceLocation(
66
+ file_path=ctx.file_path,
67
+ line_start=find_line_number(text, m.start()),
68
+ ),
69
+ ))
70
+ if supertypes_str:
71
+ for st in supertypes_str.split(","):
72
+ st = st.strip().split("(")[0].split("<")[0].strip()
73
+ if st:
74
+ result.edges.append(GraphEdge(
75
+ source=node_id,
76
+ target=st,
77
+ kind=EdgeKind.EXTENDS,
78
+ label=f"{class_name} extends {st}",
79
+ ))
80
+
81
+ for m in _KOTLIN_INTERFACE_RE.finditer(text):
82
+ iface_name = m.group(1)
83
+ result.nodes.append(GraphNode(
84
+ id=f"{ctx.file_path}:{iface_name}",
85
+ kind=NodeKind.INTERFACE,
86
+ label=iface_name,
87
+ fqn=iface_name,
88
+ module=ctx.module_name,
89
+ location=SourceLocation(
90
+ file_path=ctx.file_path,
91
+ line_start=find_line_number(text, m.start()),
92
+ ),
93
+ ))
94
+
95
+ for m in _KOTLIN_OBJECT_RE.finditer(text):
96
+ obj_name = m.group(1)
97
+ result.nodes.append(GraphNode(
98
+ id=f"{ctx.file_path}:{obj_name}",
99
+ kind=NodeKind.CLASS,
100
+ label=obj_name,
101
+ fqn=obj_name,
102
+ module=ctx.module_name,
103
+ location=SourceLocation(
104
+ file_path=ctx.file_path,
105
+ line_start=find_line_number(text, m.start()),
106
+ ),
107
+ properties={"type": "object"},
108
+ ))
109
+
110
+ for m in _KOTLIN_FUN_RE.finditer(text):
111
+ fn_name = m.group(1)
112
+ result.nodes.append(GraphNode(
113
+ id=f"{ctx.file_path}:{fn_name}",
114
+ kind=NodeKind.METHOD,
115
+ label=fn_name,
116
+ fqn=fn_name,
117
+ module=ctx.module_name,
118
+ location=SourceLocation(
119
+ file_path=ctx.file_path,
120
+ line_start=find_line_number(text, m.start()),
121
+ ),
122
+ ))
123
+
124
+ return result
@@ -0,0 +1,163 @@
1
+ """Ktor route detector for Kotlin."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
8
+ from osscodeiq.detectors.utils import decode_text
9
+ from osscodeiq.models.graph import GraphEdge, GraphNode, NodeKind, SourceLocation
10
+
11
+
12
+ class KtorRouteDetector:
13
+ """Detects Ktor route definitions, routing blocks, install plugins, and authenticate guards."""
14
+
15
+ name: str = "ktor_routes"
16
+ supported_languages: tuple[str, ...] = ("kotlin",)
17
+
18
+ # get("/path") { ... } or post("/path") { ... }
19
+ _ENDPOINT_PATTERN = re.compile(
20
+ r"\b(get|post|put|delete|patch)\(\s*\"([^\"]+)\"\s*\)\s*\{"
21
+ )
22
+
23
+ # routing { ... } block
24
+ _ROUTING_PATTERN = re.compile(r"\brouting\s*\{")
25
+
26
+ # route("/prefix") { ... } for nested route prefixes
27
+ _ROUTE_PREFIX_PATTERN = re.compile(
28
+ r"\broute\(\s*\"([^\"]+)\"\s*\)\s*\{"
29
+ )
30
+
31
+ # install(FeatureName) { ... }
32
+ _INSTALL_PATTERN = re.compile(
33
+ r"\binstall\(\s*(\w+)\s*\)"
34
+ )
35
+
36
+ # authenticate("auth-name") { ... }
37
+ _AUTHENTICATE_PATTERN = re.compile(
38
+ r"\bauthenticate\(\s*\"([^\"]+)\"\s*\)\s*\{"
39
+ )
40
+
41
+ def _build_prefix_map(self, text: str) -> dict[int, str]:
42
+ """Build a map of line numbers to their accumulated route prefixes.
43
+
44
+ Tracks route() nesting by scanning open/close braces within route blocks.
45
+ """
46
+ prefixes: dict[int, str] = {}
47
+ active_prefixes: list[tuple[str, int]] = [] # (prefix, brace_depth)
48
+ brace_depth = 0
49
+
50
+ for i, line in enumerate(text.split("\n"), 1):
51
+ # Track brace depth
52
+ brace_depth += line.count("{") - line.count("}")
53
+
54
+ # Check for route prefix opening
55
+ route_match = self._ROUTE_PREFIX_PATTERN.search(line)
56
+ if route_match:
57
+ active_prefixes.append((route_match.group(1), brace_depth))
58
+
59
+ # Remove prefixes whose scope has closed
60
+ while active_prefixes and brace_depth < active_prefixes[-1][1]:
61
+ active_prefixes.pop()
62
+
63
+ # Build the combined prefix for this line
64
+ if active_prefixes:
65
+ prefixes[i] = "".join(p for p, _ in active_prefixes)
66
+
67
+ return prefixes
68
+
69
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
70
+ result = DetectorResult()
71
+ text = decode_text(ctx)
72
+ prefix_map = self._build_prefix_map(text)
73
+
74
+ # Detect routing { } blocks as MODULE nodes
75
+ for match in self._ROUTING_PATTERN.finditer(text):
76
+ line = text[: match.start()].count("\n") + 1
77
+ node_id = f"ktor:{ctx.file_path}:routing:{line}"
78
+ result.nodes.append(
79
+ GraphNode(
80
+ id=node_id,
81
+ kind=NodeKind.MODULE,
82
+ label="routing",
83
+ fqn=f"{ctx.file_path}::routing",
84
+ module=ctx.module_name,
85
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
86
+ properties={
87
+ "framework": "ktor",
88
+ "type": "router",
89
+ },
90
+ )
91
+ )
92
+
93
+ # Detect endpoint definitions: get("/path") { ... }
94
+ for match in self._ENDPOINT_PATTERN.finditer(text):
95
+ method = match.group(1).upper()
96
+ raw_path = match.group(2)
97
+ line = text[: match.start()].count("\n") + 1
98
+
99
+ # Prepend any accumulated route prefix
100
+ prefix = prefix_map.get(line, "")
101
+ path = prefix + raw_path
102
+
103
+ node_id = f"ktor:{ctx.file_path}:{method}:{path}:{line}"
104
+ result.nodes.append(
105
+ GraphNode(
106
+ id=node_id,
107
+ kind=NodeKind.ENDPOINT,
108
+ label=f"{method} {path}",
109
+ fqn=f"{ctx.file_path}::{method}:{path}",
110
+ module=ctx.module_name,
111
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
112
+ properties={
113
+ "protocol": "REST",
114
+ "http_method": method,
115
+ "path_pattern": path,
116
+ "framework": "ktor",
117
+ },
118
+ )
119
+ )
120
+
121
+ # Detect install(Feature) as MIDDLEWARE
122
+ for match in self._INSTALL_PATTERN.finditer(text):
123
+ feature_name = match.group(1)
124
+ line = text[: match.start()].count("\n") + 1
125
+
126
+ node_id = f"ktor:{ctx.file_path}:install:{feature_name}:{line}"
127
+ result.nodes.append(
128
+ GraphNode(
129
+ id=node_id,
130
+ kind=NodeKind.MIDDLEWARE,
131
+ label=f"install:{feature_name}",
132
+ fqn=f"{ctx.file_path}::install:{feature_name}",
133
+ module=ctx.module_name,
134
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
135
+ properties={
136
+ "framework": "ktor",
137
+ "feature": feature_name,
138
+ },
139
+ )
140
+ )
141
+
142
+ # Detect authenticate("name") { ... } as GUARD
143
+ for match in self._AUTHENTICATE_PATTERN.finditer(text):
144
+ auth_name = match.group(1)
145
+ line = text[: match.start()].count("\n") + 1
146
+
147
+ node_id = f"ktor:{ctx.file_path}:auth:{auth_name}:{line}"
148
+ result.nodes.append(
149
+ GraphNode(
150
+ id=node_id,
151
+ kind=NodeKind.GUARD,
152
+ label=f"authenticate:{auth_name}",
153
+ fqn=f"{ctx.file_path}::authenticate:{auth_name}",
154
+ module=ctx.module_name,
155
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
156
+ properties={
157
+ "framework": "ktor",
158
+ "auth_name": auth_name,
159
+ },
160
+ )
161
+ )
162
+
163
+ return result
File without changes
@@ -0,0 +1,153 @@
1
+ """Protocol Buffers structure detector for services, RPCs, and messages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
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
+ _SERVICE_RE = re.compile(r'service\s+(\w+)\s*\{')
18
+ _RPC_RE = re.compile(r'rpc\s+(\w+)\s*\((\w+)\)\s*returns\s*\((\w+)\)')
19
+ _MESSAGE_RE = re.compile(r'message\s+(\w+)\s*\{')
20
+ _IMPORT_RE = re.compile(r'import\s+"([^"]+)"')
21
+ _PACKAGE_RE = re.compile(r'package\s+([\w.]+)\s*;')
22
+
23
+
24
+ class ProtoStructureDetector:
25
+ """Detects Protobuf services, RPCs, messages, imports, and package declarations."""
26
+
27
+ name: str = "proto_structure"
28
+ supported_languages: tuple[str, ...] = ("proto",)
29
+
30
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
31
+ result = DetectorResult()
32
+
33
+ try:
34
+ text = decode_text(ctx)
35
+ except Exception:
36
+ return result
37
+
38
+ filepath = ctx.file_path
39
+ lines = text.split("\n")
40
+
41
+ # Package declaration
42
+ for i, line in enumerate(lines):
43
+ m = _PACKAGE_RE.search(line)
44
+ if m:
45
+ pkg_name = m.group(1)
46
+ result.nodes.append(GraphNode(
47
+ id=f"proto:{filepath}:package:{pkg_name}",
48
+ kind=NodeKind.CONFIG_KEY,
49
+ label=f"package {pkg_name}",
50
+ fqn=pkg_name,
51
+ module=ctx.module_name,
52
+ location=SourceLocation(
53
+ file_path=filepath,
54
+ line_start=i + 1,
55
+ ),
56
+ properties={"package": pkg_name},
57
+ ))
58
+ break
59
+
60
+ # Imports
61
+ for i, line in enumerate(lines):
62
+ m = _IMPORT_RE.search(line)
63
+ if m:
64
+ import_path = m.group(1)
65
+ result.edges.append(GraphEdge(
66
+ source=filepath,
67
+ target=import_path,
68
+ kind=EdgeKind.IMPORTS,
69
+ label=f"{filepath} imports {import_path}",
70
+ ))
71
+
72
+ # Services and RPCs — track current service for RPC scoping
73
+ current_service: str | None = None
74
+ brace_depth = 0
75
+
76
+ for i, line in enumerate(lines):
77
+ # Track service blocks
78
+ svc_match = _SERVICE_RE.search(line)
79
+ if svc_match:
80
+ svc_name = svc_match.group(1)
81
+ current_service = svc_name
82
+ brace_depth = 0
83
+
84
+ result.nodes.append(GraphNode(
85
+ id=f"proto:{filepath}:service:{svc_name}",
86
+ kind=NodeKind.INTERFACE,
87
+ label=svc_name,
88
+ fqn=svc_name,
89
+ module=ctx.module_name,
90
+ location=SourceLocation(
91
+ file_path=filepath,
92
+ line_start=i + 1,
93
+ ),
94
+ ))
95
+
96
+ # Track braces for service scope
97
+ if current_service is not None:
98
+ brace_depth += line.count("{") - line.count("}")
99
+ if brace_depth <= 0:
100
+ current_service = None
101
+
102
+ # RPCs
103
+ rpc_match = _RPC_RE.search(line)
104
+ if rpc_match:
105
+ method_name = rpc_match.group(1)
106
+ request_type = rpc_match.group(2)
107
+ response_type = rpc_match.group(3)
108
+ svc = current_service or "_unknown"
109
+
110
+ rpc_id = f"proto:{filepath}:rpc:{svc}:{method_name}"
111
+ result.nodes.append(GraphNode(
112
+ id=rpc_id,
113
+ kind=NodeKind.METHOD,
114
+ label=f"{svc}.{method_name}",
115
+ fqn=f"{svc}.{method_name}",
116
+ module=ctx.module_name,
117
+ location=SourceLocation(
118
+ file_path=filepath,
119
+ line_start=i + 1,
120
+ ),
121
+ properties={
122
+ "request_type": request_type,
123
+ "response_type": response_type,
124
+ },
125
+ ))
126
+
127
+ # RPC belongs to service
128
+ if current_service:
129
+ result.edges.append(GraphEdge(
130
+ source=f"proto:{filepath}:service:{current_service}",
131
+ target=rpc_id,
132
+ kind=EdgeKind.CONTAINS,
133
+ label=f"{current_service} contains {method_name}",
134
+ ))
135
+
136
+ # Messages
137
+ for i, line in enumerate(lines):
138
+ m = _MESSAGE_RE.search(line)
139
+ if m:
140
+ msg_name = m.group(1)
141
+ result.nodes.append(GraphNode(
142
+ id=f"proto:{filepath}:message:{msg_name}",
143
+ kind=NodeKind.PROTOCOL_MESSAGE,
144
+ label=msg_name,
145
+ fqn=msg_name,
146
+ module=ctx.module_name,
147
+ location=SourceLocation(
148
+ file_path=filepath,
149
+ line_start=i + 1,
150
+ ),
151
+ ))
152
+
153
+ return result
File without changes
@@ -0,0 +1,88 @@
1
+ """Celery task detector."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
8
+ from osscodeiq.detectors.utils import decode_text
9
+ from osscodeiq.models.graph import EdgeKind, GraphEdge, GraphNode, NodeKind, SourceLocation
10
+
11
+
12
+ class CeleryTaskDetector:
13
+ """Detects Celery task definitions and task invocations."""
14
+
15
+ name: str = "python.celery_tasks"
16
+ supported_languages: tuple[str, ...] = ("python",)
17
+
18
+ # @app.task or @shared_task or @celery.task
19
+ _TASK_DECORATOR = re.compile(
20
+ r"@(?:\w+\.)?(?:task|shared_task)\(?"
21
+ r"(?:.*?name\s*=\s*['\"]([^'\"]+)['\"])?"
22
+ r"[^)]*\)?\s*\n\s*def\s+(\w+)",
23
+ re.DOTALL,
24
+ )
25
+
26
+ # task.delay(...) or task.apply_async(...)
27
+ _TASK_CALL = re.compile(
28
+ r"(\w+)\.(delay|apply_async|s|si|signature)\("
29
+ )
30
+
31
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
32
+ result = DetectorResult()
33
+ text = decode_text(ctx)
34
+
35
+ # Detect task definitions (these are like queue consumers)
36
+ for match in self._TASK_DECORATOR.finditer(text):
37
+ task_name = match.group(1) or match.group(2)
38
+ func_name = match.group(2)
39
+ line = text[:match.start()].count("\n") + 1
40
+
41
+ # Create a queue node for the task
42
+ queue_id = f"queue:{ctx.module_name or ''}:celery:{task_name}"
43
+ result.nodes.append(GraphNode(
44
+ id=queue_id,
45
+ kind=NodeKind.QUEUE,
46
+ label=f"celery:{task_name}",
47
+ module=ctx.module_name,
48
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
49
+ properties={
50
+ "broker": "celery",
51
+ "task_name": task_name,
52
+ "function": func_name,
53
+ },
54
+ ))
55
+
56
+ # The task function consumes from this queue
57
+ method_id = f"method:{ctx.file_path}::{func_name}"
58
+ result.nodes.append(GraphNode(
59
+ id=method_id,
60
+ kind=NodeKind.METHOD,
61
+ label=func_name,
62
+ fqn=f"{ctx.file_path}::{func_name}",
63
+ module=ctx.module_name,
64
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
65
+ ))
66
+ result.edges.append(GraphEdge(
67
+ source=method_id,
68
+ target=queue_id,
69
+ kind=EdgeKind.CONSUMES,
70
+ label="celery_task",
71
+ ))
72
+
73
+ # Detect task invocations (producers)
74
+ for match in self._TASK_CALL.finditer(text):
75
+ task_ref = match.group(1)
76
+ call_type = match.group(2)
77
+ line = text[:match.start()].count("\n") + 1
78
+
79
+ queue_id = f"queue:{ctx.module_name or ''}:celery:{task_ref}"
80
+ caller_id = f"method:{ctx.file_path}::caller_l{line}"
81
+ result.edges.append(GraphEdge(
82
+ source=caller_id,
83
+ target=queue_id,
84
+ kind=EdgeKind.PRODUCES,
85
+ label=f"{task_ref}.{call_type}",
86
+ ))
87
+
88
+ return result
@@ -0,0 +1,132 @@
1
+ """Django authentication and authorization detector for Python source files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ from osscodeiq.detectors.base import DetectorContext, DetectorResult
8
+ from osscodeiq.detectors.utils import decode_text
9
+ from osscodeiq.models.graph import GraphNode, NodeKind, SourceLocation
10
+
11
+ # @login_required
12
+ _LOGIN_REQUIRED_RE = re.compile(r'@login_required\b')
13
+
14
+ # @permission_required("app.can_edit") or @permission_required("app.can_edit", ...)
15
+ _PERMISSION_REQUIRED_RE = re.compile(
16
+ r'@permission_required\(\s*["\']([^"\']*)["\']'
17
+ )
18
+
19
+ # @user_passes_test(some_func) or @user_passes_test(lambda u: u.is_staff)
20
+ _USER_PASSES_TEST_RE = re.compile(
21
+ r'@user_passes_test\(\s*([^,)\s]+)'
22
+ )
23
+
24
+ # class MyView(LoginRequiredMixin, ...):
25
+ # class MyView(PermissionRequiredMixin, ...):
26
+ # class MyView(UserPassesTestMixin, ...):
27
+ _MIXIN_RE = re.compile(
28
+ r'class\s+(\w+)\s*\(([^)]*)\):'
29
+ )
30
+
31
+ _AUTH_MIXINS = {
32
+ "LoginRequiredMixin": "login_required",
33
+ "PermissionRequiredMixin": "permission_required",
34
+ "UserPassesTestMixin": "user_passes_test",
35
+ }
36
+
37
+
38
+ def _line_number(text: str, pos: int) -> int:
39
+ """Return 1-based line number for a character offset."""
40
+ return text[:pos].count("\n") + 1
41
+
42
+
43
+ class DjangoAuthDetector:
44
+ """Detects Django auth decorators and mixin patterns in Python source files."""
45
+
46
+ name: str = "django_auth"
47
+ supported_languages: tuple[str, ...] = ("python",)
48
+
49
+ def detect(self, ctx: DetectorContext) -> DetectorResult:
50
+ result = DetectorResult()
51
+ text = decode_text(ctx)
52
+
53
+ # @login_required
54
+ for m in _LOGIN_REQUIRED_RE.finditer(text):
55
+ line = _line_number(text, m.start())
56
+ result.nodes.append(GraphNode(
57
+ id=f"auth:{ctx.file_path}:login_required:{line}",
58
+ kind=NodeKind.GUARD,
59
+ label="@login_required",
60
+ module=ctx.module_name,
61
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
62
+ annotations=["@login_required"],
63
+ properties={
64
+ "auth_type": "django",
65
+ "permissions": [],
66
+ "auth_required": True,
67
+ },
68
+ ))
69
+
70
+ # @permission_required("perm")
71
+ for m in _PERMISSION_REQUIRED_RE.finditer(text):
72
+ line = _line_number(text, m.start())
73
+ permission = m.group(1)
74
+ result.nodes.append(GraphNode(
75
+ id=f"auth:{ctx.file_path}:permission_required:{line}",
76
+ kind=NodeKind.GUARD,
77
+ label=f"@permission_required({permission})",
78
+ module=ctx.module_name,
79
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
80
+ annotations=["@permission_required"],
81
+ properties={
82
+ "auth_type": "django",
83
+ "permissions": [permission],
84
+ "auth_required": True,
85
+ },
86
+ ))
87
+
88
+ # @user_passes_test(fn)
89
+ for m in _USER_PASSES_TEST_RE.finditer(text):
90
+ line = _line_number(text, m.start())
91
+ test_func = m.group(1)
92
+ result.nodes.append(GraphNode(
93
+ id=f"auth:{ctx.file_path}:user_passes_test:{line}",
94
+ kind=NodeKind.GUARD,
95
+ label=f"@user_passes_test({test_func})",
96
+ module=ctx.module_name,
97
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
98
+ annotations=["@user_passes_test"],
99
+ properties={
100
+ "auth_type": "django",
101
+ "permissions": [],
102
+ "test_function": test_func,
103
+ "auth_required": True,
104
+ },
105
+ ))
106
+
107
+ # Class-based views with auth mixins
108
+ for m in _MIXIN_RE.finditer(text):
109
+ class_name = m.group(1)
110
+ bases_str = m.group(2)
111
+ bases = [b.strip() for b in bases_str.split(",")]
112
+
113
+ for base in bases:
114
+ if base in _AUTH_MIXINS:
115
+ line = _line_number(text, m.start())
116
+ result.nodes.append(GraphNode(
117
+ id=f"auth:{ctx.file_path}:{base}:{line}",
118
+ kind=NodeKind.GUARD,
119
+ label=f"{class_name}({base})",
120
+ module=ctx.module_name,
121
+ location=SourceLocation(file_path=ctx.file_path, line_start=line),
122
+ annotations=[f"mixin:{base}"],
123
+ properties={
124
+ "auth_type": "django",
125
+ "permissions": [],
126
+ "mixin": base,
127
+ "class_name": class_name,
128
+ "auth_required": True,
129
+ },
130
+ ))
131
+
132
+ return result