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