jordan-devguard 0.1.0__tar.gz

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.
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: jordan-devguard
3
+ Version: 0.1.0
4
+ Summary: Local-first security assistant for developers
5
+ License: MIT
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: typer>=0.12
9
+ Requires-Dist: pydantic>=2.0
10
+ Requires-Dist: rich>=13.0
11
+ Requires-Dist: packaging>=23.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest; extra == "dev"
14
+ Requires-Dist: ruff; extra == "dev"
15
+ Requires-Dist: black; extra == "dev"
16
+ Requires-Dist: mypy; extra == "dev"
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "jordan-devguard"
7
+ version = "0.1.0"
8
+ description = "Local-first security assistant for developers"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = {text = "MIT"}
12
+ dependencies = [
13
+ "typer>=0.12",
14
+ "pydantic>=2.0",
15
+ "rich>=13.0",
16
+ "packaging>=23.0"
17
+ ]
18
+
19
+ [project.optional-dependencies]
20
+ dev = [
21
+ "pytest",
22
+ "ruff",
23
+ "black",
24
+ "mypy"
25
+ ]
26
+
27
+ [project.scripts]
28
+ devguard = "devguard.cli:app"
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["src"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
35
+
36
+ [tool.ruff]
37
+ line-length = 100
38
+ target-version = "py310"
39
+
40
+ [tool.black]
41
+ line-length = 100
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,166 @@
1
+ """Interface en ligne de commande DevGuard."""
2
+ import typer
3
+ from rich.console import Console
4
+ from rich.table import Table
5
+ from rich.panel import Panel
6
+ from rich import box
7
+ from pathlib import Path
8
+ import sys
9
+ import io
10
+
11
+ # Forcer l'encodage UTF-8 sur Windows
12
+ if sys.platform == "win32":
13
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
14
+
15
+ from devguard.core.scanner import SecurityScanner
16
+ from devguard import __version__
17
+
18
+ app = typer.Typer(
19
+ name="devguard",
20
+ help="Local-first security assistant for developers.",
21
+ no_args_is_help=True
22
+ )
23
+ console = Console()
24
+
25
+
26
+ def calculate_score(findings) -> int:
27
+ """Calcule le score de securite sur 100."""
28
+ weights = {
29
+ "CRITICAL": 20,
30
+ "HIGH": 10,
31
+ "MEDIUM": 5,
32
+ "LOW": 2,
33
+ "INFO": 0
34
+ }
35
+
36
+ score = 100
37
+ for finding in findings:
38
+ score -= weights.get(finding.severity, 0)
39
+
40
+ return max(0, min(100, score))
41
+
42
+
43
+ def generate_html_report(findings, project_path, score, output_path):
44
+ """Genere un rapport HTML."""
45
+ from devguard.report.html import HTMLReporter
46
+ reporter = HTMLReporter(findings, project_path, score)
47
+ reporter.generate(output_path)
48
+
49
+
50
+ def display_results(findings, score, path):
51
+ """Affiche les resultats dans le terminal."""
52
+ table = Table(
53
+ title=f"Security Scan Results ({len(findings)} issues)",
54
+ box=box.ROUNDED,
55
+ title_style="bold red" if any(f.severity == "CRITICAL" for f in findings) else "bold yellow"
56
+ )
57
+
58
+ table.add_column("Severity", style="bold", width=10)
59
+ table.add_column("Rule", style="cyan", width=15)
60
+ table.add_column("Description", width=35)
61
+ table.add_column("File", width=25)
62
+ table.add_column("Line", width=8, justify="right")
63
+
64
+ for finding in findings:
65
+ severity_style = {
66
+ "CRITICAL": "red bold",
67
+ "HIGH": "red",
68
+ "MEDIUM": "yellow",
69
+ "LOW": "blue",
70
+ "INFO": "white"
71
+ }.get(finding.severity, "white")
72
+
73
+ table.add_row(
74
+ f"[{severity_style}]{finding.severity}[/{severity_style}]",
75
+ finding.rule_id,
76
+ finding.title[:35],
77
+ finding.file[:25],
78
+ str(finding.line) if finding.line else "-"
79
+ )
80
+
81
+ console.print(table)
82
+
83
+ score_color = "red" if score < 50 else "yellow" if score < 75 else "green"
84
+ console.print(Panel(
85
+ f"[bold]Security Score:[/bold] [{score_color}]{score}/100[/{score_color}]\n"
86
+ f"[dim]{len(findings)} issues found[/dim]",
87
+ border_style=score_color,
88
+ width=40
89
+ ))
90
+
91
+
92
+ @app.command()
93
+ def scan(
94
+ path: str = typer.Argument(".", help="Path to project to scan"),
95
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Verbose mode"),
96
+ format: str = typer.Option("terminal", "--format", "-f", help="Output format: terminal, html"),
97
+ output: str = typer.Option("devguard-report.html", "--output", "-o", help="Output file path")
98
+ ):
99
+ """Scan a project for security issues."""
100
+
101
+ console.print(f"\n>> Scanning: {path}\n")
102
+
103
+ try:
104
+ scanner = SecurityScanner(path)
105
+ findings = scanner.scan()
106
+ project_types = scanner.get_project_types()
107
+
108
+ if project_types:
109
+ console.print("[dim]Project types detected:[/dim]")
110
+ for pt in project_types:
111
+ console.print(f" [green]+[/green] {pt}")
112
+ console.print("")
113
+ else:
114
+ console.print("[dim]No specific project type detected[/dim]\n")
115
+
116
+ if verbose:
117
+ console.print("[dim]Verbose mode enabled[/dim]\n")
118
+
119
+ # Calculer le score
120
+ score = calculate_score(findings)
121
+
122
+ # Exporter en HTML si demande
123
+ if format.lower() == "html":
124
+ output_path = Path(output)
125
+ generate_html_report(findings, path, score, output_path)
126
+ console.print(f"[green]Rapport HTML genere : {output_path.absolute()}[/green]")
127
+ if not findings:
128
+ console.print("[green]No security issues found![/green]")
129
+ console.print(f"[green]Score: {score}/100[/green]")
130
+ else:
131
+ display_results(findings, score, path)
132
+ raise typer.Exit(code=0)
133
+
134
+ # Affichage normal
135
+ if not findings:
136
+ console.print("[green]No security issues found![/green]")
137
+ console.print(f"[green]Score: {score}/100[/green]")
138
+ raise typer.Exit(code=0)
139
+
140
+ display_results(findings, score, path)
141
+
142
+ if any(f.severity == "CRITICAL" for f in findings):
143
+ console.print("\n[red bold]CRITICAL issues detected![/red bold]")
144
+ console.print("[dim]Review and fix immediately before deployment.[/dim]")
145
+
146
+ raise typer.Exit(code=0)
147
+
148
+ except FileNotFoundError as error:
149
+ console.print(f"[red]ERROR {error}[/red]")
150
+ raise typer.Exit(code=1)
151
+ except Exception as error:
152
+ console.print(f"[red]ERROR Unexpected error: {error}[/red]")
153
+ if verbose:
154
+ import traceback
155
+ console.print(traceback.format_exc())
156
+ raise typer.Exit(code=1)
157
+
158
+
159
+ @app.command()
160
+ def version():
161
+ """Display DevGuard version."""
162
+ console.print(f"[bold cyan]DevGuard[/bold cyan] [green]v{__version__}[/green]")
163
+
164
+
165
+ if __name__ == "__main__":
166
+ app()
@@ -0,0 +1,29 @@
1
+ from pathlib import Path
2
+ from typing import List
3
+
4
+ class ProjectDetector:
5
+ def __init__(self, project_path: str):
6
+ self.path = Path(project_path)
7
+
8
+ def detect(self) -> List[str]:
9
+ types = []
10
+
11
+ if (self.path / "manage.py").exists():
12
+ types.append("django")
13
+ elif (self.path / "settings.py").exists():
14
+ types.append("django")
15
+
16
+ if (self.path / "requirements.txt").exists():
17
+ types.append("python")
18
+ elif (self.path / "pyproject.toml").exists():
19
+ types.append("python")
20
+
21
+ if (self.path / "package.json").exists():
22
+ types.append("node")
23
+
24
+ if (self.path / "Dockerfile").exists():
25
+ types.append("docker")
26
+ elif (self.path / "docker-compose.yml").exists():
27
+ types.append("docker")
28
+
29
+ return types
@@ -0,0 +1,20 @@
1
+ from enum import Enum
2
+ from pydantic import BaseModel
3
+
4
+
5
+ class Severity(str, Enum):
6
+ CRITICAL = "CRITICAL"
7
+ HIGH = "HIGH"
8
+ MEDIUM = "MEDIUM"
9
+ LOW = "LOW"
10
+ INFO = "INFO"
11
+
12
+
13
+ class Finding(BaseModel):
14
+ rule_id: str
15
+ severity: Severity
16
+ title: str
17
+ file: str
18
+ line: int | None = None
19
+ description: str
20
+ recommendation: str
@@ -0,0 +1,59 @@
1
+ from pathlib import Path
2
+ from typing import List
3
+
4
+ from devguard.core.finding import Finding
5
+ from devguard.core.detector import ProjectDetector
6
+ from devguard.scanners.secrets import SecretsScanner
7
+ from devguard.scanners.django import DjangoScanner
8
+ from devguard.scanners.docker import DockerScanner
9
+ from devguard.scanners.dependencies import DependencyScanner
10
+
11
+ class SecurityScanner:
12
+ def __init__(self, project_path: str):
13
+ self.path = Path(project_path)
14
+ self.project_types = ProjectDetector(project_path).detect()
15
+
16
+ self.scanners = []
17
+
18
+ # Secrets scanner - toujours actif
19
+ self.scanners.append(SecretsScanner(project_path))
20
+
21
+ # Django scanner
22
+ django_scanner = DjangoScanner(project_path)
23
+ if django_scanner.is_applicable():
24
+ self.scanners.append(django_scanner)
25
+ if "django" not in self.project_types:
26
+ self.project_types.append("django")
27
+
28
+ # Docker scanner
29
+ docker_scanner = DockerScanner(project_path)
30
+ if docker_scanner.is_applicable():
31
+ self.scanners.append(docker_scanner)
32
+ if "docker" not in self.project_types:
33
+ self.project_types.append("docker")
34
+
35
+ # Dependency scanner
36
+ dep_scanner = DependencyScanner(project_path)
37
+ if dep_scanner.is_applicable():
38
+ self.scanners.append(dep_scanner)
39
+ if "dependencies" not in self.project_types:
40
+ self.project_types.append("dependencies")
41
+
42
+ def scan(self) -> List[Finding]:
43
+ if not self.path.exists():
44
+ raise FileNotFoundError(f"Project not found: {self.path}")
45
+
46
+ all_findings = []
47
+
48
+ for scanner in self.scanners:
49
+ try:
50
+ findings = scanner.scan()
51
+ all_findings.extend(findings)
52
+ except Exception as e:
53
+ # Ignorer les erreurs d'un scanner, continuer avec les autres
54
+ pass
55
+
56
+ return all_findings
57
+
58
+ def get_project_types(self) -> List[str]:
59
+ return self.project_types
@@ -0,0 +1,4 @@
1
+ """Report generation module for DevGuard."""
2
+ from devguard.report.html import HTMLReporter
3
+
4
+ __all__ = ["HTMLReporter"]
@@ -0,0 +1,312 @@
1
+ """Generateur de rapport HTML pour DevGuard."""
2
+ from pathlib import Path
3
+ from datetime import datetime
4
+ from typing import List
5
+
6
+ from devguard.core.finding import Finding
7
+
8
+ class HTMLReporter:
9
+ """Genere un rapport HTML des findings."""
10
+
11
+ def __init__(self, findings: List[Finding], project_path: str, score: int):
12
+ self.findings = findings
13
+ self.project_path = Path(project_path)
14
+ self.score = score
15
+ self.timestamp = datetime.now()
16
+
17
+ def generate(self, output_path: Path) -> None:
18
+ """Genere le rapport HTML."""
19
+
20
+ # Compter les findings par severite
21
+ counts = {
22
+ "CRITICAL": 0,
23
+ "HIGH": 0,
24
+ "MEDIUM": 0,
25
+ "LOW": 0,
26
+ "INFO": 0
27
+ }
28
+ for f in self.findings:
29
+ if f.severity in counts:
30
+ counts[f.severity] += 1
31
+
32
+ html = f"""
33
+ <!DOCTYPE html>
34
+ <html lang="fr">
35
+ <head>
36
+ <meta charset="UTF-8">
37
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
38
+ <title>DevGuard - Rapport de securite</title>
39
+ <style>
40
+ * {{
41
+ margin: 0;
42
+ padding: 0;
43
+ box-sizing: border-box;
44
+ }}
45
+ body {{
46
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
47
+ background: #f5f7fa;
48
+ padding: 40px;
49
+ color: #2d3748;
50
+ }}
51
+ .container {{
52
+ max-width: 1200px;
53
+ margin: 0 auto;
54
+ }}
55
+ .header {{
56
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
57
+ color: white;
58
+ padding: 40px;
59
+ border-radius: 12px;
60
+ margin-bottom: 30px;
61
+ }}
62
+ .header h1 {{
63
+ font-size: 32px;
64
+ margin-bottom: 10px;
65
+ }}
66
+ .header p {{
67
+ opacity: 0.9;
68
+ font-size: 16px;
69
+ }}
70
+ .score-card {{
71
+ background: white;
72
+ padding: 30px;
73
+ border-radius: 12px;
74
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
75
+ margin-bottom: 30px;
76
+ text-align: center;
77
+ }}
78
+ .score-number {{
79
+ font-size: 72px;
80
+ font-weight: bold;
81
+ color: {self._get_score_color()};
82
+ }}
83
+ .score-label {{
84
+ font-size: 18px;
85
+ color: #718096;
86
+ }}
87
+ .score-grade {{
88
+ font-size: 24px;
89
+ font-weight: bold;
90
+ color: {self._get_score_color()};
91
+ margin-top: 10px;
92
+ }}
93
+ .stats {{
94
+ display: grid;
95
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
96
+ gap: 20px;
97
+ margin-bottom: 30px;
98
+ }}
99
+ .stat-card {{
100
+ background: white;
101
+ padding: 20px;
102
+ border-radius: 12px;
103
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
104
+ text-align: center;
105
+ }}
106
+ .stat-number {{
107
+ font-size: 32px;
108
+ font-weight: bold;
109
+ }}
110
+ .stat-label {{
111
+ font-size: 14px;
112
+ color: #718096;
113
+ margin-top: 5px;
114
+ }}
115
+ .stat-critical .stat-number {{ color: #e53e3e; }}
116
+ .stat-high .stat-number {{ color: #ed8936; }}
117
+ .stat-medium .stat-number {{ color: #ecc94b; }}
118
+ .stat-low .stat-number {{ color: #48bb78; }}
119
+ .findings-table {{
120
+ background: white;
121
+ border-radius: 12px;
122
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
123
+ overflow: hidden;
124
+ }}
125
+ .findings-table table {{
126
+ width: 100%;
127
+ border-collapse: collapse;
128
+ }}
129
+ .findings-table th {{
130
+ background: #f7fafc;
131
+ padding: 15px 20px;
132
+ text-align: left;
133
+ font-weight: 600;
134
+ color: #4a5568;
135
+ border-bottom: 2px solid #e2e8f0;
136
+ }}
137
+ .findings-table td {{
138
+ padding: 15px 20px;
139
+ border-bottom: 1px solid #e2e8f0;
140
+ }}
141
+ .findings-table tr:hover {{
142
+ background: #f7fafc;
143
+ }}
144
+ .severity-badge {{
145
+ display: inline-block;
146
+ padding: 4px 12px;
147
+ border-radius: 20px;
148
+ font-size: 12px;
149
+ font-weight: bold;
150
+ text-transform: uppercase;
151
+ }}
152
+ .severity-CRITICAL {{ background: #fed7d7; color: #9b2c2c; }}
153
+ .severity-HIGH {{ background: #feebc8; color: #9c4221; }}
154
+ .severity-MEDIUM {{ background: #fefcbf; color: #975a16; }}
155
+ .severity-LOW {{ background: #c6f6d5; color: #276749; }}
156
+ .severity-INFO {{ background: #e2e8f0; color: #4a5568; }}
157
+ .recommendation {{
158
+ background: #ebf8ff;
159
+ padding: 10px 15px;
160
+ border-radius: 8px;
161
+ font-size: 14px;
162
+ color: #2b6cb0;
163
+ margin-top: 10px;
164
+ }}
165
+ .recommendation strong {{
166
+ color: #2c5282;
167
+ }}
168
+ .footer {{
169
+ text-align: center;
170
+ padding: 30px;
171
+ color: #a0aec0;
172
+ font-size: 14px;
173
+ }}
174
+ .project-info {{
175
+ background: white;
176
+ padding: 20px 30px;
177
+ border-radius: 12px;
178
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
179
+ margin-bottom: 30px;
180
+ }}
181
+ .project-info p {{
182
+ margin: 5px 0;
183
+ color: #4a5568;
184
+ }}
185
+ .project-info strong {{
186
+ color: #2d3748;
187
+ }}
188
+ @media (max-width: 768px) {{
189
+ body {{ padding: 20px; }}
190
+ .header {{ padding: 20px; }}
191
+ .stats {{ grid-template-columns: repeat(2, 1fr); }}
192
+ .findings-table {{
193
+ overflow-x: auto;
194
+ }}
195
+ }}
196
+ </style>
197
+ </head>
198
+ <body>
199
+ <div class="container">
200
+ <!-- Header -->
201
+ <div class="header">
202
+ <h1>DevGuard - Rapport de securite</h1>
203
+ <p>Analyse de securite automatisee pour developpeurs</p>
204
+ </div>
205
+
206
+ <!-- Project Info -->
207
+ <div class="project-info">
208
+ <p><strong>Projet :</strong> {self.project_path.absolute()}</p>
209
+ <p><strong>Date :</strong> {self.timestamp.strftime('%d/%m/%Y a %H:%M')}</p>
210
+ <p><strong>Total :</strong> {len(self.findings)} probleme(s) detecte(s)</p>
211
+ </div>
212
+
213
+ <!-- Score -->
214
+ <div class="score-card">
215
+ <div class="score-number">{self.score}</div>
216
+ <div class="score-label">Score de securite</div>
217
+ <div class="score-grade">{self._get_grade()}</div>
218
+ </div>
219
+
220
+ <!-- Stats -->
221
+ <div class="stats">
222
+ <div class="stat-card stat-critical">
223
+ <div class="stat-number">{counts['CRITICAL']}</div>
224
+ <div class="stat-label">Critique</div>
225
+ </div>
226
+ <div class="stat-card stat-high">
227
+ <div class="stat-number">{counts['HIGH']}</div>
228
+ <div class="stat-label">Eleve</div>
229
+ </div>
230
+ <div class="stat-card stat-medium">
231
+ <div class="stat-number">{counts['MEDIUM']}</div>
232
+ <div class="stat-label">Moyen</div>
233
+ </div>
234
+ <div class="stat-card stat-low">
235
+ <div class="stat-number">{counts['LOW']}</div>
236
+ <div class="stat-label">Faible</div>
237
+ </div>
238
+ </div>
239
+
240
+ <!-- Findings Table -->
241
+ <div class="findings-table">
242
+ <h3 style="padding: 20px; font-size: 18px;">Detail des problemes</h3>
243
+ <table>
244
+ <thead>
245
+ <tr>
246
+ <th>Severite</th>
247
+ <th>Regle</th>
248
+ <th>Description</th>
249
+ <th>Fichier</th>
250
+ <th>Ligne</th>
251
+ </tr>
252
+ </thead>
253
+ <tbody>
254
+ """
255
+
256
+ for finding in self.findings:
257
+ html += f"""
258
+ <tr>
259
+ <td><span class="severity-badge severity-{finding.severity}">{finding.severity}</span></td>
260
+ <td><code style="background: #f0f0f0; padding: 2px 8px; border-radius: 4px; font-size: 13px;">{finding.rule_id}</code></td>
261
+ <td>
262
+ <div style="font-weight: 500;">{finding.title}</div>
263
+ <div style="font-size: 13px; color: #718096; margin-top: 4px;">{finding.description[:150]}{'...' if len(finding.description) > 150 else ''}</div>
264
+ <div class="recommendation">
265
+ <strong>Recommandation :</strong> {finding.recommendation}
266
+ </div>
267
+ </td>
268
+ <td style="font-size: 14px; color: #4a5568;">{finding.file}</td>
269
+ <td style="text-align: center; font-weight: 500;">{finding.line if finding.line else '-'}</td>
270
+ </tr>
271
+ """
272
+
273
+ html += """
274
+ </tbody>
275
+ </table>
276
+ </div>
277
+
278
+ <div class="footer">
279
+ <p>Genere par DevGuard v0.1.0</p>
280
+ <p style="margin-top: 5px;">Secure your code before you deploy it.</p>
281
+ </div>
282
+ </div>
283
+ </body>
284
+ </html>
285
+ """
286
+
287
+ # Ecrire le fichier
288
+ output_path.write_text(html, encoding='utf-8')
289
+
290
+ def _get_score_color(self) -> str:
291
+ """Retourne la couleur en fonction du score."""
292
+ if self.score >= 90:
293
+ return '#48bb78' # Vert
294
+ elif self.score >= 75:
295
+ return '#ecc94b' # Jaune
296
+ elif self.score >= 50:
297
+ return '#ed8936' # Orange
298
+ else:
299
+ return '#e53e3e' # Rouge
300
+
301
+ def _get_grade(self) -> str:
302
+ """Retourne la mention en fonction du score."""
303
+ if self.score >= 90:
304
+ return 'Excellent'
305
+ elif self.score >= 75:
306
+ return 'Bon'
307
+ elif self.score >= 50:
308
+ return 'A ameliorer'
309
+ elif self.score >= 25:
310
+ return 'Critique'
311
+ else:
312
+ return 'Urgent'
@@ -0,0 +1,7 @@
1
+ """Scanners package for DevGuard."""
2
+ from devguard.scanners.secrets import SecretsScanner
3
+ from devguard.scanners.django import DjangoScanner
4
+ from devguard.scanners.docker import DockerScanner
5
+ from devguard.scanners.dependencies import DependencyScanner
6
+
7
+ __all__ = ["SecretsScanner", "DjangoScanner", "DockerScanner", "DependencyScanner"]