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.
- codedna/__init__.py +3 -0
- codedna/analyzers/__init__.py +1 -0
- codedna/analyzers/ai_analyzer.py +111 -0
- codedna/analyzers/architecture_detector.py +150 -0
- codedna/analyzers/cache_manager.py +70 -0
- codedna/analyzers/code_smell_detector.py +302 -0
- codedna/analyzers/complexity_analyzer.py +329 -0
- codedna/analyzers/dependency_mapper.py +171 -0
- codedna/analyzers/developer_analyzer.py +148 -0
- codedna/analyzers/diff_engine.py +395 -0
- codedna/analyzers/dna_generator.py +308 -0
- codedna/analyzers/evolution_engine.py +193 -0
- codedna/analyzers/github_analyzer.py +86 -0
- codedna/analyzers/language_detector.py +151 -0
- codedna/analyzers/repo_cloner.py +91 -0
- codedna/analyzers/security_detector.py +271 -0
- codedna/analyzers/structure_analyzer.py +91 -0
- codedna/ci/__init__.py +5 -0
- codedna/ci/gatekeeper.py +421 -0
- codedna/cli.py +839 -0
- codedna/compliance/__init__.py +5 -0
- codedna/compliance/sbom.py +410 -0
- codedna/config.py +167 -0
- codedna/mcp/__init__.py +5 -0
- codedna/mcp/protocol.py +65 -0
- codedna/mcp/server.py +393 -0
- codedna/remediation/__init__.py +5 -0
- codedna/remediation/planner.py +244 -0
- codedna/visualization/__init__.py +1 -0
- codedna/visualization/html_export.py +507 -0
- codedna/visualization/renderer.py +373 -0
- codedna_analyzer-1.4.0.dist-info/METADATA +425 -0
- codedna_analyzer-1.4.0.dist-info/RECORD +37 -0
- codedna_analyzer-1.4.0.dist-info/WHEEL +5 -0
- codedna_analyzer-1.4.0.dist-info/entry_points.txt +2 -0
- codedna_analyzer-1.4.0.dist-info/licenses/LICENSE +21 -0
- codedna_analyzer-1.4.0.dist-info/top_level.txt +1 -0
codedna/ci/gatekeeper.py
ADDED
|
@@ -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)
|