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.
- jordan_devguard-0.1.0/PKG-INFO +16 -0
- jordan_devguard-0.1.0/pyproject.toml +41 -0
- jordan_devguard-0.1.0/setup.cfg +4 -0
- jordan_devguard-0.1.0/src/devguard/__init__.py +1 -0
- jordan_devguard-0.1.0/src/devguard/cli.py +166 -0
- jordan_devguard-0.1.0/src/devguard/core/detector.py +29 -0
- jordan_devguard-0.1.0/src/devguard/core/finding.py +20 -0
- jordan_devguard-0.1.0/src/devguard/core/scanner.py +59 -0
- jordan_devguard-0.1.0/src/devguard/report/__init__.py +4 -0
- jordan_devguard-0.1.0/src/devguard/report/html.py +312 -0
- jordan_devguard-0.1.0/src/devguard/scanners/__init__.py +7 -0
- jordan_devguard-0.1.0/src/devguard/scanners/dependencies.py +181 -0
- jordan_devguard-0.1.0/src/devguard/scanners/django.py +241 -0
- jordan_devguard-0.1.0/src/devguard/scanners/docker.py +254 -0
- jordan_devguard-0.1.0/src/devguard/scanners/secrets.py +106 -0
- jordan_devguard-0.1.0/src/jordan_devguard.egg-info/PKG-INFO +16 -0
- jordan_devguard-0.1.0/src/jordan_devguard.egg-info/SOURCES.txt +20 -0
- jordan_devguard-0.1.0/src/jordan_devguard.egg-info/dependency_links.txt +1 -0
- jordan_devguard-0.1.0/src/jordan_devguard.egg-info/entry_points.txt +2 -0
- jordan_devguard-0.1.0/src/jordan_devguard.egg-info/requires.txt +10 -0
- jordan_devguard-0.1.0/src/jordan_devguard.egg-info/top_level.txt +1 -0
- jordan_devguard-0.1.0/tests/test_scanner.py +7 -0
|
@@ -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 @@
|
|
|
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,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"]
|