openhack 0.1.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.
- openhack/__init__.py +2 -0
- openhack/__main__.py +225 -0
- openhack/agents/__init__.py +30 -0
- openhack/agents/base.py +230 -0
- openhack/agents/browser_verifier.py +679 -0
- openhack/agents/browser_verifier_swarm.py +256 -0
- openhack/agents/checkpoint.py +89 -0
- openhack/agents/context_manager.py +356 -0
- openhack/agents/coordinator.py +1105 -0
- openhack/agents/endpoint_analyst.py +307 -0
- openhack/agents/feature_hunter.py +93 -0
- openhack/agents/hunter.py +481 -0
- openhack/agents/hunter_swarm.py +385 -0
- openhack/agents/llm.py +334 -0
- openhack/agents/recon.py +19 -0
- openhack/agents/sandbox_verifier.py +396 -0
- openhack/agents/sandbox_verifier_swarm.py +250 -0
- openhack/agents/session.py +286 -0
- openhack/agents/validator.py +217 -0
- openhack/agents/validator_swarm.py +106 -0
- openhack/auth.py +175 -0
- openhack/browser/__init__.py +12 -0
- openhack/browser/runner.py +385 -0
- openhack/categories.py +130 -0
- openhack/config.py +201 -0
- openhack/deterministic_recon.py +464 -0
- openhack/entry_points.py +745 -0
- openhack/framework_classifier.py +515 -0
- openhack/framework_detection.py +269 -0
- openhack/headless_scan.py +179 -0
- openhack/prompts/__init__.py +108 -0
- openhack/prompts/browser_verifier.py +171 -0
- openhack/prompts/coordinator.py +31 -0
- openhack/prompts/django/__init__.py +32 -0
- openhack/prompts/django/auth_bypass.py +76 -0
- openhack/prompts/django/csrf.py +62 -0
- openhack/prompts/django/data_exposure.py +67 -0
- openhack/prompts/django/idor.py +74 -0
- openhack/prompts/django/injection.py +67 -0
- openhack/prompts/django/misconfiguration.py +70 -0
- openhack/prompts/django/ssrf.py +64 -0
- openhack/prompts/endpoint_analyst.py +122 -0
- openhack/prompts/express/__init__.py +29 -0
- openhack/prompts/express/auth_bypass.py +71 -0
- openhack/prompts/express/data_exposure.py +77 -0
- openhack/prompts/express/idor.py +69 -0
- openhack/prompts/express/injection.py +75 -0
- openhack/prompts/express/misconfiguration.py +72 -0
- openhack/prompts/express/ssrf.py +63 -0
- openhack/prompts/feature_hunter.py +140 -0
- openhack/prompts/flask/__init__.py +29 -0
- openhack/prompts/flask/auth_bypass.py +86 -0
- openhack/prompts/flask/data_exposure.py +78 -0
- openhack/prompts/flask/idor.py +83 -0
- openhack/prompts/flask/injection.py +77 -0
- openhack/prompts/flask/misconfiguration.py +73 -0
- openhack/prompts/flask/ssrf.py +65 -0
- openhack/prompts/hunter.py +362 -0
- openhack/prompts/hunter_continuation_loop.py +12 -0
- openhack/prompts/hunter_continuation_no_findings.py +19 -0
- openhack/prompts/hunter_continuation_no_progress.py +22 -0
- openhack/prompts/hunter_tool_instructions.py +55 -0
- openhack/prompts/nextjs/__init__.py +42 -0
- openhack/prompts/nextjs/auth_bypass.py +80 -0
- openhack/prompts/nextjs/csrf.py +71 -0
- openhack/prompts/nextjs/data_exposure.py +88 -0
- openhack/prompts/nextjs/idor.py +64 -0
- openhack/prompts/nextjs/injection.py +65 -0
- openhack/prompts/nextjs/middleware_bypass.py +75 -0
- openhack/prompts/nextjs/misconfiguration.py +92 -0
- openhack/prompts/nextjs/server_actions.py +97 -0
- openhack/prompts/nextjs/ssrf.py +66 -0
- openhack/prompts/nextjs/xss.py +69 -0
- openhack/prompts/pr_analysis_system.py +80 -0
- openhack/prompts/pr_analysis_user.py +11 -0
- openhack/prompts/project_context.py +89 -0
- openhack/prompts/recon.py +199 -0
- openhack/prompts/reporter.py +88 -0
- openhack/prompts/researchers.py +434 -0
- openhack/prompts/sandbox_verifier.py +128 -0
- openhack/prompts/supabase/__init__.py +39 -0
- openhack/prompts/supabase/auth_tokens.py +131 -0
- openhack/prompts/supabase/edge_functions.py +150 -0
- openhack/prompts/supabase/graphql.py +102 -0
- openhack/prompts/supabase/postgrest.py +99 -0
- openhack/prompts/supabase/realtime.py +93 -0
- openhack/prompts/supabase/rls.py +110 -0
- openhack/prompts/supabase/rpc_functions.py +127 -0
- openhack/prompts/supabase/storage.py +110 -0
- openhack/prompts/supabase/tenant_isolation.py +118 -0
- openhack/prompts/validator.py +319 -0
- openhack/prompts/validator_continuation_incomplete.py +12 -0
- openhack/prompts/validator_tool_instructions.py +29 -0
- openhack/quality.py +231 -0
- openhack/sandbox/__init__.py +12 -0
- openhack/sandbox/orchestrator.py +517 -0
- openhack/sandbox/runner.py +177 -0
- openhack/scan_session.py +245 -0
- openhack/setup.py +452 -0
- openhack/static_validator.py +612 -0
- openhack/tools/__init__.py +1 -0
- openhack/tools/ast_tools.py +307 -0
- openhack/tools/coverage.py +1078 -0
- openhack/tools/filesystem.py +404 -0
- openhack/tools/nextjs.py +258 -0
- openhack/tools/registry.py +52 -0
- openhack/tui.py +3450 -0
- openhack/updates.py +170 -0
- openhack-0.1.0.dist-info/METADATA +189 -0
- openhack-0.1.0.dist-info/RECORD +113 -0
- openhack-0.1.0.dist-info/WHEEL +4 -0
- openhack-0.1.0.dist-info/entry_points.txt +2 -0
- openhack-0.1.0.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Framework classifier — detects frameworks by reading dependency files.
|
|
3
|
+
|
|
4
|
+
Walks the repo, finds all dependency files (package.json, requirements.txt, etc.),
|
|
5
|
+
reads them, and maps each directory to its framework(s). Handles monorepos with
|
|
6
|
+
multiple frameworks in different directories.
|
|
7
|
+
|
|
8
|
+
This replaces the old framework_detection.py approach of guessing from folder names.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from .tools.filesystem import FileSystemTools
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Framework detection rules: dependency file → package name → framework
|
|
24
|
+
PACKAGE_JSON_FRAMEWORKS = {
|
|
25
|
+
# JavaScript/TypeScript web frameworks
|
|
26
|
+
"next": "nextjs",
|
|
27
|
+
"express": "express",
|
|
28
|
+
"@nestjs/core": "nestjs",
|
|
29
|
+
"fastify": "fastify",
|
|
30
|
+
"hono": "hono",
|
|
31
|
+
"koa": "koa",
|
|
32
|
+
"@hapi/hapi": "hapi",
|
|
33
|
+
"sails": "sails",
|
|
34
|
+
"nuxt": "nuxt",
|
|
35
|
+
"@sveltejs/kit": "sveltekit",
|
|
36
|
+
"@remix-run/node": "remix",
|
|
37
|
+
# Protocol/API frameworks
|
|
38
|
+
"@trpc/server": "trpc",
|
|
39
|
+
"graphql": "graphql",
|
|
40
|
+
"apollo-server": "graphql",
|
|
41
|
+
"@apollo/server": "graphql",
|
|
42
|
+
"mercurius": "graphql",
|
|
43
|
+
"graphql-yoga": "graphql",
|
|
44
|
+
"socket.io": "websocket",
|
|
45
|
+
"ws": "websocket",
|
|
46
|
+
"@grpc/grpc-js": "grpc",
|
|
47
|
+
# Auth libraries (supplementary detection)
|
|
48
|
+
"passport": "passport",
|
|
49
|
+
"next-auth": "nextauth",
|
|
50
|
+
"@auth/core": "authjs",
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
PYTHON_FRAMEWORKS = {
|
|
54
|
+
"django": "django",
|
|
55
|
+
"djangorestframework": "django_rest",
|
|
56
|
+
"django-rest-framework": "django_rest",
|
|
57
|
+
"flask": "flask",
|
|
58
|
+
"fastapi": "fastapi",
|
|
59
|
+
"starlette": "starlette",
|
|
60
|
+
"tornado": "tornado",
|
|
61
|
+
"sanic": "sanic",
|
|
62
|
+
"aiohttp": "aiohttp",
|
|
63
|
+
"graphene": "graphql",
|
|
64
|
+
"strawberry-graphql": "graphql",
|
|
65
|
+
"ariadne": "graphql",
|
|
66
|
+
"channels": "websocket",
|
|
67
|
+
"celery": "celery",
|
|
68
|
+
"grpcio": "grpc",
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
RUBY_FRAMEWORKS = {
|
|
72
|
+
"rails": "rails",
|
|
73
|
+
"sinatra": "sinatra",
|
|
74
|
+
"grape": "grape",
|
|
75
|
+
"graphql": "graphql",
|
|
76
|
+
"action_cable": "websocket",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
PHP_FRAMEWORKS = {
|
|
80
|
+
"laravel/framework": "laravel",
|
|
81
|
+
"symfony/framework-bundle": "symfony",
|
|
82
|
+
"symfony/http-kernel": "symfony",
|
|
83
|
+
"codeigniter4/framework": "codeigniter",
|
|
84
|
+
"cakephp/cakephp": "cakephp",
|
|
85
|
+
"slim/slim": "slim",
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
GO_FRAMEWORKS = {
|
|
89
|
+
"github.com/gin-gonic/gin": "gin",
|
|
90
|
+
"github.com/labstack/echo": "echo",
|
|
91
|
+
"github.com/gofiber/fiber": "fiber",
|
|
92
|
+
"github.com/go-chi/chi": "chi",
|
|
93
|
+
"google.golang.org/grpc": "grpc",
|
|
94
|
+
"github.com/gorilla/mux": "gorilla",
|
|
95
|
+
"github.com/gorilla/websocket": "websocket",
|
|
96
|
+
"net/http": "net_http",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
JAVA_FRAMEWORKS = {
|
|
100
|
+
"spring-boot-starter-web": "spring",
|
|
101
|
+
"spring-boot-starter-webflux": "spring_webflux",
|
|
102
|
+
"javax.ws.rs": "jaxrs",
|
|
103
|
+
"jakarta.ws.rs": "jaxrs",
|
|
104
|
+
"io.quarkus": "quarkus",
|
|
105
|
+
"io.micronaut": "micronaut",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
DOTNET_FRAMEWORKS = {
|
|
109
|
+
"Microsoft.AspNetCore": "aspnet",
|
|
110
|
+
"Microsoft.AspNetCore.Mvc": "aspnet_mvc",
|
|
111
|
+
"Microsoft.AspNetCore.SignalR": "signalr",
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
RUST_FRAMEWORKS = {
|
|
115
|
+
"actix-web": "actix",
|
|
116
|
+
"axum": "axum",
|
|
117
|
+
"rocket": "rocket",
|
|
118
|
+
"warp": "warp",
|
|
119
|
+
"tonic": "grpc",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def classify_frameworks(fs: FileSystemTools) -> list[dict]:
|
|
124
|
+
"""Classify frameworks in the repository by reading dependency files.
|
|
125
|
+
|
|
126
|
+
Returns a list of dicts, each with:
|
|
127
|
+
- root: directory path relative to repo root
|
|
128
|
+
- language: python, javascript, ruby, php, go, java, dotnet, rust, c
|
|
129
|
+
- frameworks: list of detected framework names
|
|
130
|
+
- dep_file: path to the dependency file used for detection
|
|
131
|
+
"""
|
|
132
|
+
classifications = []
|
|
133
|
+
|
|
134
|
+
# Find all dependency files
|
|
135
|
+
dep_file_patterns = [
|
|
136
|
+
("package.json", _parse_package_json),
|
|
137
|
+
("requirements.txt", _parse_requirements_txt),
|
|
138
|
+
("pyproject.toml", _parse_pyproject_toml),
|
|
139
|
+
("Pipfile", _parse_pipfile),
|
|
140
|
+
("Gemfile", _parse_gemfile),
|
|
141
|
+
("composer.json", _parse_composer_json),
|
|
142
|
+
("go.mod", _parse_go_mod),
|
|
143
|
+
("Cargo.toml", _parse_cargo_toml),
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
for filename, parser in dep_file_patterns:
|
|
147
|
+
matches = fs.glob(f"**/{filename}", ".")
|
|
148
|
+
for filepath in matches.get("matches", []):
|
|
149
|
+
# Skip node_modules, vendor, etc.
|
|
150
|
+
if any(skip in filepath for skip in [
|
|
151
|
+
"node_modules/", "vendor/", ".venv/", "__pycache__/",
|
|
152
|
+
"test/", "tests/", "fixtures/", "examples/", "demo/",
|
|
153
|
+
]):
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
result = fs.read_file(filepath)
|
|
157
|
+
if "error" in result:
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
content = result.get("content", "")
|
|
161
|
+
# Strip line number prefixes from read_file
|
|
162
|
+
lines = []
|
|
163
|
+
for line in content.split("\n"):
|
|
164
|
+
if "\t" in line:
|
|
165
|
+
lines.append(line.split("\t", 1)[1])
|
|
166
|
+
else:
|
|
167
|
+
lines.append(line)
|
|
168
|
+
clean_content = "\n".join(lines)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
classification = parser(clean_content, filepath)
|
|
172
|
+
if classification and classification.get("frameworks"):
|
|
173
|
+
classifications.append(classification)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
logger.debug(f"Failed to parse {filepath}: {e}")
|
|
176
|
+
|
|
177
|
+
# Also detect C/C++ projects (no dependency file, use Makefile/CMakeLists)
|
|
178
|
+
c_class = _detect_c_project(fs)
|
|
179
|
+
if c_class:
|
|
180
|
+
classifications.append(c_class)
|
|
181
|
+
|
|
182
|
+
# Also detect Java projects via pom.xml / build.gradle
|
|
183
|
+
java_class = _detect_java_project(fs)
|
|
184
|
+
if java_class:
|
|
185
|
+
classifications.append(java_class)
|
|
186
|
+
|
|
187
|
+
# Also detect .NET projects via .csproj
|
|
188
|
+
dotnet_class = _detect_dotnet_project(fs)
|
|
189
|
+
if dotnet_class:
|
|
190
|
+
classifications.append(dotnet_class)
|
|
191
|
+
|
|
192
|
+
# Also detect Rails projects via config/application.rb (handles Gemfile.d/ patterns)
|
|
193
|
+
rails_class = _detect_rails_project(fs)
|
|
194
|
+
if rails_class:
|
|
195
|
+
classifications.append(rails_class)
|
|
196
|
+
|
|
197
|
+
# Deduplicate — if same root has multiple dep files, merge
|
|
198
|
+
merged = _merge_classifications(classifications)
|
|
199
|
+
|
|
200
|
+
logger.info(f"Classified {len(merged)} framework(s): "
|
|
201
|
+
+ ", ".join(f"{c['root']}={c['frameworks']}" for c in merged))
|
|
202
|
+
|
|
203
|
+
return merged
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _get_root(filepath: str) -> str:
|
|
207
|
+
"""Get the directory of a dependency file relative to repo root."""
|
|
208
|
+
parts = filepath.split("/")
|
|
209
|
+
if len(parts) <= 1:
|
|
210
|
+
return "."
|
|
211
|
+
return "/".join(parts[:-1])
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _parse_package_json(content: str, filepath: str) -> Optional[dict]:
|
|
215
|
+
"""Parse package.json for framework dependencies."""
|
|
216
|
+
try:
|
|
217
|
+
pkg = json.loads(content)
|
|
218
|
+
except json.JSONDecodeError:
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
deps = {}
|
|
222
|
+
deps.update(pkg.get("dependencies", {}))
|
|
223
|
+
deps.update(pkg.get("devDependencies", {}))
|
|
224
|
+
|
|
225
|
+
frameworks = []
|
|
226
|
+
for package_name, framework_name in PACKAGE_JSON_FRAMEWORKS.items():
|
|
227
|
+
if package_name in deps:
|
|
228
|
+
frameworks.append(framework_name)
|
|
229
|
+
|
|
230
|
+
if not frameworks:
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
"root": _get_root(filepath),
|
|
235
|
+
"language": "javascript",
|
|
236
|
+
"frameworks": sorted(set(frameworks)),
|
|
237
|
+
"dep_file": filepath,
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _parse_requirements_txt(content: str, filepath: str) -> Optional[dict]:
|
|
242
|
+
"""Parse requirements.txt for Python framework dependencies."""
|
|
243
|
+
frameworks = []
|
|
244
|
+
for line in content.split("\n"):
|
|
245
|
+
line = line.strip().lower()
|
|
246
|
+
if not line or line.startswith("#") or line.startswith("-"):
|
|
247
|
+
continue
|
|
248
|
+
# Extract package name (before ==, >=, ~=, etc.)
|
|
249
|
+
pkg = re.split(r"[=<>~!;\[]", line)[0].strip()
|
|
250
|
+
if pkg in PYTHON_FRAMEWORKS:
|
|
251
|
+
frameworks.append(PYTHON_FRAMEWORKS[pkg])
|
|
252
|
+
|
|
253
|
+
if not frameworks:
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
"root": _get_root(filepath),
|
|
258
|
+
"language": "python",
|
|
259
|
+
"frameworks": sorted(set(frameworks)),
|
|
260
|
+
"dep_file": filepath,
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _parse_pyproject_toml(content: str, filepath: str) -> Optional[dict]:
|
|
265
|
+
"""Parse pyproject.toml for Python framework dependencies."""
|
|
266
|
+
frameworks = []
|
|
267
|
+
# Simple TOML parsing — look for dependency declarations
|
|
268
|
+
for line in content.split("\n"):
|
|
269
|
+
line_lower = line.strip().lower()
|
|
270
|
+
for pkg, framework in PYTHON_FRAMEWORKS.items():
|
|
271
|
+
if pkg in line_lower:
|
|
272
|
+
frameworks.append(framework)
|
|
273
|
+
|
|
274
|
+
if not frameworks:
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
"root": _get_root(filepath),
|
|
279
|
+
"language": "python",
|
|
280
|
+
"frameworks": sorted(set(frameworks)),
|
|
281
|
+
"dep_file": filepath,
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _parse_pipfile(content: str, filepath: str) -> Optional[dict]:
|
|
286
|
+
"""Parse Pipfile for Python framework dependencies."""
|
|
287
|
+
return _parse_pyproject_toml(content, filepath) # Similar enough format
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _parse_gemfile(content: str, filepath: str) -> Optional[dict]:
|
|
291
|
+
"""Parse Gemfile for Ruby framework dependencies."""
|
|
292
|
+
frameworks = []
|
|
293
|
+
for line in content.split("\n"):
|
|
294
|
+
line = line.strip()
|
|
295
|
+
if not line or line.startswith("#"):
|
|
296
|
+
continue
|
|
297
|
+
# gem 'rails', '~> 7.0'
|
|
298
|
+
match = re.match(r"gem\s+['\"]([^'\"]+)['\"]", line)
|
|
299
|
+
if match:
|
|
300
|
+
gem_name = match.group(1).lower()
|
|
301
|
+
if gem_name in RUBY_FRAMEWORKS:
|
|
302
|
+
frameworks.append(RUBY_FRAMEWORKS[gem_name])
|
|
303
|
+
|
|
304
|
+
if not frameworks:
|
|
305
|
+
return None
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
"root": _get_root(filepath),
|
|
309
|
+
"language": "ruby",
|
|
310
|
+
"frameworks": sorted(set(frameworks)),
|
|
311
|
+
"dep_file": filepath,
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _parse_composer_json(content: str, filepath: str) -> Optional[dict]:
|
|
316
|
+
"""Parse composer.json for PHP framework dependencies."""
|
|
317
|
+
try:
|
|
318
|
+
pkg = json.loads(content)
|
|
319
|
+
except json.JSONDecodeError:
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
deps = pkg.get("require", {})
|
|
323
|
+
frameworks = []
|
|
324
|
+
for package_name, framework_name in PHP_FRAMEWORKS.items():
|
|
325
|
+
if package_name in deps:
|
|
326
|
+
frameworks.append(framework_name)
|
|
327
|
+
|
|
328
|
+
if not frameworks:
|
|
329
|
+
# Check if it's raw PHP (has composer.json but no framework)
|
|
330
|
+
return {
|
|
331
|
+
"root": _get_root(filepath),
|
|
332
|
+
"language": "php",
|
|
333
|
+
"frameworks": ["php_raw"],
|
|
334
|
+
"dep_file": filepath,
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
"root": _get_root(filepath),
|
|
339
|
+
"language": "php",
|
|
340
|
+
"frameworks": sorted(set(frameworks)),
|
|
341
|
+
"dep_file": filepath,
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _parse_go_mod(content: str, filepath: str) -> Optional[dict]:
|
|
346
|
+
"""Parse go.mod for Go framework dependencies."""
|
|
347
|
+
frameworks = []
|
|
348
|
+
for line in content.split("\n"):
|
|
349
|
+
line = line.strip()
|
|
350
|
+
for module_path, framework_name in GO_FRAMEWORKS.items():
|
|
351
|
+
if module_path in line:
|
|
352
|
+
frameworks.append(framework_name)
|
|
353
|
+
|
|
354
|
+
if not frameworks:
|
|
355
|
+
# Go project with no detected framework — could be net/http
|
|
356
|
+
frameworks = ["net_http"]
|
|
357
|
+
|
|
358
|
+
return {
|
|
359
|
+
"root": _get_root(filepath),
|
|
360
|
+
"language": "go",
|
|
361
|
+
"frameworks": sorted(set(frameworks)),
|
|
362
|
+
"dep_file": filepath,
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _parse_cargo_toml(content: str, filepath: str) -> Optional[dict]:
|
|
367
|
+
"""Parse Cargo.toml for Rust framework dependencies."""
|
|
368
|
+
frameworks = []
|
|
369
|
+
for line in content.split("\n"):
|
|
370
|
+
line = line.strip().lower()
|
|
371
|
+
for crate_name, framework_name in RUST_FRAMEWORKS.items():
|
|
372
|
+
if crate_name in line:
|
|
373
|
+
frameworks.append(framework_name)
|
|
374
|
+
|
|
375
|
+
if not frameworks:
|
|
376
|
+
return None
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
"root": _get_root(filepath),
|
|
380
|
+
"language": "rust",
|
|
381
|
+
"frameworks": sorted(set(frameworks)),
|
|
382
|
+
"dep_file": filepath,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _detect_c_project(fs: FileSystemTools) -> Optional[dict]:
|
|
387
|
+
"""Detect C/C++ projects by looking for Makefile/CMakeLists."""
|
|
388
|
+
for indicator in ["Makefile", "CMakeLists.txt", "configure", "meson.build"]:
|
|
389
|
+
result = fs.glob(indicator, ".")
|
|
390
|
+
if result.get("matches"):
|
|
391
|
+
# Verify there are actually C files
|
|
392
|
+
c_files = fs.glob("**/*.c", ".")
|
|
393
|
+
h_files = fs.glob("**/*.h", ".")
|
|
394
|
+
cpp_files = fs.glob("**/*.cpp", ".")
|
|
395
|
+
total = (len(c_files.get("matches", [])) +
|
|
396
|
+
len(h_files.get("matches", [])) +
|
|
397
|
+
len(cpp_files.get("matches", [])))
|
|
398
|
+
if total > 5:
|
|
399
|
+
lang = "cpp" if len(cpp_files.get("matches", [])) > len(c_files.get("matches", [])) else "c"
|
|
400
|
+
return {
|
|
401
|
+
"root": ".",
|
|
402
|
+
"language": lang,
|
|
403
|
+
"frameworks": [lang],
|
|
404
|
+
"dep_file": indicator,
|
|
405
|
+
}
|
|
406
|
+
return None
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def _detect_java_project(fs: FileSystemTools) -> Optional[dict]:
|
|
410
|
+
"""Detect Java projects via pom.xml or build.gradle."""
|
|
411
|
+
for indicator in ["pom.xml", "build.gradle", "build.gradle.kts"]:
|
|
412
|
+
result = fs.glob(indicator, ".")
|
|
413
|
+
if result.get("matches"):
|
|
414
|
+
filepath = result["matches"][0]
|
|
415
|
+
read_result = fs.read_file(filepath)
|
|
416
|
+
if "error" in read_result:
|
|
417
|
+
continue
|
|
418
|
+
content = read_result.get("content", "")
|
|
419
|
+
frameworks = []
|
|
420
|
+
for pattern, framework in JAVA_FRAMEWORKS.items():
|
|
421
|
+
if pattern.lower() in content.lower():
|
|
422
|
+
frameworks.append(framework)
|
|
423
|
+
if not frameworks:
|
|
424
|
+
frameworks = ["java_raw"]
|
|
425
|
+
return {
|
|
426
|
+
"root": _get_root(filepath),
|
|
427
|
+
"language": "java",
|
|
428
|
+
"frameworks": frameworks,
|
|
429
|
+
"dep_file": filepath,
|
|
430
|
+
}
|
|
431
|
+
return None
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _detect_dotnet_project(fs: FileSystemTools) -> Optional[dict]:
|
|
435
|
+
"""Detect .NET projects via .csproj files."""
|
|
436
|
+
result = fs.glob("**/*.csproj", ".")
|
|
437
|
+
matches = result.get("matches", [])
|
|
438
|
+
if not matches:
|
|
439
|
+
return None
|
|
440
|
+
|
|
441
|
+
filepath = matches[0]
|
|
442
|
+
read_result = fs.read_file(filepath)
|
|
443
|
+
if "error" in read_result:
|
|
444
|
+
return None
|
|
445
|
+
|
|
446
|
+
content = read_result.get("content", "")
|
|
447
|
+
frameworks = []
|
|
448
|
+
for pattern, framework in DOTNET_FRAMEWORKS.items():
|
|
449
|
+
if pattern.lower() in content.lower():
|
|
450
|
+
frameworks.append(framework)
|
|
451
|
+
if not frameworks:
|
|
452
|
+
frameworks = ["dotnet_raw"]
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
"root": _get_root(filepath),
|
|
456
|
+
"language": "dotnet",
|
|
457
|
+
"frameworks": frameworks,
|
|
458
|
+
"dep_file": filepath,
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _detect_rails_project(fs: FileSystemTools) -> Optional[dict]:
|
|
463
|
+
"""Detect Rails projects via config/application.rb or Gemfile.d/."""
|
|
464
|
+
for indicator in ["config/application.rb"]:
|
|
465
|
+
result = fs.glob(indicator, ".")
|
|
466
|
+
if result.get("matches"):
|
|
467
|
+
filepath = result["matches"][0]
|
|
468
|
+
read_result = fs.read_file(filepath)
|
|
469
|
+
content = read_result.get("content", "") if "error" not in read_result else ""
|
|
470
|
+
if "Rails" in content or "rails" in content.lower():
|
|
471
|
+
root = _get_root(filepath).rsplit("/config", 1)[0] if "/config" in filepath else "."
|
|
472
|
+
frameworks = ["rails"]
|
|
473
|
+
if "graphql" in content.lower():
|
|
474
|
+
frameworks.append("graphql")
|
|
475
|
+
return {
|
|
476
|
+
"root": root,
|
|
477
|
+
"language": "ruby",
|
|
478
|
+
"frameworks": frameworks,
|
|
479
|
+
"dep_file": filepath,
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
for gemfile_d in ["Gemfile.d/app.rb", "Gemfile.d"]:
|
|
483
|
+
result = fs.glob(gemfile_d, ".")
|
|
484
|
+
if result.get("matches"):
|
|
485
|
+
filepath = result["matches"][0]
|
|
486
|
+
if filepath.endswith(".rb"):
|
|
487
|
+
read_result = fs.read_file(filepath)
|
|
488
|
+
if "error" not in read_result:
|
|
489
|
+
content = read_result.get("content", "")
|
|
490
|
+
for line in content.split("\n"):
|
|
491
|
+
if "gem" in line and "rails" in line.lower() and "rubocop" not in line.lower():
|
|
492
|
+
root = _get_root(filepath).rsplit("/Gemfile.d", 1)[0] if "/Gemfile.d" in filepath else "."
|
|
493
|
+
return {
|
|
494
|
+
"root": root,
|
|
495
|
+
"language": "ruby",
|
|
496
|
+
"frameworks": ["rails"],
|
|
497
|
+
"dep_file": filepath,
|
|
498
|
+
}
|
|
499
|
+
return None
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _merge_classifications(classifications: list[dict]) -> list[dict]:
|
|
503
|
+
"""Merge classifications with the same root directory."""
|
|
504
|
+
by_root: dict[str, dict] = {}
|
|
505
|
+
for c in classifications:
|
|
506
|
+
root = c["root"]
|
|
507
|
+
if root in by_root:
|
|
508
|
+
existing = by_root[root]
|
|
509
|
+
existing["frameworks"] = sorted(set(existing["frameworks"] + c["frameworks"]))
|
|
510
|
+
# Keep the more specific language
|
|
511
|
+
if existing["language"] == "javascript" and c["language"] != "javascript":
|
|
512
|
+
existing["language"] = c["language"]
|
|
513
|
+
else:
|
|
514
|
+
by_root[root] = dict(c)
|
|
515
|
+
return list(by_root.values())
|