codedna-analyzer 1.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. codedna/__init__.py +3 -0
  2. codedna/analyzers/__init__.py +1 -0
  3. codedna/analyzers/ai_analyzer.py +111 -0
  4. codedna/analyzers/architecture_detector.py +150 -0
  5. codedna/analyzers/cache_manager.py +70 -0
  6. codedna/analyzers/code_smell_detector.py +302 -0
  7. codedna/analyzers/complexity_analyzer.py +329 -0
  8. codedna/analyzers/dependency_mapper.py +171 -0
  9. codedna/analyzers/developer_analyzer.py +148 -0
  10. codedna/analyzers/diff_engine.py +395 -0
  11. codedna/analyzers/dna_generator.py +308 -0
  12. codedna/analyzers/evolution_engine.py +193 -0
  13. codedna/analyzers/github_analyzer.py +86 -0
  14. codedna/analyzers/language_detector.py +151 -0
  15. codedna/analyzers/repo_cloner.py +91 -0
  16. codedna/analyzers/security_detector.py +271 -0
  17. codedna/analyzers/structure_analyzer.py +91 -0
  18. codedna/ci/__init__.py +5 -0
  19. codedna/ci/gatekeeper.py +421 -0
  20. codedna/cli.py +839 -0
  21. codedna/compliance/__init__.py +5 -0
  22. codedna/compliance/sbom.py +410 -0
  23. codedna/config.py +167 -0
  24. codedna/mcp/__init__.py +5 -0
  25. codedna/mcp/protocol.py +65 -0
  26. codedna/mcp/server.py +393 -0
  27. codedna/remediation/__init__.py +5 -0
  28. codedna/remediation/planner.py +244 -0
  29. codedna/visualization/__init__.py +1 -0
  30. codedna/visualization/html_export.py +507 -0
  31. codedna/visualization/renderer.py +373 -0
  32. codedna_analyzer-1.4.0.dist-info/METADATA +425 -0
  33. codedna_analyzer-1.4.0.dist-info/RECORD +37 -0
  34. codedna_analyzer-1.4.0.dist-info/WHEEL +5 -0
  35. codedna_analyzer-1.4.0.dist-info/entry_points.txt +2 -0
  36. codedna_analyzer-1.4.0.dist-info/licenses/LICENSE +21 -0
  37. codedna_analyzer-1.4.0.dist-info/top_level.txt +1 -0
codedna/mcp/server.py ADDED
@@ -0,0 +1,393 @@
1
+ """CodeDNA MCP Server — JSON-RPC 2.0 stdio server for AI client inter-connectivity."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from codedna import __version__
12
+ from codedna.analyzers.architecture_detector import ArchitectureDetector
13
+ from codedna.analyzers.complexity_analyzer import ComplexityAnalyzer
14
+ from codedna.analyzers.dependency_mapper import DependencyMapper
15
+ from codedna.analyzers.diff_engine import DiffEngine
16
+ from codedna.analyzers.language_detector import LanguageDetector
17
+ from codedna.analyzers.security_detector import SecurityDetector
18
+ from codedna.analyzers.structure_analyzer import StructureAnalyzer
19
+ from codedna.ci.gatekeeper import Gatekeeper
20
+ from codedna.config import load_config
21
+
22
+ from .protocol import (
23
+ METHOD_NOT_FOUND,
24
+ JSONRPCRequest,
25
+ JSONRPCResponse,
26
+ MCPToolDefinition,
27
+ )
28
+
29
+ logger = logging.getLogger("codedna.mcp")
30
+
31
+
32
+ class CodeDNAMCPServer:
33
+ """Stdio and programmatic Model Context Protocol server for CodeDNA."""
34
+
35
+ def __init__(self) -> None:
36
+ self.server_name = "codedna-mcp"
37
+ self.server_version = __version__
38
+ self._register_tools()
39
+
40
+ def _register_tools(self) -> None:
41
+ self.tools: dict[str, MCPToolDefinition] = {
42
+ "codedna_analyze": MCPToolDefinition(
43
+ name="codedna_analyze",
44
+ description="Run complete reverse-engineering DNA analysis on a repository path or directory.",
45
+ inputSchema={
46
+ "type": "object",
47
+ "properties": {
48
+ "path": {"type": "string", "description": "Local path to codebase root", "default": "."},
49
+ },
50
+ },
51
+ ),
52
+ "codedna_inspect_architecture": MCPToolDefinition(
53
+ name="codedna_inspect_architecture",
54
+ description="Detect architecture patterns (MVC, Layered, Clean/Hexagonal, Microservices, Event-Driven) and infrastructure traits.",
55
+ inputSchema={
56
+ "type": "object",
57
+ "properties": {
58
+ "path": {"type": "string", "description": "Local directory path", "default": "."},
59
+ },
60
+ },
61
+ ),
62
+ "codedna_security_audit": MCPToolDefinition(
63
+ name="codedna_security_audit",
64
+ description="Scan codebase for hardcoded credentials, AI API keys, Shannon high-entropy secrets, and injection vulnerabilities.",
65
+ inputSchema={
66
+ "type": "object",
67
+ "properties": {
68
+ "path": {"type": "string", "description": "Local directory path", "default": "."},
69
+ },
70
+ },
71
+ ),
72
+ "codedna_complexity_metrics": MCPToolDefinition(
73
+ name="codedna_complexity_metrics",
74
+ description="Analyze McCabe Cyclomatic Complexity, Cognitive Complexity, and Maintainability Index (MI) across source functions.",
75
+ inputSchema={
76
+ "type": "object",
77
+ "properties": {
78
+ "path": {"type": "string", "description": "Local directory path", "default": "."},
79
+ },
80
+ },
81
+ ),
82
+ "codedna_dependency_graph": MCPToolDefinition(
83
+ name="codedna_dependency_graph",
84
+ description="Build internal dependency graph, detect circular cycles, coupling density, and PageRank centrality.",
85
+ inputSchema={
86
+ "type": "object",
87
+ "properties": {
88
+ "path": {"type": "string", "description": "Local directory path", "default": "."},
89
+ },
90
+ },
91
+ ),
92
+ "codedna_quality_gate": MCPToolDefinition(
93
+ name="codedna_quality_gate",
94
+ description="Evaluate repository against CI/CD architectural quality gates and security policies.",
95
+ inputSchema={
96
+ "type": "object",
97
+ "properties": {
98
+ "path": {"type": "string", "description": "Local directory path", "default": "."},
99
+ "min_health": {"type": "string", "enum": ["Healthy", "Fair", "Needs Attention", "Critical"], "default": "Fair"},
100
+ "fail_on_secrets": {"type": "boolean", "default": True},
101
+ "fail_on_circular": {"type": "boolean", "default": False},
102
+ },
103
+ },
104
+ ),
105
+ "codedna_drift_diff": MCPToolDefinition(
106
+ name="codedna_drift_diff",
107
+ description="Compare two codebases or branches to calculate genetic drift, similarity percentage, and architectural deltas.",
108
+ inputSchema={
109
+ "type": "object",
110
+ "properties": {
111
+ "baseline": {"type": "string", "description": "Baseline directory or DNA JSON profile"},
112
+ "target": {"type": "string", "description": "Target directory or DNA JSON profile"},
113
+ },
114
+ "required": ["baseline", "target"],
115
+ },
116
+ ),
117
+ "codedna_generate_sbom": MCPToolDefinition(
118
+ name="codedna_generate_sbom",
119
+ description="Generate CycloneDX 1.5 or SPDX 2.3 Software Bill of Materials (SBOM) across Python, Node.js, Rust, and Go dependencies.",
120
+ inputSchema={
121
+ "type": "object",
122
+ "properties": {
123
+ "path": {"type": "string", "description": "Local codebase path", "default": "."},
124
+ "format": {"type": "string", "enum": ["cyclonedx", "spdx"], "default": "cyclonedx"},
125
+ },
126
+ },
127
+ ),
128
+ "codedna_plan_remediation": MCPToolDefinition(
129
+ name="codedna_plan_remediation",
130
+ description="Plan and optionally apply automated, deterministic code remediation diffs for security flaws, bare exceptions, and wildcard pins.",
131
+ inputSchema={
132
+ "type": "object",
133
+ "properties": {
134
+ "path": {"type": "string", "description": "Local codebase path", "default": "."},
135
+ "apply_fixes": {"type": "boolean", "description": "Apply autofixes directly to disk", "default": False},
136
+ "rules": {"type": "array", "items": {"type": "string"}, "description": "Filter by rule IDs"},
137
+ },
138
+ },
139
+ ),
140
+ }
141
+
142
+ def handle_message(self, message_str: str) -> str | None:
143
+ """Handle raw JSON-RPC string message and return JSON string response."""
144
+ try:
145
+ data = json.loads(message_str)
146
+ except Exception as e:
147
+ err_resp = JSONRPCResponse(id=None, error={"code": -32700, "message": f"Parse error: {e}"})
148
+ return json.dumps(err_resp.to_dict())
149
+
150
+ if not isinstance(data, (dict, list)):
151
+ err_resp = JSONRPCResponse(id=None, error={"code": -32600, "message": "Invalid Request: expected JSON object or array"})
152
+ return json.dumps(err_resp.to_dict())
153
+
154
+ if isinstance(data, list):
155
+ responses = []
156
+ for item in data:
157
+ if not isinstance(item, dict):
158
+ responses.append(JSONRPCResponse(id=None, error={"code": -32600, "message": "Invalid Request: expected object"}).to_dict())
159
+ continue
160
+ req = JSONRPCRequest.from_dict(item)
161
+ res = self.handle_request(req)
162
+ if res:
163
+ responses.append(res.to_dict())
164
+ return json.dumps(responses) if responses else None
165
+
166
+ req = JSONRPCRequest.from_dict(data)
167
+ res = self.handle_request(req)
168
+ return json.dumps(res.to_dict()) if res else None
169
+
170
+ def handle_request(self, req: JSONRPCRequest) -> JSONRPCResponse | None:
171
+ """Dispatch JSON-RPC request to appropriate MCP handler."""
172
+ method = req.method
173
+
174
+ if method == "initialize":
175
+ return JSONRPCResponse(
176
+ id=req.id,
177
+ result={
178
+ "protocolVersion": "2024-11-05",
179
+ "serverInfo": {
180
+ "name": self.server_name,
181
+ "version": self.server_version,
182
+ },
183
+ "capabilities": {
184
+ "tools": {"listChanged": False},
185
+ },
186
+ },
187
+ )
188
+
189
+ elif method == "notifications/initialized":
190
+ # Handshake notification, no response required
191
+ return None
192
+
193
+ elif method == "ping":
194
+ return JSONRPCResponse(id=req.id, result={})
195
+
196
+ elif method == "tools/list":
197
+ return JSONRPCResponse(
198
+ id=req.id,
199
+ result={
200
+ "tools": [t.to_dict() for t in self.tools.values()],
201
+ },
202
+ )
203
+
204
+ elif method == "tools/call":
205
+ params = req.params
206
+ tool_name = params.get("name", "")
207
+ args = params.get("arguments", {})
208
+
209
+ if tool_name not in self.tools:
210
+ return JSONRPCResponse(
211
+ id=req.id,
212
+ error={"code": METHOD_NOT_FOUND, "message": f"Tool '{tool_name}' not found"},
213
+ )
214
+
215
+ try:
216
+ content_text = self._execute_tool(tool_name, args)
217
+ return JSONRPCResponse(
218
+ id=req.id,
219
+ result={
220
+ "content": [{"type": "text", "text": content_text}],
221
+ "isError": False,
222
+ },
223
+ )
224
+ except Exception as e:
225
+ return JSONRPCResponse(
226
+ id=req.id,
227
+ result={
228
+ "content": [{"type": "text", "text": f"Error executing '{tool_name}': {e}"}],
229
+ "isError": True,
230
+ },
231
+ )
232
+
233
+ else:
234
+ return JSONRPCResponse(
235
+ id=req.id,
236
+ error={"code": METHOD_NOT_FOUND, "message": f"Method '{method}' not supported"},
237
+ )
238
+
239
+ def _execute_tool(self, name: str, args: dict[str, Any]) -> str:
240
+ """Execute selected tool and return serialized response text."""
241
+ target_path = Path(args.get("path", ".")).resolve()
242
+
243
+ if name == "codedna_analyze":
244
+ config = load_config(target_path)
245
+ langs = LanguageDetector().detect(target_path)
246
+ struct = StructureAnalyzer().analyze(target_path)
247
+ arch = ArchitectureDetector().detect(target_path)
248
+ sec = SecurityDetector(config=config).detect(target_path)
249
+ cmp = ComplexityAnalyzer().analyze(target_path, config=config)
250
+ deps = DependencyMapper().map(target_path)
251
+
252
+ summary = {
253
+ "source": str(target_path),
254
+ "primary_language": langs.get("primary", "Unknown"),
255
+ "system_type": arch.get("primary_pattern", "Unknown"),
256
+ "total_files": struct.get("total_files", 0),
257
+ "total_lines": langs.get("total_lines", 0),
258
+ "maintainability_index": cmp.get("maintainability_index", 100.0),
259
+ "maintainability_rank": cmp.get("maintainability_rank", "A"),
260
+ "critical_security_vulnerabilities": sec.get("total_critical", 0),
261
+ "has_secrets": sec.get("has_secrets", False),
262
+ "circular_dependencies": deps.get("has_circular_deps", False),
263
+ "dependency_density": deps.get("density", 0.0),
264
+ }
265
+ return json.dumps(summary, indent=2)
266
+
267
+ elif name == "codedna_inspect_architecture":
268
+ arch = ArchitectureDetector().detect(target_path)
269
+ return json.dumps(arch, indent=2)
270
+
271
+ elif name == "codedna_security_audit":
272
+ config = load_config(target_path)
273
+ sec = SecurityDetector(config=config).detect(target_path)
274
+ return json.dumps(sec, indent=2)
275
+
276
+ elif name == "codedna_complexity_metrics":
277
+ config = load_config(target_path)
278
+ cmp = ComplexityAnalyzer().analyze(target_path, config=config)
279
+ return json.dumps(cmp, indent=2)
280
+
281
+ elif name == "codedna_dependency_graph":
282
+ deps = DependencyMapper().map(target_path)
283
+ return json.dumps(deps, indent=2)
284
+
285
+ elif name == "codedna_quality_gate":
286
+ config = load_config(target_path)
287
+ langs = LanguageDetector().detect(target_path)
288
+ struct = StructureAnalyzer().analyze(target_path)
289
+ arch = ArchitectureDetector().detect(target_path)
290
+ deps = DependencyMapper().map(target_path)
291
+ sec = SecurityDetector(config=config).detect(target_path)
292
+ cmp = ComplexityAnalyzer().analyze(target_path, config=config)
293
+
294
+ mock_profile = {
295
+ "health": {
296
+ "overall": "Healthy" if sec.get("total_critical", 0) == 0 else "Critical",
297
+ "severity_counts": {"critical": sec.get("total_critical", 0), "warning": 0, "info": 0},
298
+ },
299
+ "dependencies": deps,
300
+ "security": sec,
301
+ "complexity": cmp,
302
+ "structure_stats": struct,
303
+ "architecture": arch,
304
+ "languages": langs,
305
+ }
306
+
307
+ gatekeeper = Gatekeeper(
308
+ min_health=args.get("min_health", config.min_health),
309
+ fail_on_secrets=args.get("fail_on_secrets", config.fail_on_secrets),
310
+ fail_on_circular=args.get("fail_on_circular", config.fail_on_circular),
311
+ max_cyclomatic_complexity=config.max_cyclomatic_complexity,
312
+ min_maintainability_index=config.min_maintainability_index,
313
+ max_density=config.max_density,
314
+ max_critical_smells=config.max_critical_smells,
315
+ max_smells=config.max_smells,
316
+ )
317
+ result = gatekeeper.evaluate(mock_profile)
318
+ return json.dumps(result.to_dict(), indent=2)
319
+
320
+ elif name == "codedna_drift_diff":
321
+ baseline_path = Path(args["baseline"]).resolve()
322
+ target_path_b = Path(args["target"]).resolve()
323
+
324
+ # Helper to load or analyze
325
+ def _get_profile(p: Path) -> dict:
326
+ if p.is_file() and p.suffix == ".json":
327
+ return json.loads(p.read_text(encoding="utf-8"))
328
+ cfg = load_config(p)
329
+ return {
330
+ "metadata": {"source": str(p)},
331
+ "languages": LanguageDetector().detect(p),
332
+ "structure_stats": StructureAnalyzer().analyze(p),
333
+ "architecture": ArchitectureDetector().detect(p),
334
+ "dependencies": DependencyMapper().map(p),
335
+ "security": SecurityDetector(config=cfg).detect(p),
336
+ "health": {"overall": "Healthy", "severity_counts": {"critical": 0, "warning": 0, "info": 0}},
337
+ }
338
+
339
+ prof_a = _get_profile(baseline_path)
340
+ prof_b = _get_profile(target_path_b)
341
+ diff = DiffEngine().compare(prof_a, prof_b)
342
+ return json.dumps(diff, indent=2)
343
+
344
+ elif name == "codedna_generate_sbom":
345
+ from codedna.compliance.sbom import SBOMGenerator
346
+
347
+ fmt = args.get("format", "cyclonedx").lower()
348
+ generator = SBOMGenerator(target_path)
349
+ if fmt == "spdx":
350
+ doc = generator.generate_spdx_json()
351
+ else:
352
+ doc = generator.generate_cyclonedx_json()
353
+ return json.dumps(doc, indent=2)
354
+
355
+ elif name == "codedna_plan_remediation":
356
+ from codedna.remediation.planner import RemediationPlanner
357
+
358
+ planner = RemediationPlanner(target_path)
359
+ rules = args.get("rules")
360
+ actions = planner.plan(rule_filter=rules)
361
+ apply_fixes = args.get("apply_fixes", False)
362
+ if apply_fixes:
363
+ apply_res = planner.apply(actions, dry_run=False)
364
+ return json.dumps({
365
+ "plan": [a.to_dict() for a in actions],
366
+ "application": apply_res,
367
+ }, indent=2)
368
+ return json.dumps({
369
+ "total_remediations": len(actions),
370
+ "autofixable_count": sum(1 for a in actions if a.autofixable),
371
+ "actions": [a.to_dict() for a in actions],
372
+ }, indent=2)
373
+
374
+ return "{}"
375
+
376
+ def run_stdio(self) -> None:
377
+ """Run standard I/O loop consuming line-delimited JSON-RPC from stdin."""
378
+ sys.stderr.write(f"🧬 CodeDNA MCP Server v{self.server_version} running on stdio...\n")
379
+ sys.stderr.flush()
380
+
381
+ for line in sys.stdin:
382
+ line = line.strip()
383
+ if not line:
384
+ continue
385
+ try:
386
+ response_str = self.handle_message(line)
387
+ if response_str:
388
+ sys.stdout.write(response_str + "\n")
389
+ sys.stdout.flush()
390
+ except Exception as e:
391
+ err_resp = JSONRPCResponse(id=None, error={"code": -32603, "message": f"Internal error: {e}"})
392
+ sys.stdout.write(json.dumps(err_resp.to_dict()) + "\n")
393
+ sys.stdout.flush()
@@ -0,0 +1,5 @@
1
+ """Intelligent Remediation and Auto-Fix Subsystem."""
2
+
3
+ from .planner import RemediationAction, RemediationPlanner
4
+
5
+ __all__ = ["RemediationAction", "RemediationPlanner"]
@@ -0,0 +1,244 @@
1
+ """Automated Intelligent Remediation Planner for CodeDNA.
2
+
3
+ Translates security vulnerabilities, code smells, and quality gate failures
4
+ into structured, deterministic, reviewable remediation diffs and patches.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import difflib
10
+ import logging
11
+ import os
12
+ import re
13
+ from dataclasses import dataclass, field
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ IGNORE_DIRS = {
20
+ ".git", ".svn", ".hg", "node_modules", "venv", ".venv", "env",
21
+ "__pycache__", ".pytest_cache", "build", "dist", "target"
22
+ }
23
+
24
+
25
+ @dataclass
26
+ class RemediationAction:
27
+ rule_id: str
28
+ title: str
29
+ file_path: str
30
+ line_number: int
31
+ severity: str
32
+ description: str
33
+ original_text: str
34
+ proposed_text: str
35
+ unified_diff: str
36
+ autofixable: bool = True
37
+ metadata: Dict[str, Any] = field(default_factory=dict)
38
+
39
+ def to_dict(self) -> Dict[str, Any]:
40
+ return {
41
+ "rule_id": self.rule_id,
42
+ "title": self.title,
43
+ "file": self.file_path,
44
+ "line": self.line_number,
45
+ "severity": self.severity,
46
+ "description": self.description,
47
+ "original_text": self.original_text,
48
+ "proposed_text": self.proposed_text,
49
+ "diff": self.unified_diff,
50
+ "autofixable": self.autofixable,
51
+ "metadata": self.metadata,
52
+ }
53
+
54
+
55
+ class RemediationPlanner:
56
+ """Analyzes codebase findings and plans safe, deterministic code patches."""
57
+
58
+ def __init__(self, repo_path: str | Path = ".") -> None:
59
+ self.repo_path = Path(repo_path).resolve()
60
+
61
+ def plan(self, rule_filter: Optional[List[str]] = None) -> List[RemediationAction]:
62
+ """Scans the repository and produces prioritized remediation proposals."""
63
+ actions: List[RemediationAction] = []
64
+
65
+ for root, dirs, files in os.walk(self.repo_path):
66
+ dirs[:] = [d for d in dirs if d not in IGNORE_DIRS and not d.startswith(".")]
67
+
68
+ for fname in files:
69
+ fpath = Path(root) / fname
70
+ rel_path = str(fpath.relative_to(self.repo_path))
71
+
72
+ # Python remediations
73
+ if fname.endswith(".py"):
74
+ actions.extend(self._scan_python_file(fpath, rel_path))
75
+
76
+ # Node.js package.json wildcard remediations
77
+ elif fname == "package.json":
78
+ actions.extend(self._scan_package_json(fpath, rel_path))
79
+
80
+ if rule_filter:
81
+ allowed = set(r.upper() for r in rule_filter)
82
+ actions = [a for a in actions if a.rule_id.upper() in allowed]
83
+
84
+ actions.sort(key=lambda a: (0 if a.severity == "critical" else 1 if a.severity == "warning" else 2, a.file_path, a.line_number))
85
+ return actions
86
+
87
+ def _scan_python_file(self, path: Path, rel_path: str) -> List[RemediationAction]:
88
+ actions: List[RemediationAction] = []
89
+ try:
90
+ content = path.read_text(encoding="utf-8", errors="ignore")
91
+ lines = content.splitlines(keepends=True)
92
+
93
+ # 1. Bare except: -> except Exception:
94
+ for idx, line in enumerate(lines):
95
+ bare_match = re.search(r"^([ \t]*)except[ \t]*:", line)
96
+ if bare_match:
97
+ indent = bare_match.group(1)
98
+ orig_line = line
99
+ fixed_line = f"{indent}except Exception:\n" if line.endswith("\n") else f"{indent}except Exception:"
100
+ diff = self._make_diff(orig_line, fixed_line, rel_path)
101
+ actions.append(RemediationAction(
102
+ rule_id="SMELL-BARE-EXCEPT",
103
+ title="Narrow Bare Exception Handler",
104
+ file_path=rel_path,
105
+ line_number=idx + 1,
106
+ severity="warning",
107
+ description="Bare 'except:' catches system interrupts (KeyboardInterrupt, SystemExit). Narrow to 'except Exception:'.",
108
+ original_text=orig_line.strip(),
109
+ proposed_text=fixed_line.strip(),
110
+ unified_diff=diff,
111
+ autofixable=True
112
+ ))
113
+
114
+ # 2. Dangerous eval / exec calls with user input or direct strings
115
+ for idx, line in enumerate(lines):
116
+ if re.search(r"\b(eval|exec)\s*\(", line) and not line.strip().startswith("#"):
117
+ actions.append(RemediationAction(
118
+ rule_id="SEC-DANGEROUS-DYNAMIC-EXEC",
119
+ title="Refactor Dynamic Code Execution",
120
+ file_path=rel_path,
121
+ line_number=idx + 1,
122
+ severity="critical",
123
+ description=f"Direct invocation of {line.strip()[:30]}... allows arbitrary code execution. Refactor to ast.literal_eval or structured parsers.",
124
+ original_text=line.strip(),
125
+ proposed_text="# TODO: Replace dynamic execution with safe ast.literal_eval or parser",
126
+ unified_diff=self._make_diff(line, f"# Refactor dynamic exec: {line}", rel_path),
127
+ autofixable=False
128
+ ))
129
+
130
+ # 3. High-entropy hardcoded secret assignment
131
+ secret_pattern = re.compile(
132
+ r"^([ \t]*)(?:api_key|secret_key|auth_token|password|access_token)\s*=\s*['\"][A-Za-z0-9_\-+/]{24,}['\"]",
133
+ re.IGNORECASE
134
+ )
135
+ for idx, line in enumerate(lines):
136
+ m = secret_pattern.search(line)
137
+ if m and not line.strip().startswith("#"):
138
+ indent = m.group(1)
139
+ var_name = line.split("=")[0].strip()
140
+ env_var = var_name.upper()
141
+ orig_line = line
142
+ fixed_line = f"{indent}{var_name} = os.getenv('{env_var}', '')\n" if line.endswith("\n") else f"{indent}{var_name} = os.getenv('{env_var}', '')"
143
+ diff = self._make_diff(orig_line, fixed_line, rel_path)
144
+ actions.append(RemediationAction(
145
+ rule_id="SEC-HARDCODED-SECRET",
146
+ title="Extract Hardcoded Secret to Environment Variable",
147
+ file_path=rel_path,
148
+ line_number=idx + 1,
149
+ severity="critical",
150
+ description=f"Hardcoded sensitive token assigned to {var_name}. Extract to os.getenv('{env_var}').",
151
+ original_text=orig_line.strip(),
152
+ proposed_text=fixed_line.strip(),
153
+ unified_diff=diff,
154
+ autofixable=True
155
+ ))
156
+
157
+ except Exception as e:
158
+ logger.debug("Failed scanning %s for remediation: %s", path, e)
159
+ return actions
160
+
161
+ def _scan_package_json(self, path: Path, rel_path: str) -> List[RemediationAction]:
162
+ actions: List[RemediationAction] = []
163
+ try:
164
+ content = path.read_text(encoding="utf-8", errors="ignore")
165
+ lines = content.splitlines(keepends=True)
166
+ for idx, line in enumerate(lines):
167
+ # Pattern: "some-pkg": "*"
168
+ m = re.search(r'([ \t]*"[^"]+"\s*:\s*)"\*"(,?)', line)
169
+ if m:
170
+ prefix = m.group(1)
171
+ comma = m.group(2)
172
+ orig_line = line
173
+ fixed_line = f'{prefix}"^1.0.0"{comma}\n' if line.endswith("\n") else f'{prefix}"^1.0.0"{comma}'
174
+ diff = self._make_diff(orig_line, fixed_line, rel_path)
175
+ actions.append(RemediationAction(
176
+ rule_id="DEP-WILDCARD-PIN",
177
+ title="Pin Wildcard Dependency Version",
178
+ file_path=rel_path,
179
+ line_number=idx + 1,
180
+ severity="warning",
181
+ description="Unrestricted wildcard version '*' exposes builds to supply-chain tampering. Pin to semantic range.",
182
+ original_text=orig_line.strip(),
183
+ proposed_text=fixed_line.strip(),
184
+ unified_diff=diff,
185
+ autofixable=True
186
+ ))
187
+ except Exception as e:
188
+ logger.debug("Failed scanning %s for remediation: %s", path, e)
189
+ return actions
190
+
191
+ def _make_diff(self, original: str, proposed: str, file_path: str) -> str:
192
+ orig_lines = [original if original.endswith("\n") else original + "\n"]
193
+ prop_lines = [proposed if proposed.endswith("\n") else proposed + "\n"]
194
+ diff = difflib.unified_diff(
195
+ orig_lines,
196
+ prop_lines,
197
+ fromfile=f"a/{file_path}",
198
+ tofile=f"b/{file_path}",
199
+ n=1
200
+ )
201
+ return "".join(diff)
202
+
203
+ def apply(self, actions: List[RemediationAction], dry_run: bool = False) -> Dict[str, Any]:
204
+ """Applies autofixable remediation actions to workspace files."""
205
+ autofixable = [a for a in actions if a.autofixable]
206
+ files_modified = set()
207
+ applied_count = 0
208
+
209
+ # Group actions by file path
210
+ by_file: Dict[str, List[RemediationAction]] = {}
211
+ for a in autofixable:
212
+ by_file.setdefault(a.file_path, []).append(a)
213
+
214
+ for rel_file, file_actions in by_file.items():
215
+ full_path = self.repo_path / rel_file
216
+ if not full_path.exists():
217
+ continue
218
+
219
+ try:
220
+ content = full_path.read_text(encoding="utf-8")
221
+ modified_content = content
222
+ file_changed = False
223
+
224
+ for act in file_actions:
225
+ if act.original_text and act.original_text in modified_content:
226
+ modified_content = modified_content.replace(act.original_text, act.proposed_text, 1)
227
+ applied_count += 1
228
+ file_changed = True
229
+
230
+ if file_changed:
231
+ files_modified.add(rel_file)
232
+ if not dry_run:
233
+ full_path.write_text(modified_content, encoding="utf-8")
234
+
235
+ except Exception as e:
236
+ logger.error("Error applying remediation to %s: %s", rel_file, e)
237
+
238
+ return {
239
+ "success": True,
240
+ "dry_run": dry_run,
241
+ "applied_count": applied_count,
242
+ "total_candidates": len(actions),
243
+ "files_modified": sorted(list(files_modified)),
244
+ }
@@ -0,0 +1 @@
1
+ """CodeDNA visualization — rendering modules."""