ragsec 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,11 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ .ruff_cache/
8
+ *.egg
9
+ .venv/
10
+ report.md
11
+ report.html
ragsec-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: ragsec
3
+ Version: 0.1.0
4
+ Summary: Static security scanner for RAG pipelines
5
+ Author-email: Hrushikesh Yadav <yadavhrushikesh65@gmail.com>
6
+ License-Expression: Apache-2.0
7
+ Keywords: rag,scanner,security,static-analysis,vector-store
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Topic :: Security
11
+ Classifier: Topic :: Software Development :: Quality Assurance
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: click>=8.0
14
+ Requires-Dist: jinja2>=3.0
15
+ Requires-Dist: rich>=13.0
16
+ Description-Content-Type: text/markdown
17
+
18
+ # RAGGuard
19
+
20
+ Static security scanner for RAG pipelines. Finds injection vulnerabilities, secret logging, auth gaps, and resource safety issues in Python codebases.
21
+
22
+ Built from real-world security audits of production RAG frameworks.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install ragguard
28
+ ```
29
+
30
+ Or from source:
31
+
32
+ ```bash
33
+ git clone https://github.com/HrushiYadav/ragGuard.git
34
+ cd ragguard
35
+ pip install -e .
36
+ ```
37
+
38
+ ## Usage
39
+
40
+ ```bash
41
+ # Terminal output (default)
42
+ ragguard scan ./path/to/codebase
43
+
44
+ # Generate reports
45
+ ragguard scan ./path/to/codebase --output report.md --format markdown
46
+ ragguard scan ./path/to/codebase --output report.html --format html
47
+
48
+ # Filter by severity or category
49
+ ragguard scan ./path/to/codebase --severity high
50
+ ragguard scan ./path/to/codebase --category filter-injection
51
+ ```
52
+
53
+ ## What it detects
54
+
55
+ | Scanner | Severity | What it finds |
56
+ |---------|----------|---------------|
57
+ | Filter Injection | HIGH | f-string interpolation in Milvus, Valkey, Azure, Elasticsearch filter expressions |
58
+ | NoSQL Injection | HIGH | Unvalidated dict values in MongoDB/Elasticsearch queries |
59
+ | SQL Injection | HIGH | f-string SQL construction (INSERT, DELETE, SELECT, UPDATE) |
60
+ | Secret Logging | MEDIUM | API keys, passwords, connection strings in logger calls |
61
+ | Auth Gaps | MEDIUM | FastAPI/Flask routes without auth, client-controlled user IDs (IDOR) |
62
+ | Resource Safety | HIGH/MEDIUM/LOW | pickle deserialization, zip bombs, eval/exec, unbounded reads |
63
+
64
+ ## Example output
65
+
66
+ ```
67
+ RAGGuard scanning ./my-rag-app
68
+
69
+ RG-001 [HIGH] Filter injection: Possible filter expression injection
70
+ vector_stores/store.py:42
71
+ > conditions.append(f'(metadata["{key}"] == "{value}")')
72
+
73
+ RG-002 [HIGH] NoSQL injection: Filter value passed into query
74
+ vector_stores/mongo.py:89
75
+ > filter_dict["payload." + key] = value
76
+
77
+ Summary
78
+ +------------------+
79
+ | Severity | Count |
80
+ |----------+-------|
81
+ | HIGH | 5 |
82
+ | MEDIUM | 8 |
83
+ | LOW | 3 |
84
+ | Total | 16 |
85
+ +------------------+
86
+ ```
87
+
88
+ ## HTML Report
89
+
90
+ Generate a styled HTML report for sharing:
91
+
92
+ ```bash
93
+ ragguard scan ./my-rag-app --output report.html --format html
94
+ ```
95
+
96
+ Dark theme with severity badges, code snippets, and remediation guidance.
97
+
98
+ ## Development
99
+
100
+ ```bash
101
+ pip install -e .
102
+ pytest tests/ -v
103
+ ruff check ragguard/
104
+ ```
105
+
106
+ ## License
107
+
108
+ Apache-2.0
ragsec-0.1.0/README.md ADDED
@@ -0,0 +1,91 @@
1
+ # RAGGuard
2
+
3
+ Static security scanner for RAG pipelines. Finds injection vulnerabilities, secret logging, auth gaps, and resource safety issues in Python codebases.
4
+
5
+ Built from real-world security audits of production RAG frameworks.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pip install ragguard
11
+ ```
12
+
13
+ Or from source:
14
+
15
+ ```bash
16
+ git clone https://github.com/HrushiYadav/ragGuard.git
17
+ cd ragguard
18
+ pip install -e .
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```bash
24
+ # Terminal output (default)
25
+ ragguard scan ./path/to/codebase
26
+
27
+ # Generate reports
28
+ ragguard scan ./path/to/codebase --output report.md --format markdown
29
+ ragguard scan ./path/to/codebase --output report.html --format html
30
+
31
+ # Filter by severity or category
32
+ ragguard scan ./path/to/codebase --severity high
33
+ ragguard scan ./path/to/codebase --category filter-injection
34
+ ```
35
+
36
+ ## What it detects
37
+
38
+ | Scanner | Severity | What it finds |
39
+ |---------|----------|---------------|
40
+ | Filter Injection | HIGH | f-string interpolation in Milvus, Valkey, Azure, Elasticsearch filter expressions |
41
+ | NoSQL Injection | HIGH | Unvalidated dict values in MongoDB/Elasticsearch queries |
42
+ | SQL Injection | HIGH | f-string SQL construction (INSERT, DELETE, SELECT, UPDATE) |
43
+ | Secret Logging | MEDIUM | API keys, passwords, connection strings in logger calls |
44
+ | Auth Gaps | MEDIUM | FastAPI/Flask routes without auth, client-controlled user IDs (IDOR) |
45
+ | Resource Safety | HIGH/MEDIUM/LOW | pickle deserialization, zip bombs, eval/exec, unbounded reads |
46
+
47
+ ## Example output
48
+
49
+ ```
50
+ RAGGuard scanning ./my-rag-app
51
+
52
+ RG-001 [HIGH] Filter injection: Possible filter expression injection
53
+ vector_stores/store.py:42
54
+ > conditions.append(f'(metadata["{key}"] == "{value}")')
55
+
56
+ RG-002 [HIGH] NoSQL injection: Filter value passed into query
57
+ vector_stores/mongo.py:89
58
+ > filter_dict["payload." + key] = value
59
+
60
+ Summary
61
+ +------------------+
62
+ | Severity | Count |
63
+ |----------+-------|
64
+ | HIGH | 5 |
65
+ | MEDIUM | 8 |
66
+ | LOW | 3 |
67
+ | Total | 16 |
68
+ +------------------+
69
+ ```
70
+
71
+ ## HTML Report
72
+
73
+ Generate a styled HTML report for sharing:
74
+
75
+ ```bash
76
+ ragguard scan ./my-rag-app --output report.html --format html
77
+ ```
78
+
79
+ Dark theme with severity badges, code snippets, and remediation guidance.
80
+
81
+ ## Development
82
+
83
+ ```bash
84
+ pip install -e .
85
+ pytest tests/ -v
86
+ ruff check ragguard/
87
+ ```
88
+
89
+ ## License
90
+
91
+ Apache-2.0
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "ragsec"
7
+ version = "0.1.0"
8
+ description = "Static security scanner for RAG pipelines"
9
+ readme = "README.md"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "Hrushikesh Yadav", email = "yadavhrushikesh65@gmail.com" }]
13
+ keywords = ["rag", "security", "scanner", "vector-store", "static-analysis"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Topic :: Security",
18
+ "Topic :: Software Development :: Quality Assurance",
19
+ ]
20
+ dependencies = [
21
+ "click>=8.0",
22
+ "jinja2>=3.0",
23
+ "rich>=13.0",
24
+ ]
25
+
26
+ [project.scripts]
27
+ ragguard = "ragguard.cli:main"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["ragguard"]
31
+
32
+ [tool.ruff]
33
+ line-length = 120
34
+ target-version = "py310"
35
+
36
+ [tool.ruff.lint]
37
+ select = ["E", "F", "I", "W"]
38
+
39
+ [tool.pytest.ini_options]
40
+ testpaths = ["tests"]
@@ -0,0 +1,3 @@
1
+ """RAGGuard -- static security scanner for RAG pipelines."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,86 @@
1
+ import os
2
+
3
+ import click
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+
7
+ from ragguard.engine import run_scan
8
+ from ragguard.report.html import write_html_report
9
+ from ragguard.report.markdown import write_markdown_report
10
+ from ragguard.scanners import ALL_SCANNERS
11
+
12
+ console = Console()
13
+
14
+
15
+ @click.group()
16
+ @click.version_option()
17
+ def main():
18
+ """RAGGuard -- static security scanner for RAG pipelines."""
19
+
20
+
21
+ @main.command()
22
+ @click.argument("target", type=click.Path(exists=True))
23
+ @click.option("--output", "-o", type=click.Path(), help="Output file path (auto-detects format from extension).")
24
+ @click.option("--format", "fmt", type=click.Choice(["markdown", "html", "terminal"]), default="terminal")
25
+ @click.option(
26
+ "--severity", type=click.Choice(["high", "medium", "low"], case_sensitive=False), help="Filter by severity."
27
+ )
28
+ @click.option("--category", help="Filter by category (e.g. filter-injection, nosql-injection).")
29
+ def scan(target: str, output: str | None, fmt: str, severity: str | None, category: str | None):
30
+ """Scan a codebase for RAG security vulnerabilities."""
31
+ target = os.path.abspath(target)
32
+ console.print(f"\n[bold blue]RAGGuard[/] scanning [cyan]{target}[/]\n")
33
+
34
+ scanners = [cls() for cls in ALL_SCANNERS]
35
+ findings = run_scan(target, scanners, severity_filter=severity, category_filter=category)
36
+
37
+ if output and not fmt:
38
+ if output.endswith(".html"):
39
+ fmt = "html"
40
+ elif output.endswith(".md"):
41
+ fmt = "markdown"
42
+
43
+ if fmt == "terminal" and not output:
44
+ _print_terminal(findings, target)
45
+ elif fmt == "html" or (output and output.endswith(".html")):
46
+ path = output or "ragguard-report.html"
47
+ write_html_report(findings, target, path)
48
+ console.print(f"\n[green]HTML report written to {path}[/]")
49
+ elif fmt == "markdown" or (output and output.endswith(".md")):
50
+ path = output or "ragguard-report.md"
51
+ write_markdown_report(findings, target, path)
52
+ console.print(f"\n[green]Markdown report written to {path}[/]")
53
+ else:
54
+ _print_terminal(findings, target)
55
+
56
+ _print_summary(findings)
57
+
58
+
59
+ def _print_terminal(findings: list, target: str):
60
+ if not findings:
61
+ console.print("[green]No findings.[/]")
62
+ return
63
+
64
+ for f in findings:
65
+ sev_color = {"HIGH": "red", "MEDIUM": "yellow", "LOW": "blue"}.get(f.severity, "white")
66
+ console.print(f"\n[bold {sev_color}]{f.id} [{f.severity}][/] {f.title}")
67
+ console.print(f" [dim]{f.file_path}:{f.line_number}[/]")
68
+ console.print(f" {f.description}")
69
+ if f.code_snippet:
70
+ console.print(f" [dim]> {f.code_snippet.strip()[:120]}[/]")
71
+
72
+
73
+ def _print_summary(findings: list):
74
+ high = sum(1 for f in findings if f.severity == "HIGH")
75
+ med = sum(1 for f in findings if f.severity == "MEDIUM")
76
+ low = sum(1 for f in findings if f.severity == "LOW")
77
+
78
+ console.print()
79
+ table = Table(title="Summary", show_header=True)
80
+ table.add_column("Severity", style="bold")
81
+ table.add_column("Count", justify="right")
82
+ table.add_row("[red]HIGH[/]", str(high))
83
+ table.add_row("[yellow]MEDIUM[/]", str(med))
84
+ table.add_row("[blue]LOW[/]", str(low))
85
+ table.add_row("[bold]Total[/]", f"[bold]{len(findings)}[/]")
86
+ console.print(table)
@@ -0,0 +1,51 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from ragguard.finding import Finding
5
+ from ragguard.scanners.base import BaseScanner
6
+
7
+
8
+ def discover_python_files(root: str) -> list[str]:
9
+ files = []
10
+ for dirpath, _, filenames in os.walk(root):
11
+ if any(skip in dirpath for skip in ("__pycache__", ".git", "node_modules", ".venv", "venv")):
12
+ continue
13
+ for f in filenames:
14
+ if f.endswith(".py"):
15
+ files.append(os.path.join(dirpath, f))
16
+ return sorted(files)
17
+
18
+
19
+ def run_scan(
20
+ target: str,
21
+ scanners: list[BaseScanner],
22
+ severity_filter: str | None = None,
23
+ category_filter: str | None = None,
24
+ ) -> list[Finding]:
25
+ root = os.path.abspath(target)
26
+ files = discover_python_files(root)
27
+ findings: list[Finding] = []
28
+ counter = 1
29
+
30
+ for file_path in files:
31
+ try:
32
+ content = Path(file_path).read_text(encoding="utf-8", errors="replace")
33
+ except OSError:
34
+ continue
35
+
36
+ lines = content.splitlines()
37
+ rel_path = os.path.relpath(file_path, root)
38
+
39
+ for scanner in scanners:
40
+ if category_filter and scanner.category != category_filter:
41
+ continue
42
+
43
+ for finding in scanner.scan_file(rel_path, content, lines):
44
+ if severity_filter and finding.severity.lower() != severity_filter.lower():
45
+ continue
46
+ finding.id = f"RG-{counter:03d}"
47
+ counter += 1
48
+ findings.append(finding)
49
+
50
+ findings.sort(key=lambda f: ({"HIGH": 0, "MEDIUM": 1, "LOW": 2}.get(f.severity, 3), f.file_path, f.line_number))
51
+ return findings
@@ -0,0 +1,15 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Finding:
6
+ id: str
7
+ severity: str
8
+ category: str
9
+ title: str
10
+ file_path: str
11
+ line_number: int
12
+ code_snippet: str
13
+ description: str
14
+ remediation: str
15
+ cwe_id: str | None = None
File without changes
@@ -0,0 +1,32 @@
1
+ import html
2
+ from datetime import datetime, timezone
3
+ from pathlib import Path
4
+
5
+ from jinja2 import Template
6
+
7
+ from ragguard.finding import Finding
8
+
9
+ _TEMPLATE_PATH = Path(__file__).parent / "template.html"
10
+
11
+
12
+ def write_html_report(findings: list[Finding], target: str, output_path: str) -> None:
13
+ high = sum(1 for f in findings if f.severity == "HIGH")
14
+ med = sum(1 for f in findings if f.severity == "MEDIUM")
15
+ low = sum(1 for f in findings if f.severity == "LOW")
16
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
17
+
18
+ template_str = _TEMPLATE_PATH.read_text(encoding="utf-8")
19
+ template = Template(template_str)
20
+
21
+ rendered = template.render(
22
+ target=html.escape(target),
23
+ timestamp=timestamp,
24
+ total=len(findings),
25
+ high=high,
26
+ medium=med,
27
+ low=low,
28
+ findings=findings,
29
+ )
30
+
31
+ with open(output_path, "w", encoding="utf-8") as fp:
32
+ fp.write(rendered)
@@ -0,0 +1,61 @@
1
+ from datetime import datetime, timezone
2
+
3
+ from ragguard.finding import Finding
4
+
5
+
6
+ def write_markdown_report(findings: list[Finding], target: str, output_path: str) -> None:
7
+ high = sum(1 for f in findings if f.severity == "HIGH")
8
+ med = sum(1 for f in findings if f.severity == "MEDIUM")
9
+ low = sum(1 for f in findings if f.severity == "LOW")
10
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
11
+
12
+ lines = [
13
+ "# RAGGuard Security Report",
14
+ "",
15
+ f"**Target**: `{target}`",
16
+ f"**Date**: {timestamp}",
17
+ f"**Findings**: {len(findings)} ({high} high, {med} medium, {low} low)",
18
+ "",
19
+ "---",
20
+ "",
21
+ "## Summary",
22
+ "",
23
+ "| Severity | Count |",
24
+ "|----------|-------|",
25
+ f"| HIGH | {high} |",
26
+ f"| MEDIUM | {med} |",
27
+ f"| LOW | {low} |",
28
+ f"| **Total**| **{len(findings)}** |",
29
+ "",
30
+ ]
31
+
32
+ if not findings:
33
+ lines.append("No security findings detected.")
34
+ else:
35
+ lines.append("## Findings")
36
+ lines.append("")
37
+
38
+ for f in findings:
39
+ cwe = f" ({f.cwe_id})" if f.cwe_id else ""
40
+ lines.append(f"### {f.id} [{f.severity}] {f.title}{cwe}")
41
+ lines.append("")
42
+ lines.append(f"**File**: `{f.file_path}:{f.line_number}`")
43
+ lines.append(f"**Category**: {f.category}")
44
+ lines.append("")
45
+ lines.append(f"{f.description}")
46
+ lines.append("")
47
+ if f.code_snippet:
48
+ lines.append("```python")
49
+ lines.append(f"{f.code_snippet}")
50
+ lines.append("```")
51
+ lines.append("")
52
+ lines.append(f"**Remediation**: {f.remediation}")
53
+ lines.append("")
54
+ lines.append("---")
55
+ lines.append("")
56
+
57
+ lines.append("")
58
+ lines.append("*Generated by [RAGGuard](https://github.com/HrushiYadav/ragguard)*")
59
+
60
+ with open(output_path, "w", encoding="utf-8") as fp:
61
+ fp.write("\n".join(lines))
@@ -0,0 +1,164 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>RAGGuard Security Report</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0d1117;
10
+ --card: #161b22;
11
+ --border: #30363d;
12
+ --text: #e6edf3;
13
+ --dim: #8b949e;
14
+ --high: #f85149;
15
+ --medium: #d29922;
16
+ --low: #58a6ff;
17
+ --green: #3fb950;
18
+ }
19
+ * { margin: 0; padding: 0; box-sizing: border-box; }
20
+ body {
21
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
22
+ background: var(--bg);
23
+ color: var(--text);
24
+ line-height: 1.6;
25
+ padding: 2rem;
26
+ max-width: 960px;
27
+ margin: 0 auto;
28
+ }
29
+ h1 { font-size: 1.8rem; margin-bottom: 0.5rem; }
30
+ .meta { color: var(--dim); margin-bottom: 2rem; font-size: 0.9rem; }
31
+ .stats {
32
+ display: flex;
33
+ gap: 1rem;
34
+ margin-bottom: 2rem;
35
+ }
36
+ .stat {
37
+ background: var(--card);
38
+ border: 1px solid var(--border);
39
+ border-radius: 8px;
40
+ padding: 1rem 1.5rem;
41
+ text-align: center;
42
+ flex: 1;
43
+ }
44
+ .stat .number { font-size: 2rem; font-weight: 700; }
45
+ .stat .label { font-size: 0.8rem; color: var(--dim); text-transform: uppercase; letter-spacing: 0.05em; }
46
+ .stat.high .number { color: var(--high); }
47
+ .stat.medium .number { color: var(--medium); }
48
+ .stat.low .number { color: var(--low); }
49
+ .stat.total .number { color: var(--text); }
50
+ .finding {
51
+ background: var(--card);
52
+ border: 1px solid var(--border);
53
+ border-radius: 8px;
54
+ padding: 1.25rem;
55
+ margin-bottom: 1rem;
56
+ border-left: 4px solid var(--border);
57
+ }
58
+ .finding.sev-HIGH { border-left-color: var(--high); }
59
+ .finding.sev-MEDIUM { border-left-color: var(--medium); }
60
+ .finding.sev-LOW { border-left-color: var(--low); }
61
+ .finding-header {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 0.75rem;
65
+ margin-bottom: 0.5rem;
66
+ }
67
+ .badge {
68
+ font-size: 0.7rem;
69
+ font-weight: 700;
70
+ padding: 0.15rem 0.5rem;
71
+ border-radius: 4px;
72
+ text-transform: uppercase;
73
+ letter-spacing: 0.05em;
74
+ }
75
+ .badge.HIGH { background: var(--high); color: #fff; }
76
+ .badge.MEDIUM { background: var(--medium); color: #000; }
77
+ .badge.LOW { background: var(--low); color: #000; }
78
+ .finding-id { color: var(--dim); font-size: 0.85rem; font-weight: 600; }
79
+ .finding-title { font-weight: 600; }
80
+ .finding-location { color: var(--dim); font-size: 0.85rem; margin-bottom: 0.5rem; }
81
+ .finding-desc { margin-bottom: 0.75rem; font-size: 0.9rem; }
82
+ .code {
83
+ background: #0d1117;
84
+ border: 1px solid var(--border);
85
+ border-radius: 4px;
86
+ padding: 0.75rem;
87
+ font-family: 'SF Mono', 'Fira Code', monospace;
88
+ font-size: 0.8rem;
89
+ overflow-x: auto;
90
+ margin-bottom: 0.75rem;
91
+ color: var(--dim);
92
+ }
93
+ .remediation {
94
+ font-size: 0.85rem;
95
+ color: var(--green);
96
+ }
97
+ .remediation::before { content: "Fix: "; font-weight: 600; }
98
+ .footer {
99
+ text-align: center;
100
+ color: var(--dim);
101
+ margin-top: 2rem;
102
+ font-size: 0.8rem;
103
+ padding-top: 1rem;
104
+ border-top: 1px solid var(--border);
105
+ }
106
+ .footer a { color: var(--blue, #58a6ff); text-decoration: none; }
107
+ .no-findings {
108
+ text-align: center;
109
+ padding: 3rem;
110
+ color: var(--green);
111
+ font-size: 1.2rem;
112
+ }
113
+ </style>
114
+ </head>
115
+ <body>
116
+ <h1>RAGGuard Security Report</h1>
117
+ <div class="meta">
118
+ Target: <code>{{ target }}</code> &middot; {{ timestamp }}
119
+ </div>
120
+
121
+ <div class="stats">
122
+ <div class="stat high">
123
+ <div class="number">{{ high }}</div>
124
+ <div class="label">High</div>
125
+ </div>
126
+ <div class="stat medium">
127
+ <div class="number">{{ medium }}</div>
128
+ <div class="label">Medium</div>
129
+ </div>
130
+ <div class="stat low">
131
+ <div class="number">{{ low }}</div>
132
+ <div class="label">Low</div>
133
+ </div>
134
+ <div class="stat total">
135
+ <div class="number">{{ total }}</div>
136
+ <div class="label">Total</div>
137
+ </div>
138
+ </div>
139
+
140
+ {% if not findings %}
141
+ <div class="no-findings">No security findings detected.</div>
142
+ {% endif %}
143
+
144
+ {% for f in findings %}
145
+ <div class="finding sev-{{ f.severity }}">
146
+ <div class="finding-header">
147
+ <span class="finding-id">{{ f.id }}</span>
148
+ <span class="badge {{ f.severity }}">{{ f.severity }}</span>
149
+ <span class="finding-title">{{ f.title }}</span>
150
+ </div>
151
+ <div class="finding-location">{{ f.file_path }}:{{ f.line_number }}{% if f.cwe_id %} &middot; {{ f.cwe_id }}{% endif %}</div>
152
+ <div class="finding-desc">{{ f.description }}</div>
153
+ {% if f.code_snippet %}
154
+ <div class="code">{{ f.code_snippet | e }}</div>
155
+ {% endif %}
156
+ <div class="remediation">{{ f.remediation }}</div>
157
+ </div>
158
+ {% endfor %}
159
+
160
+ <div class="footer">
161
+ Generated by <a href="https://github.com/HrushiYadav/ragguard">RAGGuard</a>
162
+ </div>
163
+ </body>
164
+ </html>