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,194 @@
|
|
|
1
|
+
"""Detector for OpenAPI 3.x and Swagger 2.0 specification files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
6
|
+
from osscodeiq.models.graph import (
|
|
7
|
+
EdgeKind,
|
|
8
|
+
GraphEdge,
|
|
9
|
+
GraphNode,
|
|
10
|
+
NodeKind,
|
|
11
|
+
SourceLocation,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OpenApiDetector:
|
|
16
|
+
"""Detects API endpoints and schemas from OpenAPI/Swagger specifications."""
|
|
17
|
+
|
|
18
|
+
name: str = "openapi"
|
|
19
|
+
supported_languages: tuple[str, ...] = ("json", "yaml")
|
|
20
|
+
|
|
21
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
22
|
+
result = DetectorResult()
|
|
23
|
+
|
|
24
|
+
data = ctx.parsed_data
|
|
25
|
+
if not isinstance(data, dict) or not isinstance(data.get("data"), dict):
|
|
26
|
+
return result
|
|
27
|
+
|
|
28
|
+
spec = data["data"]
|
|
29
|
+
|
|
30
|
+
# Only trigger if this is an OpenAPI or Swagger spec
|
|
31
|
+
if "openapi" not in spec and "swagger" not in spec:
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
filepath = ctx.file_path
|
|
35
|
+
config_id = f"api:{filepath}"
|
|
36
|
+
|
|
37
|
+
# Extract info metadata
|
|
38
|
+
info = spec.get("info") if isinstance(spec.get("info"), dict) else {}
|
|
39
|
+
api_title = info.get("title", filepath) if isinstance(info.get("title"), str) else filepath
|
|
40
|
+
api_version = info.get("version", "") if isinstance(info.get("version"), str) else ""
|
|
41
|
+
spec_version = spec.get("openapi") or spec.get("swagger") or ""
|
|
42
|
+
|
|
43
|
+
# CONFIG_FILE node for the spec
|
|
44
|
+
result.nodes.append(GraphNode(
|
|
45
|
+
id=config_id,
|
|
46
|
+
kind=NodeKind.CONFIG_FILE,
|
|
47
|
+
label=api_title,
|
|
48
|
+
fqn=filepath,
|
|
49
|
+
module=ctx.module_name,
|
|
50
|
+
location=SourceLocation(file_path=filepath),
|
|
51
|
+
properties={
|
|
52
|
+
"config_type": "openapi",
|
|
53
|
+
"api_title": api_title,
|
|
54
|
+
"api_version": api_version,
|
|
55
|
+
"spec_version": str(spec_version),
|
|
56
|
+
},
|
|
57
|
+
))
|
|
58
|
+
|
|
59
|
+
# ENDPOINT nodes for each path + method combination
|
|
60
|
+
paths = spec.get("paths")
|
|
61
|
+
if isinstance(paths, dict):
|
|
62
|
+
for path, path_item in paths.items():
|
|
63
|
+
if not isinstance(path, str) or not isinstance(path_item, dict):
|
|
64
|
+
continue
|
|
65
|
+
for method, operation in path_item.items():
|
|
66
|
+
if not isinstance(method, str):
|
|
67
|
+
continue
|
|
68
|
+
# Skip non-HTTP-method keys (e.g. "parameters", "summary")
|
|
69
|
+
if method.lower() not in (
|
|
70
|
+
"get", "post", "put", "patch", "delete",
|
|
71
|
+
"head", "options", "trace",
|
|
72
|
+
):
|
|
73
|
+
continue
|
|
74
|
+
method_upper = method.upper()
|
|
75
|
+
endpoint_id = f"api:{filepath}:{method.lower()}:{path}"
|
|
76
|
+
props: dict[str, object] = {
|
|
77
|
+
"http_method": method_upper,
|
|
78
|
+
"path": path,
|
|
79
|
+
}
|
|
80
|
+
if isinstance(operation, dict):
|
|
81
|
+
op_id = operation.get("operationId")
|
|
82
|
+
if isinstance(op_id, str):
|
|
83
|
+
props["operation_id"] = op_id
|
|
84
|
+
summary = operation.get("summary")
|
|
85
|
+
if isinstance(summary, str):
|
|
86
|
+
props["summary"] = summary
|
|
87
|
+
|
|
88
|
+
result.nodes.append(GraphNode(
|
|
89
|
+
id=endpoint_id,
|
|
90
|
+
kind=NodeKind.ENDPOINT,
|
|
91
|
+
label=f"{method_upper} {path}",
|
|
92
|
+
module=ctx.module_name,
|
|
93
|
+
location=SourceLocation(file_path=filepath),
|
|
94
|
+
properties=props,
|
|
95
|
+
))
|
|
96
|
+
result.edges.append(GraphEdge(
|
|
97
|
+
source=config_id,
|
|
98
|
+
target=endpoint_id,
|
|
99
|
+
kind=EdgeKind.CONTAINS,
|
|
100
|
+
label=f"{api_title} contains {method_upper} {path}",
|
|
101
|
+
))
|
|
102
|
+
|
|
103
|
+
# ENTITY nodes for schemas — OpenAPI 3.x and Swagger 2.0
|
|
104
|
+
schemas = _extract_schemas(spec)
|
|
105
|
+
for schema_name, schema_def in schemas.items():
|
|
106
|
+
if not isinstance(schema_name, str):
|
|
107
|
+
continue
|
|
108
|
+
schema_id = f"api:{filepath}:schema:{schema_name}"
|
|
109
|
+
schema_props: dict[str, object] = {"schema_name": schema_name}
|
|
110
|
+
if isinstance(schema_def, dict):
|
|
111
|
+
schema_type = schema_def.get("type")
|
|
112
|
+
if isinstance(schema_type, str):
|
|
113
|
+
schema_props["schema_type"] = schema_type
|
|
114
|
+
|
|
115
|
+
result.nodes.append(GraphNode(
|
|
116
|
+
id=schema_id,
|
|
117
|
+
kind=NodeKind.ENTITY,
|
|
118
|
+
label=schema_name,
|
|
119
|
+
module=ctx.module_name,
|
|
120
|
+
location=SourceLocation(file_path=filepath),
|
|
121
|
+
properties=schema_props,
|
|
122
|
+
))
|
|
123
|
+
result.edges.append(GraphEdge(
|
|
124
|
+
source=config_id,
|
|
125
|
+
target=schema_id,
|
|
126
|
+
kind=EdgeKind.CONTAINS,
|
|
127
|
+
label=f"{api_title} defines schema {schema_name}",
|
|
128
|
+
))
|
|
129
|
+
|
|
130
|
+
# DEPENDS_ON edges for $ref references within this schema
|
|
131
|
+
if isinstance(schema_def, dict):
|
|
132
|
+
refs = _collect_refs(schema_def)
|
|
133
|
+
for ref in refs:
|
|
134
|
+
ref_name = _ref_to_schema_name(ref)
|
|
135
|
+
if ref_name and ref_name != schema_name and ref_name in schemas:
|
|
136
|
+
result.edges.append(GraphEdge(
|
|
137
|
+
source=schema_id,
|
|
138
|
+
target=f"api:{filepath}:schema:{ref_name}",
|
|
139
|
+
kind=EdgeKind.DEPENDS_ON,
|
|
140
|
+
label=f"{schema_name} references {ref_name}",
|
|
141
|
+
))
|
|
142
|
+
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _extract_schemas(spec: dict) -> dict:
|
|
147
|
+
"""Extract schema definitions from both OpenAPI 3.x and Swagger 2.0."""
|
|
148
|
+
# OpenAPI 3.x: components.schemas
|
|
149
|
+
components = spec.get("components")
|
|
150
|
+
if isinstance(components, dict):
|
|
151
|
+
schemas = components.get("schemas")
|
|
152
|
+
if isinstance(schemas, dict):
|
|
153
|
+
return schemas
|
|
154
|
+
|
|
155
|
+
# Swagger 2.0: definitions
|
|
156
|
+
definitions = spec.get("definitions")
|
|
157
|
+
if isinstance(definitions, dict):
|
|
158
|
+
return definitions
|
|
159
|
+
|
|
160
|
+
return {}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _collect_refs(obj: dict | list, _seen: set | None = None) -> list[str]:
|
|
164
|
+
"""Recursively collect all $ref values from a schema definition."""
|
|
165
|
+
if _seen is None:
|
|
166
|
+
_seen = set()
|
|
167
|
+
refs: list[str] = []
|
|
168
|
+
obj_id = id(obj)
|
|
169
|
+
if obj_id in _seen:
|
|
170
|
+
return refs
|
|
171
|
+
_seen.add(obj_id)
|
|
172
|
+
|
|
173
|
+
if isinstance(obj, dict):
|
|
174
|
+
ref = obj.get("$ref")
|
|
175
|
+
if isinstance(ref, str):
|
|
176
|
+
refs.append(ref)
|
|
177
|
+
for value in obj.values():
|
|
178
|
+
if isinstance(value, (dict, list)):
|
|
179
|
+
refs.extend(_collect_refs(value, _seen))
|
|
180
|
+
elif isinstance(obj, list):
|
|
181
|
+
for item in obj:
|
|
182
|
+
if isinstance(item, (dict, list)):
|
|
183
|
+
refs.extend(_collect_refs(item, _seen))
|
|
184
|
+
return refs
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _ref_to_schema_name(ref: str) -> str | None:
|
|
188
|
+
"""Extract a schema name from a $ref string like '#/components/schemas/User'."""
|
|
189
|
+
if not ref.startswith("#/"):
|
|
190
|
+
return None
|
|
191
|
+
parts = ref.split("/")
|
|
192
|
+
if len(parts) >= 2:
|
|
193
|
+
return parts[-1]
|
|
194
|
+
return None
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Detector for package.json files (npm/Node.js dependencies and scripts)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
8
|
+
from osscodeiq.models.graph import (
|
|
9
|
+
EdgeKind,
|
|
10
|
+
GraphEdge,
|
|
11
|
+
GraphNode,
|
|
12
|
+
NodeKind,
|
|
13
|
+
SourceLocation,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PackageJsonDetector:
|
|
18
|
+
"""Detects module dependencies and scripts from package.json files."""
|
|
19
|
+
|
|
20
|
+
name: str = "package_json"
|
|
21
|
+
supported_languages: tuple[str, ...] = ("json",)
|
|
22
|
+
|
|
23
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
24
|
+
result = DetectorResult()
|
|
25
|
+
|
|
26
|
+
# Only trigger for files named exactly "package.json"
|
|
27
|
+
if os.path.basename(ctx.file_path) != "package.json":
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
data = ctx.parsed_data
|
|
31
|
+
if not isinstance(data, dict) or not isinstance(data.get("data"), dict):
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
pkg = data["data"]
|
|
35
|
+
filepath = ctx.file_path
|
|
36
|
+
module_id = f"npm:{filepath}"
|
|
37
|
+
pkg_name = pkg.get("name") or filepath
|
|
38
|
+
|
|
39
|
+
# MODULE node for the package
|
|
40
|
+
props: dict[str, object] = {"package_name": pkg_name}
|
|
41
|
+
version = pkg.get("version")
|
|
42
|
+
if version:
|
|
43
|
+
props["version"] = version
|
|
44
|
+
|
|
45
|
+
result.nodes.append(GraphNode(
|
|
46
|
+
id=module_id,
|
|
47
|
+
kind=NodeKind.MODULE,
|
|
48
|
+
label=pkg_name,
|
|
49
|
+
fqn=pkg_name,
|
|
50
|
+
module=ctx.module_name,
|
|
51
|
+
location=SourceLocation(file_path=filepath),
|
|
52
|
+
properties=props,
|
|
53
|
+
))
|
|
54
|
+
|
|
55
|
+
# DEPENDS_ON edges for dependencies and devDependencies
|
|
56
|
+
for dep_key in ("dependencies", "devDependencies"):
|
|
57
|
+
deps = pkg.get(dep_key)
|
|
58
|
+
if not isinstance(deps, dict):
|
|
59
|
+
continue
|
|
60
|
+
for dep_name, dep_version in deps.items():
|
|
61
|
+
if not isinstance(dep_name, str):
|
|
62
|
+
continue
|
|
63
|
+
edge_props: dict[str, object] = {"dep_type": dep_key}
|
|
64
|
+
if isinstance(dep_version, str):
|
|
65
|
+
edge_props["version_spec"] = dep_version
|
|
66
|
+
result.edges.append(GraphEdge(
|
|
67
|
+
source=module_id,
|
|
68
|
+
target=f"npm:{dep_name}",
|
|
69
|
+
kind=EdgeKind.DEPENDS_ON,
|
|
70
|
+
label=f"{pkg_name} depends on {dep_name}",
|
|
71
|
+
properties=edge_props,
|
|
72
|
+
))
|
|
73
|
+
|
|
74
|
+
# METHOD nodes for each script
|
|
75
|
+
scripts = pkg.get("scripts")
|
|
76
|
+
if isinstance(scripts, dict):
|
|
77
|
+
for script_name, script_cmd in scripts.items():
|
|
78
|
+
if not isinstance(script_name, str):
|
|
79
|
+
continue
|
|
80
|
+
script_id = f"npm:{filepath}:script:{script_name}"
|
|
81
|
+
script_props: dict[str, object] = {"script_name": script_name}
|
|
82
|
+
if isinstance(script_cmd, str):
|
|
83
|
+
script_props["command"] = script_cmd
|
|
84
|
+
result.nodes.append(GraphNode(
|
|
85
|
+
id=script_id,
|
|
86
|
+
kind=NodeKind.METHOD,
|
|
87
|
+
label=f"npm run {script_name}",
|
|
88
|
+
module=ctx.module_name,
|
|
89
|
+
location=SourceLocation(file_path=filepath),
|
|
90
|
+
properties=script_props,
|
|
91
|
+
))
|
|
92
|
+
result.edges.append(GraphEdge(
|
|
93
|
+
source=module_id,
|
|
94
|
+
target=script_id,
|
|
95
|
+
kind=EdgeKind.CONTAINS,
|
|
96
|
+
label=f"{pkg_name} contains script {script_name}",
|
|
97
|
+
))
|
|
98
|
+
|
|
99
|
+
return result
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Properties file detector for Java .properties and Spring configuration files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
6
|
+
from osscodeiq.models.graph import (
|
|
7
|
+
EdgeKind,
|
|
8
|
+
GraphEdge,
|
|
9
|
+
GraphNode,
|
|
10
|
+
NodeKind,
|
|
11
|
+
SourceLocation,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
_DB_KEYWORDS = {"url", "jdbc", "datasource"}
|
|
15
|
+
_MAX_KEYS = 200
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PropertiesDetector:
|
|
19
|
+
"""Detects property keys, Spring config markers, and database connections from .properties files."""
|
|
20
|
+
|
|
21
|
+
name: str = "properties"
|
|
22
|
+
supported_languages: tuple[str, ...] = ("properties",)
|
|
23
|
+
|
|
24
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
25
|
+
result = DetectorResult()
|
|
26
|
+
|
|
27
|
+
# parsed_data expected: {"type": "properties", "file": path, "data": {"key": "value", ...}}
|
|
28
|
+
if not isinstance(ctx.parsed_data, dict):
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
if ctx.parsed_data.get("type") != "properties":
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
data = ctx.parsed_data.get("data")
|
|
35
|
+
if not isinstance(data, dict):
|
|
36
|
+
return result
|
|
37
|
+
|
|
38
|
+
filepath = ctx.file_path
|
|
39
|
+
file_id = f"props:{filepath}"
|
|
40
|
+
|
|
41
|
+
# CONFIG_FILE node
|
|
42
|
+
result.nodes.append(GraphNode(
|
|
43
|
+
id=file_id,
|
|
44
|
+
kind=NodeKind.CONFIG_FILE,
|
|
45
|
+
label=filepath,
|
|
46
|
+
fqn=filepath,
|
|
47
|
+
module=ctx.module_name,
|
|
48
|
+
location=SourceLocation(
|
|
49
|
+
file_path=filepath,
|
|
50
|
+
line_start=1,
|
|
51
|
+
),
|
|
52
|
+
properties={"format": "properties"},
|
|
53
|
+
))
|
|
54
|
+
|
|
55
|
+
# Process keys (limit to avoid node explosion)
|
|
56
|
+
keys = list(data.items())[:_MAX_KEYS]
|
|
57
|
+
|
|
58
|
+
for key, value in keys:
|
|
59
|
+
if not isinstance(key, str):
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
key_lower = key.lower()
|
|
63
|
+
key_id = f"props:{filepath}:{key}"
|
|
64
|
+
|
|
65
|
+
# Detect DB connection properties
|
|
66
|
+
is_db = any(kw in key_lower for kw in _DB_KEYWORDS)
|
|
67
|
+
|
|
68
|
+
if is_db:
|
|
69
|
+
props: dict[str, object] = {"key": key}
|
|
70
|
+
if isinstance(value, str):
|
|
71
|
+
props["value"] = value
|
|
72
|
+
|
|
73
|
+
result.nodes.append(GraphNode(
|
|
74
|
+
id=key_id,
|
|
75
|
+
kind=NodeKind.DATABASE_CONNECTION,
|
|
76
|
+
label=key,
|
|
77
|
+
fqn=f"{filepath}:{key}",
|
|
78
|
+
module=ctx.module_name,
|
|
79
|
+
location=SourceLocation(file_path=filepath),
|
|
80
|
+
properties=props,
|
|
81
|
+
))
|
|
82
|
+
else:
|
|
83
|
+
props = {"key": key}
|
|
84
|
+
if isinstance(value, str):
|
|
85
|
+
props["value"] = value
|
|
86
|
+
|
|
87
|
+
# Detect Spring config
|
|
88
|
+
if key.startswith("spring."):
|
|
89
|
+
props["spring_config"] = True
|
|
90
|
+
|
|
91
|
+
result.nodes.append(GraphNode(
|
|
92
|
+
id=key_id,
|
|
93
|
+
kind=NodeKind.CONFIG_KEY,
|
|
94
|
+
label=key,
|
|
95
|
+
fqn=f"{filepath}:{key}",
|
|
96
|
+
module=ctx.module_name,
|
|
97
|
+
location=SourceLocation(file_path=filepath),
|
|
98
|
+
properties=props,
|
|
99
|
+
))
|
|
100
|
+
|
|
101
|
+
result.edges.append(GraphEdge(
|
|
102
|
+
source=file_id,
|
|
103
|
+
target=key_id,
|
|
104
|
+
kind=EdgeKind.CONTAINS,
|
|
105
|
+
label=f"{filepath} contains {key}",
|
|
106
|
+
))
|
|
107
|
+
|
|
108
|
+
return result
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Detector for pyproject.toml files (Python project metadata and dependencies)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from osscodeiq.detectors.base import DetectorContext, DetectorResult
|
|
9
|
+
from osscodeiq.detectors.utils import decode_text
|
|
10
|
+
from osscodeiq.models.graph import (
|
|
11
|
+
EdgeKind,
|
|
12
|
+
GraphEdge,
|
|
13
|
+
GraphNode,
|
|
14
|
+
NodeKind,
|
|
15
|
+
SourceLocation,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
if sys.version_info >= (3, 11):
|
|
19
|
+
import tomllib
|
|
20
|
+
else:
|
|
21
|
+
try:
|
|
22
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
23
|
+
except ImportError:
|
|
24
|
+
tomllib = None # type: ignore[assignment]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PyprojectTomlDetector:
|
|
28
|
+
"""Detects Python project metadata, dependencies, and entry points from pyproject.toml."""
|
|
29
|
+
|
|
30
|
+
name: str = "pyproject_toml"
|
|
31
|
+
supported_languages: tuple[str, ...] = ("toml",)
|
|
32
|
+
|
|
33
|
+
def detect(self, ctx: DetectorContext) -> DetectorResult:
|
|
34
|
+
result = DetectorResult()
|
|
35
|
+
|
|
36
|
+
# Only trigger for files named exactly "pyproject.toml"
|
|
37
|
+
if os.path.basename(ctx.file_path) != "pyproject.toml":
|
|
38
|
+
return result
|
|
39
|
+
|
|
40
|
+
if tomllib is None:
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
data = tomllib.loads(decode_text(ctx))
|
|
45
|
+
except Exception:
|
|
46
|
+
return result
|
|
47
|
+
|
|
48
|
+
if not isinstance(data, dict):
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
filepath = ctx.file_path
|
|
52
|
+
module_id = f"pypi:{filepath}"
|
|
53
|
+
|
|
54
|
+
# Resolve project name from [project] or [tool.poetry]
|
|
55
|
+
project_section = data.get("project", {})
|
|
56
|
+
poetry_section = data.get("tool", {}).get("poetry", {})
|
|
57
|
+
|
|
58
|
+
pkg_name = (
|
|
59
|
+
project_section.get("name")
|
|
60
|
+
or poetry_section.get("name")
|
|
61
|
+
or filepath
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Module properties
|
|
65
|
+
props: dict[str, object] = {"package_name": pkg_name}
|
|
66
|
+
version = project_section.get("version") or poetry_section.get("version")
|
|
67
|
+
if version:
|
|
68
|
+
props["version"] = version
|
|
69
|
+
description = project_section.get("description") or poetry_section.get("description")
|
|
70
|
+
if description:
|
|
71
|
+
props["description"] = description
|
|
72
|
+
|
|
73
|
+
# MODULE node for the project
|
|
74
|
+
result.nodes.append(GraphNode(
|
|
75
|
+
id=module_id,
|
|
76
|
+
kind=NodeKind.MODULE,
|
|
77
|
+
label=pkg_name,
|
|
78
|
+
fqn=pkg_name,
|
|
79
|
+
module=ctx.module_name,
|
|
80
|
+
location=SourceLocation(file_path=filepath),
|
|
81
|
+
properties=props,
|
|
82
|
+
))
|
|
83
|
+
|
|
84
|
+
# DEPENDS_ON edges for dependencies
|
|
85
|
+
# PEP 621 style: [project].dependencies is a list of strings
|
|
86
|
+
pep621_deps = project_section.get("dependencies", [])
|
|
87
|
+
if isinstance(pep621_deps, list):
|
|
88
|
+
for dep_spec in pep621_deps:
|
|
89
|
+
if not isinstance(dep_spec, str):
|
|
90
|
+
continue
|
|
91
|
+
# Extract package name from spec like "requests>=2.0"
|
|
92
|
+
dep_name = _parse_dep_name(dep_spec)
|
|
93
|
+
if dep_name:
|
|
94
|
+
result.edges.append(GraphEdge(
|
|
95
|
+
source=module_id,
|
|
96
|
+
target=f"pypi:{dep_name}",
|
|
97
|
+
kind=EdgeKind.DEPENDS_ON,
|
|
98
|
+
label=f"{pkg_name} depends on {dep_name}",
|
|
99
|
+
properties={"dep_spec": dep_spec},
|
|
100
|
+
))
|
|
101
|
+
|
|
102
|
+
# Poetry style: [tool.poetry].dependencies is a dict
|
|
103
|
+
poetry_deps = poetry_section.get("dependencies", {})
|
|
104
|
+
if isinstance(poetry_deps, dict):
|
|
105
|
+
for dep_name, dep_version in poetry_deps.items():
|
|
106
|
+
if not isinstance(dep_name, str):
|
|
107
|
+
continue
|
|
108
|
+
# Skip python itself
|
|
109
|
+
if dep_name.lower() == "python":
|
|
110
|
+
continue
|
|
111
|
+
edge_props: dict[str, object] = {}
|
|
112
|
+
if isinstance(dep_version, str):
|
|
113
|
+
edge_props["version_spec"] = dep_version
|
|
114
|
+
result.edges.append(GraphEdge(
|
|
115
|
+
source=module_id,
|
|
116
|
+
target=f"pypi:{dep_name}",
|
|
117
|
+
kind=EdgeKind.DEPENDS_ON,
|
|
118
|
+
label=f"{pkg_name} depends on {dep_name}",
|
|
119
|
+
properties=edge_props,
|
|
120
|
+
))
|
|
121
|
+
|
|
122
|
+
# CONFIG_DEFINITION nodes for entry points / scripts
|
|
123
|
+
scripts = project_section.get("scripts", {})
|
|
124
|
+
if not isinstance(scripts, dict):
|
|
125
|
+
scripts = {}
|
|
126
|
+
poetry_scripts = poetry_section.get("scripts", {})
|
|
127
|
+
if isinstance(poetry_scripts, dict):
|
|
128
|
+
scripts.update(poetry_scripts)
|
|
129
|
+
|
|
130
|
+
for script_name, script_target in scripts.items():
|
|
131
|
+
if not isinstance(script_name, str):
|
|
132
|
+
continue
|
|
133
|
+
script_id = f"pypi:{filepath}:script:{script_name}"
|
|
134
|
+
script_props: dict[str, object] = {"script_name": script_name}
|
|
135
|
+
if isinstance(script_target, str):
|
|
136
|
+
script_props["target"] = script_target
|
|
137
|
+
|
|
138
|
+
result.nodes.append(GraphNode(
|
|
139
|
+
id=script_id,
|
|
140
|
+
kind=NodeKind.CONFIG_DEFINITION,
|
|
141
|
+
label=script_name,
|
|
142
|
+
fqn=f"{pkg_name}:script:{script_name}",
|
|
143
|
+
module=ctx.module_name,
|
|
144
|
+
location=SourceLocation(file_path=filepath),
|
|
145
|
+
properties=script_props,
|
|
146
|
+
))
|
|
147
|
+
|
|
148
|
+
result.edges.append(GraphEdge(
|
|
149
|
+
source=module_id,
|
|
150
|
+
target=script_id,
|
|
151
|
+
kind=EdgeKind.CONTAINS,
|
|
152
|
+
label=f"{pkg_name} defines script {script_name}",
|
|
153
|
+
))
|
|
154
|
+
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _parse_dep_name(spec: str) -> str | None:
|
|
159
|
+
"""Extract package name from a PEP 508 dependency specifier."""
|
|
160
|
+
# e.g. "requests>=2.0", "numpy", "black[jupyter]>=22.0"
|
|
161
|
+
spec = spec.strip()
|
|
162
|
+
if not spec:
|
|
163
|
+
return None
|
|
164
|
+
# Split on version specifiers or extras
|
|
165
|
+
for ch in ">=<![;@ ":
|
|
166
|
+
idx = spec.find(ch)
|
|
167
|
+
if idx > 0:
|
|
168
|
+
spec = spec[:idx]
|
|
169
|
+
return spec.strip() or None
|