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
@@ -0,0 +1,421 @@
1
+ """CodeDNA Gatekeeper — validates architectural policies & enforces quality gates."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import asdict, dataclass, field
7
+ from datetime import datetime
8
+
9
+ from codedna import __version__
10
+
11
+
12
+ @dataclass
13
+ class PolicyEvaluation:
14
+ """Evaluation result for a single architectural or quality policy."""
15
+
16
+ rule_id: str
17
+ name: str
18
+ passed: bool
19
+ severity: str # "error" | "warning"
20
+ message: str
21
+
22
+
23
+ @dataclass
24
+ class GatekeeperResult:
25
+ """Complete evaluation outcome for CI pipeline execution."""
26
+
27
+ passed: bool
28
+ overall_health: str
29
+ policies_evaluated: int
30
+ policies_passed: int
31
+ policies_failed: int
32
+ evaluations: list[PolicyEvaluation] = field(default_factory=list)
33
+ timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
34
+
35
+ def to_dict(self) -> dict:
36
+ """Convert result to dictionary representation."""
37
+ return asdict(self)
38
+
39
+
40
+ class Gatekeeper:
41
+ """Enforces architecture, security, and code quality gates for continuous delivery."""
42
+
43
+ HEALTH_HIERARCHY = {
44
+ "Healthy": 4,
45
+ "Fair": 3,
46
+ "Needs Attention": 2,
47
+ "Critical": 1,
48
+ "Unknown": 0,
49
+ }
50
+
51
+ def __init__(
52
+ self,
53
+ min_health: str = "Fair",
54
+ max_smells: int | None = None,
55
+ max_critical_smells: int = 0,
56
+ fail_on_secrets: bool = True,
57
+ fail_on_circular: bool = False,
58
+ max_density: float | None = None,
59
+ max_cyclomatic_complexity: int | None = None,
60
+ min_maintainability_index: float | None = None,
61
+ ):
62
+ """Initialize the Gatekeeper with policy thresholds."""
63
+ self.min_health = min_health
64
+ self.max_smells = max_smells
65
+ self.max_critical_smells = max_critical_smells
66
+ self.fail_on_secrets = fail_on_secrets
67
+ self.fail_on_circular = fail_on_circular
68
+ self.max_density = max_density
69
+ self.max_cyclomatic_complexity = max_cyclomatic_complexity
70
+ self.min_maintainability_index = min_maintainability_index
71
+
72
+ def evaluate(self, profile: dict) -> GatekeeperResult:
73
+ """Evaluate a DNA profile against the configured policies.
74
+
75
+ Args:
76
+ profile: Complete DNA profile generated by DNAGenerator.
77
+
78
+ Returns:
79
+ GatekeeperResult with detailed policy evaluations and pass/fail status.
80
+ """
81
+ evaluations: list[PolicyEvaluation] = []
82
+
83
+ health_data = profile.get("health", {})
84
+ current_health = health_data.get("overall", "Unknown")
85
+ severity_counts = health_data.get("severity_counts", {})
86
+ crit_smells = severity_counts.get("critical", 0)
87
+ warn_smells = severity_counts.get("warning", 0)
88
+ total_smells = crit_smells + warn_smells + severity_counts.get("info", 0)
89
+
90
+ deps_data = profile.get("dependencies", {})
91
+ has_circular = deps_data.get("has_circular_deps", False)
92
+ density = deps_data.get("density", 0.0)
93
+
94
+ security_data = profile.get("security", {})
95
+ has_secrets = security_data.get("has_secrets", False)
96
+ total_critical_vulns = security_data.get("total_critical", 0)
97
+
98
+ # 1. Minimum Health Policy
99
+ target_rank = self.HEALTH_HIERARCHY.get(self.min_health, 3)
100
+ actual_rank = self.HEALTH_HIERARCHY.get(current_health, 0)
101
+ health_pass = actual_rank >= target_rank
102
+ evaluations.append(
103
+ PolicyEvaluation(
104
+ rule_id="DNA-HLT-001",
105
+ name="Minimum Codebase Health",
106
+ passed=health_pass,
107
+ severity="error",
108
+ message=(
109
+ f"Overall health '{current_health}' meets or exceeds requirement '{self.min_health}'"
110
+ if health_pass
111
+ else f"Overall health '{current_health}' falls below required '{self.min_health}'"
112
+ ),
113
+ )
114
+ )
115
+
116
+ # 2. Hardcoded Secrets Policy
117
+ if self.fail_on_secrets:
118
+ secrets_pass = not has_secrets and total_critical_vulns == 0
119
+ vuln_count = len(security_data.get("vulnerabilities", []))
120
+ evaluations.append(
121
+ PolicyEvaluation(
122
+ rule_id="DNA-SEC-001",
123
+ name="Zero Hardcoded Secrets & Critical CVEs",
124
+ passed=secrets_pass,
125
+ severity="error",
126
+ message=(
127
+ "No credentials or critical security vulnerabilities detected"
128
+ if secrets_pass
129
+ else f"Security scan found {vuln_count} vulnerability findings"
130
+ ),
131
+ )
132
+ )
133
+
134
+ # 3. Critical Code Smells Policy
135
+ crit_pass = crit_smells <= self.max_critical_smells
136
+ evaluations.append(
137
+ PolicyEvaluation(
138
+ rule_id="DNA-SML-001",
139
+ name="Critical Smells Ceiling",
140
+ passed=crit_pass,
141
+ severity="error",
142
+ message=(
143
+ f"Critical smells ({crit_smells}) within allowable limit ({self.max_critical_smells})"
144
+ if crit_pass
145
+ else f"Critical smells ({crit_smells}) exceeded allowable limit ({self.max_critical_smells})"
146
+ ),
147
+ )
148
+ )
149
+
150
+ # 4. Total Code Smells Ceiling (Optional)
151
+ if self.max_smells is not None:
152
+ smells_pass = total_smells <= self.max_smells
153
+ evaluations.append(
154
+ PolicyEvaluation(
155
+ rule_id="DNA-SML-002",
156
+ name="Total Smells Threshold",
157
+ passed=smells_pass,
158
+ severity="warning" if self.max_smells > 20 else "error",
159
+ message=(
160
+ f"Total smells ({total_smells}) within limit ({self.max_smells})"
161
+ if smells_pass
162
+ else f"Total smells ({total_smells}) exceeded threshold ({self.max_smells})"
163
+ ),
164
+ )
165
+ )
166
+
167
+ # 5. Circular Dependency Policy
168
+ if self.fail_on_circular:
169
+ circ_pass = not has_circular
170
+ evaluations.append(
171
+ PolicyEvaluation(
172
+ rule_id="DNA-DEP-001",
173
+ name="No Circular Dependencies",
174
+ passed=circ_pass,
175
+ severity="error",
176
+ message=(
177
+ "Architecture dependency graph is acyclic"
178
+ if circ_pass
179
+ else f"Detected {deps_data.get('circular_count', 0)} circular dependency cycles"
180
+ ),
181
+ )
182
+ )
183
+
184
+ # 6. Graph Density Policy
185
+ if self.max_density is not None:
186
+ density_pass = density <= self.max_density
187
+ evaluations.append(
188
+ PolicyEvaluation(
189
+ rule_id="DNA-DEP-002",
190
+ name="Coupling Density Limit",
191
+ passed=density_pass,
192
+ severity="warning",
193
+ message=(
194
+ f"Dependency density {density:.4f} within limit {self.max_density:.4f}"
195
+ if density_pass
196
+ else f"Dependency density {density:.4f} exceeds limit {self.max_density:.4f}"
197
+ ),
198
+ )
199
+ )
200
+
201
+ # 7. Complexity Ceiling Policy
202
+ complexity_info = profile.get("smells", {}).get("complexity", {}) or profile.get("complexity", {})
203
+ if self.max_cyclomatic_complexity is not None and complexity_info:
204
+ max_cc = complexity_info.get("max_cyclomatic_complexity", 1)
205
+ cc_pass = max_cc <= self.max_cyclomatic_complexity
206
+ evaluations.append(
207
+ PolicyEvaluation(
208
+ rule_id="DNA-CMP-001",
209
+ name="Cyclomatic Complexity Ceiling",
210
+ passed=cc_pass,
211
+ severity="error" if self.max_cyclomatic_complexity < 20 else "warning",
212
+ message=(
213
+ f"Peak cyclomatic complexity ({max_cc}) within limit ({self.max_cyclomatic_complexity})"
214
+ if cc_pass
215
+ else f"Peak cyclomatic complexity ({max_cc}) exceeds limit ({self.max_cyclomatic_complexity})"
216
+ ),
217
+ )
218
+ )
219
+
220
+ # 8. Maintainability Index Floor Policy
221
+ if self.min_maintainability_index is not None and complexity_info:
222
+ current_mi = complexity_info.get("maintainability_index", 100.0)
223
+ mi_pass = current_mi >= self.min_maintainability_index
224
+ evaluations.append(
225
+ PolicyEvaluation(
226
+ rule_id="DNA-CMP-002",
227
+ name="Maintainability Index Floor",
228
+ passed=mi_pass,
229
+ severity="error" if self.min_maintainability_index > 65 else "warning",
230
+ message=(
231
+ f"Maintainability Index ({current_mi}) meets required floor ({self.min_maintainability_index})"
232
+ if mi_pass
233
+ else f"Maintainability Index ({current_mi}) below required floor ({self.min_maintainability_index})"
234
+ ),
235
+ )
236
+ )
237
+
238
+ # Final Verdict
239
+ passed_count = sum(1 for e in evaluations if e.passed)
240
+ failed_count = sum(1 for e in evaluations if not e.passed)
241
+ overall_pass = all(e.passed for e in evaluations if e.severity == "error")
242
+
243
+ return GatekeeperResult(
244
+ passed=overall_pass,
245
+ overall_health=current_health,
246
+ policies_evaluated=len(evaluations),
247
+ policies_passed=passed_count,
248
+ policies_failed=failed_count,
249
+ evaluations=evaluations,
250
+ )
251
+
252
+ def to_sarif(self, result: GatekeeperResult, profile: dict) -> dict:
253
+ """Generate standard SARIF v2.1.0 output for GitHub Code Scanning integration."""
254
+ rules = []
255
+ rule_indices = {}
256
+
257
+ # Define standard rules in SARIF schema
258
+ standard_rules = [
259
+ {
260
+ "id": "DNA-HLT-001",
261
+ "name": "MinimumCodebaseHealth",
262
+ "shortDescription": {"text": "CodeDNA Minimum Codebase Health Gate"},
263
+ "fullDescription": {"text": "Enforces that the codebase overall health does not degrade below policy."},
264
+ "defaultConfiguration": {"level": "error"},
265
+ },
266
+ {
267
+ "id": "DNA-SEC-001",
268
+ "name": "HardcodedSecretsOrVulnerabilities",
269
+ "shortDescription": {"text": "Hardcoded Secret or Security Vulnerability"},
270
+ "fullDescription": {"text": "Identifies hardcoded API tokens, private keys, or critical security vulnerabilities."},
271
+ "defaultConfiguration": {"level": "error"},
272
+ },
273
+ {
274
+ "id": "DNA-SML-001",
275
+ "name": "CriticalCodeSmellCeiling",
276
+ "shortDescription": {"text": "Critical Code Smell Limit Exceeded"},
277
+ "fullDescription": {"text": "Enforces that severe structural issues such as God Classes do not exceed thresholds."},
278
+ "defaultConfiguration": {"level": "error"},
279
+ },
280
+ {
281
+ "id": "DNA-SML-002",
282
+ "name": "TotalCodeSmellsThreshold",
283
+ "shortDescription": {"text": "Total Code Smells Limit Exceeded"},
284
+ "fullDescription": {"text": "Enforces that cumulative code smell findings remain within acceptable boundaries."},
285
+ "defaultConfiguration": {"level": "warning"},
286
+ },
287
+ {
288
+ "id": "DNA-DEP-001",
289
+ "name": "CircularDependencyDetected",
290
+ "shortDescription": {"text": "Circular Dependency Cycle"},
291
+ "fullDescription": {"text": "Architectural coupling cycle detected between modules."},
292
+ "defaultConfiguration": {"level": "error"},
293
+ },
294
+ {
295
+ "id": "DNA-DEP-002",
296
+ "name": "CouplingDensityLimit",
297
+ "shortDescription": {"text": "Coupling Density Limit Exceeded"},
298
+ "fullDescription": {"text": "Enforces that dependency density between modules stays below complexity limits."},
299
+ "defaultConfiguration": {"level": "warning"},
300
+ },
301
+ {
302
+ "id": "DNA-CMP-001",
303
+ "name": "CyclomaticComplexityCeiling",
304
+ "shortDescription": {"text": "Peak Cyclomatic Complexity Limit Exceeded"},
305
+ "fullDescription": {"text": "Enforces that peak function cyclomatic complexity does not exceed thresholds."},
306
+ "defaultConfiguration": {"level": "error"},
307
+ },
308
+ {
309
+ "id": "DNA-CMP-002",
310
+ "name": "MaintainabilityIndexFloor",
311
+ "shortDescription": {"text": "Maintainability Index Below Floor"},
312
+ "fullDescription": {"text": "Enforces that system maintainability index meets required rating."},
313
+ "defaultConfiguration": {"level": "error"},
314
+ },
315
+ ]
316
+
317
+ for idx, r in enumerate(standard_rules):
318
+ rules.append(r)
319
+ rule_indices[r["id"]] = idx
320
+
321
+ sarif_results = []
322
+
323
+ # 1. Add Gatekeeper policy breaches
324
+ for evaluation in result.evaluations:
325
+ if not evaluation.passed:
326
+ rule_idx = rule_indices.get(evaluation.rule_id, 0)
327
+ level = "error" if evaluation.severity == "error" else "warning"
328
+ sarif_results.append({
329
+ "ruleId": evaluation.rule_id,
330
+ "ruleIndex": rule_idx,
331
+ "level": level,
332
+ "message": {"text": f"{evaluation.name}: {evaluation.message}"},
333
+ "locations": [
334
+ {
335
+ "physicalLocation": {
336
+ "artifactLocation": {"uri": "pyproject.toml"},
337
+ "region": {"startLine": 1},
338
+ }
339
+ }
340
+ ],
341
+ })
342
+
343
+ # 2. Add individual secrets if present
344
+ security = profile.get("security", {})
345
+ for vuln in security.get("vulnerabilities", []):
346
+ raw_file = vuln.get("file", "unknown")
347
+ line = 1
348
+ if ":" in raw_file:
349
+ parts = raw_file.split(":")
350
+ raw_file = parts[0]
351
+ try:
352
+ line = int(parts[1])
353
+ except (ValueError, IndexError):
354
+ line = 1
355
+
356
+ sarif_results.append({
357
+ "ruleId": "DNA-SEC-001",
358
+ "ruleIndex": rule_indices.get("DNA-SEC-001", 1),
359
+ "level": "error",
360
+ "message": {"text": f"{vuln.get('type')}: {vuln.get('detail')}"},
361
+ "locations": [
362
+ {
363
+ "physicalLocation": {
364
+ "artifactLocation": {"uri": raw_file.replace("\\", "/")},
365
+ "region": {"startLine": line},
366
+ }
367
+ }
368
+ ],
369
+ })
370
+
371
+ return {
372
+ "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
373
+ "version": "2.1.0",
374
+ "runs": [
375
+ {
376
+ "tool": {
377
+ "driver": {
378
+ "name": "CodeDNA Gatekeeper",
379
+ "semanticVersion": __version__,
380
+ "informationUri": "https://github.com/shenald-dev/codedna",
381
+ "rules": rules,
382
+ }
383
+ },
384
+ "results": sarif_results,
385
+ }
386
+ ],
387
+ }
388
+
389
+ def to_markdown(self, result: GatekeeperResult, profile: dict) -> str:
390
+ """Format Gatekeeper evaluation as a GitHub Flavored Markdown PR comment report."""
391
+ badge = "🟢 PASSED" if result.passed else "🔴 FAILED"
392
+ lines = [
393
+ f"## 🛡️ CodeDNA Quality Gatekeeper — {badge}\n",
394
+ f"> **Overall Health:** `{result.overall_health}` ",
395
+ f"> **Policies Evaluated:** **{result.policies_evaluated}** (Passed: {result.policies_passed}, Failed: {result.policies_failed}) ",
396
+ f"> **Evaluated At:** {result.timestamp[:19]}\n",
397
+ "---\n",
398
+ "### 📋 Architectural & Quality Policy Checklist\n",
399
+ "| Rule ID | Policy | Severity | Status | Message |",
400
+ "| :--- | :--- | :---: | :---: | :--- |",
401
+ ]
402
+
403
+ for e in result.evaluations:
404
+ status_icon = "✅ Pass" if e.passed else "❌ Breach"
405
+ lines.append(f"| `{e.rule_id}` | {e.name} | `{e.severity.upper()}` | {status_icon} | {e.message} |")
406
+
407
+ security = profile.get("security", {})
408
+ vulns = security.get("vulnerabilities", [])
409
+ if vulns:
410
+ lines.append("\n### 🔒 Security Alerts")
411
+ lines.append("| Type | Location | Detail |")
412
+ lines.append("| :--- | :--- | :--- |")
413
+ for v in vulns:
414
+ lines.append(f"| `{v.get('type')}` | `{v.get('file')}` | {v.get('detail')} |")
415
+
416
+ lines.append("\n---\n*Generated by [CodeDNA Gatekeeper](https://github.com/shenald-dev/codedna)*")
417
+ return "\n".join(lines)
418
+
419
+ def to_json(self, result: GatekeeperResult) -> str:
420
+ """Serialize result to formatted JSON string."""
421
+ return json.dumps(result.to_dict(), indent=2)