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,151 @@
|
|
|
1
|
+
"""Mongoose ODM detector for TypeScript/JavaScript."""
|
|
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 MongooseORMDetector:
|
|
13
|
+
"""Detects Mongoose ODM usage patterns in TypeScript/JavaScript files."""
|
|
14
|
+
|
|
15
|
+
name: str = "mongoose_orm"
|
|
16
|
+
supported_languages: tuple[str, ...] = ("typescript", "javascript")
|
|
17
|
+
|
|
18
|
+
# mongoose.model('Name', schema)
|
|
19
|
+
_MODEL_RE = re.compile(r"mongoose\.model\s*\(\s*['\"](\w+)['\"]")
|
|
20
|
+
# new Schema({ or new mongoose.Schema({
|
|
21
|
+
_SCHEMA_RE = re.compile(r"(?:const|let|var)\s+(\w+)\s*=\s*new\s+(?:mongoose\.)?Schema\s*\(")
|
|
22
|
+
# mongoose.connect(
|
|
23
|
+
_CONNECT_RE = re.compile(r"mongoose\.connect\s*\(")
|
|
24
|
+
# Model query operations
|
|
25
|
+
_QUERY_RE = re.compile(
|
|
26
|
+
r"(\w+)\.(find|findOne|findById|findOneAndUpdate|findOneAndDelete"
|
|
27
|
+
r"|create|insertMany|updateOne|updateMany|deleteOne|deleteMany"
|
|
28
|
+
r"|countDocuments|aggregate)\s*\("
|
|
29
|
+
)
|
|
30
|
+
# schema.virtual('name')
|
|
31
|
+
_VIRTUAL_RE = re.compile(r"(\w+)\.virtual\s*\(\s*['\"](\w+)['\"]")
|
|
32
|
+
# schema.pre('save', ...) / schema.post('save', ...)
|
|
33
|
+
_HOOK_RE = re.compile(r"(\w+)\.(pre|post)\s*\(\s*['\"](\w+)['\"]")
|
|
34
|
+
|
|
35
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
36
|
+
result = DetectorResult()
|
|
37
|
+
text = decode_text(ctx)
|
|
38
|
+
|
|
39
|
+
seen_models: dict[str, str] = {}
|
|
40
|
+
schema_vars: set[str] = set()
|
|
41
|
+
|
|
42
|
+
# Detect mongoose.connect -> DATABASE_CONNECTION
|
|
43
|
+
for match in self._CONNECT_RE.finditer(text):
|
|
44
|
+
line = text[: match.start()].count("\n") + 1
|
|
45
|
+
node_id = f"mongoose:{ctx.file_path}:connection:{line}"
|
|
46
|
+
result.nodes.append(
|
|
47
|
+
GraphNode(
|
|
48
|
+
id=node_id,
|
|
49
|
+
kind=NodeKind.DATABASE_CONNECTION,
|
|
50
|
+
label="mongoose.connect",
|
|
51
|
+
fqn=f"{ctx.file_path}::mongoose.connect",
|
|
52
|
+
module=ctx.module_name,
|
|
53
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
54
|
+
properties={"framework": "mongoose"},
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Detect new Schema({ ... }) -> ENTITY for schema definition
|
|
59
|
+
for match in self._SCHEMA_RE.finditer(text):
|
|
60
|
+
var_name = match.group(1)
|
|
61
|
+
schema_vars.add(var_name)
|
|
62
|
+
line = text[: match.start()].count("\n") + 1
|
|
63
|
+
node_id = f"mongoose:{ctx.file_path}:schema:{var_name}"
|
|
64
|
+
result.nodes.append(
|
|
65
|
+
GraphNode(
|
|
66
|
+
id=node_id,
|
|
67
|
+
kind=NodeKind.ENTITY,
|
|
68
|
+
label=var_name,
|
|
69
|
+
fqn=f"{ctx.file_path}::{var_name}",
|
|
70
|
+
module=ctx.module_name,
|
|
71
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
72
|
+
properties={"framework": "mongoose", "definition": "schema"},
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Detect mongoose.model('Name', schema) -> ENTITY
|
|
77
|
+
for match in self._MODEL_RE.finditer(text):
|
|
78
|
+
model_name = match.group(1)
|
|
79
|
+
line = text[: match.start()].count("\n") + 1
|
|
80
|
+
model_id = f"mongoose:{ctx.file_path}:model:{model_name}"
|
|
81
|
+
seen_models[model_name] = model_id
|
|
82
|
+
result.nodes.append(
|
|
83
|
+
GraphNode(
|
|
84
|
+
id=model_id,
|
|
85
|
+
kind=NodeKind.ENTITY,
|
|
86
|
+
label=model_name,
|
|
87
|
+
fqn=f"{ctx.file_path}::{model_name}",
|
|
88
|
+
module=ctx.module_name,
|
|
89
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
90
|
+
properties={"framework": "mongoose", "definition": "model"},
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Detect schema.virtual('name') -> property on schema
|
|
95
|
+
virtuals: list[str] = []
|
|
96
|
+
for match in self._VIRTUAL_RE.finditer(text):
|
|
97
|
+
var_name = match.group(1)
|
|
98
|
+
virtual_name = match.group(2)
|
|
99
|
+
if var_name in schema_vars:
|
|
100
|
+
virtuals.append(virtual_name)
|
|
101
|
+
|
|
102
|
+
# Attach virtuals to schema nodes
|
|
103
|
+
if virtuals:
|
|
104
|
+
for node in result.nodes:
|
|
105
|
+
if node.properties.get("definition") == "schema":
|
|
106
|
+
node.properties["virtuals"] = virtuals
|
|
107
|
+
|
|
108
|
+
# Detect schema.pre/post hooks -> EVENT nodes
|
|
109
|
+
for match in self._HOOK_RE.finditer(text):
|
|
110
|
+
var_name = match.group(1)
|
|
111
|
+
hook_type = match.group(2)
|
|
112
|
+
event_name = match.group(3)
|
|
113
|
+
if var_name in schema_vars:
|
|
114
|
+
line = text[: match.start()].count("\n") + 1
|
|
115
|
+
event_id = f"mongoose:{ctx.file_path}:hook:{hook_type}:{event_name}:{line}"
|
|
116
|
+
result.nodes.append(
|
|
117
|
+
GraphNode(
|
|
118
|
+
id=event_id,
|
|
119
|
+
kind=NodeKind.EVENT,
|
|
120
|
+
label=f"{hook_type}:{event_name}",
|
|
121
|
+
fqn=f"{ctx.file_path}::{hook_type}:{event_name}",
|
|
122
|
+
module=ctx.module_name,
|
|
123
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
124
|
+
properties={
|
|
125
|
+
"framework": "mongoose",
|
|
126
|
+
"hook_type": hook_type,
|
|
127
|
+
"event": event_name,
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Detect query operations -> QUERIES edges
|
|
133
|
+
for match in self._QUERY_RE.finditer(text):
|
|
134
|
+
model_name = match.group(1)
|
|
135
|
+
operation = match.group(2)
|
|
136
|
+
line = text[: match.start()].count("\n") + 1
|
|
137
|
+
|
|
138
|
+
target_id = seen_models.get(
|
|
139
|
+
model_name, f"mongoose:{ctx.file_path}:model:{model_name}"
|
|
140
|
+
)
|
|
141
|
+
result.edges.append(
|
|
142
|
+
GraphEdge(
|
|
143
|
+
source=ctx.file_path,
|
|
144
|
+
target=target_id,
|
|
145
|
+
kind=EdgeKind.QUERIES,
|
|
146
|
+
label=f"{model_name}.{operation}",
|
|
147
|
+
properties={"operation": operation, "line": line},
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return result
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""NestJS controller 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 NestJSControllerDetector:
|
|
13
|
+
"""Detects NestJS controller decorators (@Controller, @Get, @Post, etc.)."""
|
|
14
|
+
|
|
15
|
+
name: str = "typescript.nestjs_controllers"
|
|
16
|
+
supported_languages: tuple[str, ...] = ("typescript",)
|
|
17
|
+
|
|
18
|
+
# @Controller('users') or @Controller('/api/users')
|
|
19
|
+
_CONTROLLER_PATTERN = re.compile(
|
|
20
|
+
r"@Controller\(\s*['\"`]?([^'\"`)\s]*)['\"`]?\s*\)\s*\n\s*(?:export\s+)?class\s+(\w+)"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# @Get('/:id'), @Post(), @Put('/:id'), @Delete('/:id'), @Patch('/:id')
|
|
24
|
+
_ROUTE_PATTERN = re.compile(
|
|
25
|
+
r"@(Get|Post|Put|Delete|Patch|Options|Head)\(\s*['\"`]?([^'\"`)\s]*)['\"`]?\s*\)"
|
|
26
|
+
r"\s*\n\s*(?:async\s+)?(\w+)"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
30
|
+
result = DetectorResult()
|
|
31
|
+
text = decode_text(ctx)
|
|
32
|
+
|
|
33
|
+
# Find controllers and their base paths
|
|
34
|
+
controllers: dict[str, str] = {}
|
|
35
|
+
for match in self._CONTROLLER_PATTERN.finditer(text):
|
|
36
|
+
base_path = match.group(1) or ""
|
|
37
|
+
class_name = match.group(2)
|
|
38
|
+
controllers[class_name] = base_path
|
|
39
|
+
line = text[:match.start()].count("\n") + 1
|
|
40
|
+
|
|
41
|
+
class_id = f"class:{ctx.file_path}::{class_name}"
|
|
42
|
+
result.nodes.append(GraphNode(
|
|
43
|
+
id=class_id,
|
|
44
|
+
kind=NodeKind.CLASS,
|
|
45
|
+
label=class_name,
|
|
46
|
+
fqn=f"{ctx.file_path}::{class_name}",
|
|
47
|
+
module=ctx.module_name,
|
|
48
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
49
|
+
annotations=["@Controller"],
|
|
50
|
+
properties={"framework": "nestjs", "stereotype": "controller"},
|
|
51
|
+
))
|
|
52
|
+
|
|
53
|
+
# Build sorted list of (line, class_name, base_path)
|
|
54
|
+
controller_ranges = []
|
|
55
|
+
for match in self._CONTROLLER_PATTERN.finditer(text):
|
|
56
|
+
ctrl_line = text[:match.start()].count("\n") + 1
|
|
57
|
+
controller_ranges.append((ctrl_line, match.group(2), match.group(1) or ""))
|
|
58
|
+
|
|
59
|
+
# For each route, find the controller it belongs to
|
|
60
|
+
for match in self._ROUTE_PATTERN.finditer(text):
|
|
61
|
+
route_line = text[:match.start()].count("\n") + 1
|
|
62
|
+
# Find the last controller that starts before this route
|
|
63
|
+
enclosing_ctrl = ("", "") # (class_name, base_path)
|
|
64
|
+
for ctrl_line, ctrl_name, ctrl_path in controller_ranges:
|
|
65
|
+
if ctrl_line <= route_line:
|
|
66
|
+
enclosing_ctrl = (ctrl_name, ctrl_path)
|
|
67
|
+
current_class, base_path = enclosing_ctrl
|
|
68
|
+
|
|
69
|
+
method = match.group(1).upper()
|
|
70
|
+
path = match.group(2) or ""
|
|
71
|
+
func_name = match.group(3)
|
|
72
|
+
|
|
73
|
+
full_path = f"/{base_path}/{path}".replace("//", "/").rstrip("/") or "/"
|
|
74
|
+
|
|
75
|
+
node_id = f"endpoint:{ctx.module_name or ''}:{method}:{full_path}"
|
|
76
|
+
result.nodes.append(GraphNode(
|
|
77
|
+
id=node_id,
|
|
78
|
+
kind=NodeKind.ENDPOINT,
|
|
79
|
+
label=f"{method} {full_path}",
|
|
80
|
+
fqn=f"{ctx.file_path}::{func_name}",
|
|
81
|
+
module=ctx.module_name,
|
|
82
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=route_line),
|
|
83
|
+
properties={
|
|
84
|
+
"protocol": "REST",
|
|
85
|
+
"http_method": method,
|
|
86
|
+
"path_pattern": full_path,
|
|
87
|
+
"framework": "nestjs",
|
|
88
|
+
},
|
|
89
|
+
))
|
|
90
|
+
|
|
91
|
+
if current_class:
|
|
92
|
+
class_id = f"class:{ctx.file_path}::{current_class}"
|
|
93
|
+
result.edges.append(GraphEdge(
|
|
94
|
+
source=class_id,
|
|
95
|
+
target=node_id,
|
|
96
|
+
kind=EdgeKind.EXPOSES,
|
|
97
|
+
))
|
|
98
|
+
|
|
99
|
+
return result
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""NestJS guard and role-based access control 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 GraphNode, NodeKind, SourceLocation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NestJSGuardsDetector:
|
|
13
|
+
"""Detects NestJS guard decorators, role decorators, and canActivate implementations."""
|
|
14
|
+
|
|
15
|
+
name: str = "typescript.nestjs_guards"
|
|
16
|
+
supported_languages: tuple[str, ...] = ("typescript",)
|
|
17
|
+
|
|
18
|
+
# @UseGuards(JwtAuthGuard) or @UseGuards(JwtAuthGuard, RolesGuard)
|
|
19
|
+
_USE_GUARDS_PATTERN = re.compile(
|
|
20
|
+
r"@UseGuards\(\s*([^)]+)\)"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# @Roles('admin', 'user') or @Roles("admin", "user")
|
|
24
|
+
_ROLES_PATTERN = re.compile(
|
|
25
|
+
r"@Roles\(\s*([^)]+)\)"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# canActivate(context): boolean/Promise<boolean>/Observable<boolean>
|
|
29
|
+
_CAN_ACTIVATE_PATTERN = re.compile(
|
|
30
|
+
r"(?:async\s+)?canActivate\s*\("
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# AuthGuard('jwt') or AuthGuard("local")
|
|
34
|
+
_AUTH_GUARD_PATTERN = re.compile(
|
|
35
|
+
r"AuthGuard\(\s*['\"](\w+)['\"]\s*\)"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _parse_roles(raw: str) -> list[str]:
|
|
40
|
+
"""Extract role strings from a @Roles(...) argument list."""
|
|
41
|
+
roles: list[str] = []
|
|
42
|
+
for match in re.finditer(r"""['"]([\w\-]+)['"]""", raw):
|
|
43
|
+
roles.append(match.group(1))
|
|
44
|
+
return roles
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _parse_guard_names(raw: str) -> list[str]:
|
|
48
|
+
"""Extract guard class names from a @UseGuards(...) argument list."""
|
|
49
|
+
names: list[str] = []
|
|
50
|
+
for token in raw.split(","):
|
|
51
|
+
token = token.strip()
|
|
52
|
+
if token and re.match(r"^\w+$", token):
|
|
53
|
+
names.append(token)
|
|
54
|
+
return names
|
|
55
|
+
|
|
56
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
57
|
+
result = DetectorResult()
|
|
58
|
+
text = decode_text(ctx)
|
|
59
|
+
|
|
60
|
+
# Detect @UseGuards(...)
|
|
61
|
+
for match in self._USE_GUARDS_PATTERN.finditer(text):
|
|
62
|
+
line = text[: match.start()].count("\n") + 1
|
|
63
|
+
guard_names = self._parse_guard_names(match.group(1))
|
|
64
|
+
for guard_name in guard_names:
|
|
65
|
+
node_id = f"auth:{ctx.file_path}:UseGuards({guard_name}):{line}"
|
|
66
|
+
result.nodes.append(GraphNode(
|
|
67
|
+
id=node_id,
|
|
68
|
+
kind=NodeKind.GUARD,
|
|
69
|
+
label=f"UseGuards({guard_name})",
|
|
70
|
+
fqn=f"{ctx.file_path}::UseGuards({guard_name})",
|
|
71
|
+
module=ctx.module_name,
|
|
72
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
73
|
+
annotations=["@UseGuards"],
|
|
74
|
+
properties={
|
|
75
|
+
"auth_type": "nestjs_guard",
|
|
76
|
+
"guard_name": guard_name,
|
|
77
|
+
"roles": [],
|
|
78
|
+
},
|
|
79
|
+
))
|
|
80
|
+
|
|
81
|
+
# Detect @Roles(...)
|
|
82
|
+
for match in self._ROLES_PATTERN.finditer(text):
|
|
83
|
+
line = text[: match.start()].count("\n") + 1
|
|
84
|
+
roles = self._parse_roles(match.group(1))
|
|
85
|
+
node_id = f"auth:{ctx.file_path}:Roles:{line}"
|
|
86
|
+
result.nodes.append(GraphNode(
|
|
87
|
+
id=node_id,
|
|
88
|
+
kind=NodeKind.GUARD,
|
|
89
|
+
label=f"Roles({', '.join(roles)})",
|
|
90
|
+
fqn=f"{ctx.file_path}::Roles",
|
|
91
|
+
module=ctx.module_name,
|
|
92
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
93
|
+
annotations=["@Roles"],
|
|
94
|
+
properties={
|
|
95
|
+
"auth_type": "nestjs_guard",
|
|
96
|
+
"roles": roles,
|
|
97
|
+
},
|
|
98
|
+
))
|
|
99
|
+
|
|
100
|
+
# Detect canActivate() implementations
|
|
101
|
+
for match in self._CAN_ACTIVATE_PATTERN.finditer(text):
|
|
102
|
+
line = text[: match.start()].count("\n") + 1
|
|
103
|
+
node_id = f"auth:{ctx.file_path}:canActivate:{line}"
|
|
104
|
+
result.nodes.append(GraphNode(
|
|
105
|
+
id=node_id,
|
|
106
|
+
kind=NodeKind.GUARD,
|
|
107
|
+
label="canActivate()",
|
|
108
|
+
fqn=f"{ctx.file_path}::canActivate",
|
|
109
|
+
module=ctx.module_name,
|
|
110
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
111
|
+
properties={
|
|
112
|
+
"auth_type": "nestjs_guard",
|
|
113
|
+
"guard_impl": "canActivate",
|
|
114
|
+
"roles": [],
|
|
115
|
+
},
|
|
116
|
+
))
|
|
117
|
+
|
|
118
|
+
# Detect AuthGuard('jwt') etc.
|
|
119
|
+
for match in self._AUTH_GUARD_PATTERN.finditer(text):
|
|
120
|
+
line = text[: match.start()].count("\n") + 1
|
|
121
|
+
strategy = match.group(1)
|
|
122
|
+
node_id = f"auth:{ctx.file_path}:AuthGuard({strategy}):{line}"
|
|
123
|
+
result.nodes.append(GraphNode(
|
|
124
|
+
id=node_id,
|
|
125
|
+
kind=NodeKind.GUARD,
|
|
126
|
+
label=f"AuthGuard('{strategy}')",
|
|
127
|
+
fqn=f"{ctx.file_path}::AuthGuard({strategy})",
|
|
128
|
+
module=ctx.module_name,
|
|
129
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
130
|
+
annotations=["AuthGuard"],
|
|
131
|
+
properties={
|
|
132
|
+
"auth_type": "nestjs_guard",
|
|
133
|
+
"strategy": strategy,
|
|
134
|
+
"roles": [],
|
|
135
|
+
},
|
|
136
|
+
))
|
|
137
|
+
|
|
138
|
+
return result
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Passport.js and JWT authentication 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 GraphNode, NodeKind, SourceLocation
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PassportJwtDetector:
|
|
13
|
+
"""Detects Passport.js strategy registrations, authenticate calls, and JWT verification."""
|
|
14
|
+
|
|
15
|
+
name: str = "typescript.passport_jwt"
|
|
16
|
+
supported_languages: tuple[str, ...] = ("typescript", "javascript")
|
|
17
|
+
|
|
18
|
+
# passport.use(new JwtStrategy(...)) or passport.use(new LocalStrategy(...))
|
|
19
|
+
_PASSPORT_USE_PATTERN = re.compile(
|
|
20
|
+
r"passport\.use\(\s*new\s+(\w+Strategy)\s*\("
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# passport.authenticate('jwt') or passport.authenticate("local", ...)
|
|
24
|
+
_PASSPORT_AUTH_PATTERN = re.compile(
|
|
25
|
+
r"passport\.authenticate\(\s*['\"](\w+)['\"]"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# jwt.verify(token, secret) or jwt.verify(token, secret, options)
|
|
29
|
+
_JWT_VERIFY_PATTERN = re.compile(
|
|
30
|
+
r"jwt\.verify\s*\("
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# require('express-jwt') or require("express-jwt")
|
|
34
|
+
_REQUIRE_EXPRESS_JWT_PATTERN = re.compile(
|
|
35
|
+
r"""require\(\s*['"]express-jwt['"]\s*\)"""
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# import { expressjwt } from 'express-jwt' (and variants)
|
|
39
|
+
_IMPORT_EXPRESS_JWT_PATTERN = re.compile(
|
|
40
|
+
r"""import\s+\{[^}]*\bexpressjwt\b[^}]*\}\s+from\s+['"]express-jwt['"]"""
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
44
|
+
result = DetectorResult()
|
|
45
|
+
text = decode_text(ctx)
|
|
46
|
+
|
|
47
|
+
# Detect passport.use(new XxxStrategy(...))
|
|
48
|
+
for match in self._PASSPORT_USE_PATTERN.finditer(text):
|
|
49
|
+
line = text[: match.start()].count("\n") + 1
|
|
50
|
+
strategy_name = match.group(1)
|
|
51
|
+
node_id = f"auth:{ctx.file_path}:passport.use({strategy_name}):{line}"
|
|
52
|
+
result.nodes.append(GraphNode(
|
|
53
|
+
id=node_id,
|
|
54
|
+
kind=NodeKind.GUARD,
|
|
55
|
+
label=f"passport.use({strategy_name})",
|
|
56
|
+
fqn=f"{ctx.file_path}::passport.use({strategy_name})",
|
|
57
|
+
module=ctx.module_name,
|
|
58
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
59
|
+
properties={
|
|
60
|
+
"auth_type": "passport",
|
|
61
|
+
"strategy": strategy_name,
|
|
62
|
+
},
|
|
63
|
+
))
|
|
64
|
+
|
|
65
|
+
# Detect passport.authenticate('xxx')
|
|
66
|
+
for match in self._PASSPORT_AUTH_PATTERN.finditer(text):
|
|
67
|
+
line = text[: match.start()].count("\n") + 1
|
|
68
|
+
strategy = match.group(1)
|
|
69
|
+
node_id = f"auth:{ctx.file_path}:passport.authenticate({strategy}):{line}"
|
|
70
|
+
result.nodes.append(GraphNode(
|
|
71
|
+
id=node_id,
|
|
72
|
+
kind=NodeKind.MIDDLEWARE,
|
|
73
|
+
label=f"passport.authenticate('{strategy}')",
|
|
74
|
+
fqn=f"{ctx.file_path}::passport.authenticate({strategy})",
|
|
75
|
+
module=ctx.module_name,
|
|
76
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
77
|
+
properties={
|
|
78
|
+
"auth_type": "jwt",
|
|
79
|
+
"strategy": strategy,
|
|
80
|
+
},
|
|
81
|
+
))
|
|
82
|
+
|
|
83
|
+
# Detect jwt.verify(...)
|
|
84
|
+
for match in self._JWT_VERIFY_PATTERN.finditer(text):
|
|
85
|
+
line = text[: match.start()].count("\n") + 1
|
|
86
|
+
node_id = f"auth:{ctx.file_path}:jwt.verify:{line}"
|
|
87
|
+
result.nodes.append(GraphNode(
|
|
88
|
+
id=node_id,
|
|
89
|
+
kind=NodeKind.MIDDLEWARE,
|
|
90
|
+
label="jwt.verify()",
|
|
91
|
+
fqn=f"{ctx.file_path}::jwt.verify",
|
|
92
|
+
module=ctx.module_name,
|
|
93
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
94
|
+
properties={
|
|
95
|
+
"auth_type": "jwt",
|
|
96
|
+
},
|
|
97
|
+
))
|
|
98
|
+
|
|
99
|
+
# Detect require('express-jwt')
|
|
100
|
+
for match in self._REQUIRE_EXPRESS_JWT_PATTERN.finditer(text):
|
|
101
|
+
line = text[: match.start()].count("\n") + 1
|
|
102
|
+
node_id = f"auth:{ctx.file_path}:require(express-jwt):{line}"
|
|
103
|
+
result.nodes.append(GraphNode(
|
|
104
|
+
id=node_id,
|
|
105
|
+
kind=NodeKind.MIDDLEWARE,
|
|
106
|
+
label="require('express-jwt')",
|
|
107
|
+
fqn=f"{ctx.file_path}::require(express-jwt)",
|
|
108
|
+
module=ctx.module_name,
|
|
109
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
110
|
+
properties={
|
|
111
|
+
"auth_type": "jwt",
|
|
112
|
+
"library": "express-jwt",
|
|
113
|
+
},
|
|
114
|
+
))
|
|
115
|
+
|
|
116
|
+
# Detect import { expressjwt } from 'express-jwt'
|
|
117
|
+
for match in self._IMPORT_EXPRESS_JWT_PATTERN.finditer(text):
|
|
118
|
+
line = text[: match.start()].count("\n") + 1
|
|
119
|
+
node_id = f"auth:{ctx.file_path}:import(expressjwt):{line}"
|
|
120
|
+
result.nodes.append(GraphNode(
|
|
121
|
+
id=node_id,
|
|
122
|
+
kind=NodeKind.MIDDLEWARE,
|
|
123
|
+
label="import { expressjwt }",
|
|
124
|
+
fqn=f"{ctx.file_path}::import(expressjwt)",
|
|
125
|
+
module=ctx.module_name,
|
|
126
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
127
|
+
properties={
|
|
128
|
+
"auth_type": "jwt",
|
|
129
|
+
"library": "express-jwt",
|
|
130
|
+
},
|
|
131
|
+
))
|
|
132
|
+
|
|
133
|
+
return result
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Prisma ORM detector for TypeScript/JavaScript."""
|
|
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 PrismaORMDetector:
|
|
13
|
+
"""Detects Prisma ORM usage patterns in TypeScript/JavaScript files."""
|
|
14
|
+
|
|
15
|
+
name: str = "prisma_orm"
|
|
16
|
+
supported_languages: tuple[str, ...] = ("typescript", "javascript")
|
|
17
|
+
|
|
18
|
+
_PRISMA_OP_RE = re.compile(
|
|
19
|
+
r"prisma\.(\w+)\.(findMany|findFirst|findUnique|create|update|delete|upsert|count|aggregate|groupBy)\s*\("
|
|
20
|
+
)
|
|
21
|
+
_PRISMA_CLIENT_RE = re.compile(r"new\s+PrismaClient\s*\(|PrismaClient\s*\(")
|
|
22
|
+
_PRISMA_IMPORT_RE = re.compile(r"""(?:import|require)\s*\(?[^)]*['"]@prisma/client['"]""")
|
|
23
|
+
_PRISMA_TRANSACTION_RE = re.compile(r"prisma\.\$transaction\s*\(")
|
|
24
|
+
|
|
25
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
26
|
+
result = DetectorResult()
|
|
27
|
+
text = decode_text(ctx)
|
|
28
|
+
|
|
29
|
+
# Detect PrismaClient instantiation -> DATABASE_CONNECTION
|
|
30
|
+
for match in self._PRISMA_CLIENT_RE.finditer(text):
|
|
31
|
+
line = text[: match.start()].count("\n") + 1
|
|
32
|
+
node_id = f"prisma:{ctx.file_path}:client:{line}"
|
|
33
|
+
props: dict[str, object] = {"framework": "prisma"}
|
|
34
|
+
# Check for $transaction usage
|
|
35
|
+
if self._PRISMA_TRANSACTION_RE.search(text):
|
|
36
|
+
props["transaction"] = True
|
|
37
|
+
result.nodes.append(
|
|
38
|
+
GraphNode(
|
|
39
|
+
id=node_id,
|
|
40
|
+
kind=NodeKind.DATABASE_CONNECTION,
|
|
41
|
+
label="PrismaClient",
|
|
42
|
+
fqn=f"{ctx.file_path}::PrismaClient",
|
|
43
|
+
module=ctx.module_name,
|
|
44
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
45
|
+
properties=props,
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Detect @prisma/client imports -> IMPORTS edge
|
|
50
|
+
for match in self._PRISMA_IMPORT_RE.finditer(text):
|
|
51
|
+
line = text[: match.start()].count("\n") + 1
|
|
52
|
+
result.edges.append(
|
|
53
|
+
GraphEdge(
|
|
54
|
+
source=ctx.file_path,
|
|
55
|
+
target="@prisma/client",
|
|
56
|
+
kind=EdgeKind.IMPORTS,
|
|
57
|
+
label="import @prisma/client",
|
|
58
|
+
properties={"line": line},
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Detect prisma model operations -> ENTITY nodes + QUERIES edges
|
|
63
|
+
seen_models: dict[str, str] = {}
|
|
64
|
+
for match in self._PRISMA_OP_RE.finditer(text):
|
|
65
|
+
model_name = match.group(1)
|
|
66
|
+
operation = match.group(2)
|
|
67
|
+
line = text[: match.start()].count("\n") + 1
|
|
68
|
+
|
|
69
|
+
# Create ENTITY node for each unique model
|
|
70
|
+
if model_name not in seen_models:
|
|
71
|
+
model_id = f"prisma:{ctx.file_path}:model:{model_name}"
|
|
72
|
+
seen_models[model_name] = model_id
|
|
73
|
+
result.nodes.append(
|
|
74
|
+
GraphNode(
|
|
75
|
+
id=model_id,
|
|
76
|
+
kind=NodeKind.ENTITY,
|
|
77
|
+
label=model_name,
|
|
78
|
+
fqn=f"{ctx.file_path}::{model_name}",
|
|
79
|
+
module=ctx.module_name,
|
|
80
|
+
location=SourceLocation(file_path=ctx.file_path, line_start=line),
|
|
81
|
+
properties={"framework": "prisma"},
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# QUERIES edge from file to model
|
|
86
|
+
result.edges.append(
|
|
87
|
+
GraphEdge(
|
|
88
|
+
source=ctx.file_path,
|
|
89
|
+
target=seen_models[model_name],
|
|
90
|
+
kind=EdgeKind.QUERIES,
|
|
91
|
+
label=f"{model_name}.{operation}",
|
|
92
|
+
properties={"operation": operation, "line": line},
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return result
|