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.
- ragsec-0.1.0/.gitignore +11 -0
- ragsec-0.1.0/PKG-INFO +108 -0
- ragsec-0.1.0/README.md +91 -0
- ragsec-0.1.0/pyproject.toml +40 -0
- ragsec-0.1.0/ragguard/__init__.py +3 -0
- ragsec-0.1.0/ragguard/cli.py +86 -0
- ragsec-0.1.0/ragguard/engine.py +51 -0
- ragsec-0.1.0/ragguard/finding.py +15 -0
- ragsec-0.1.0/ragguard/report/__init__.py +0 -0
- ragsec-0.1.0/ragguard/report/html.py +32 -0
- ragsec-0.1.0/ragguard/report/markdown.py +61 -0
- ragsec-0.1.0/ragguard/report/template.html +164 -0
- ragsec-0.1.0/ragguard/scanners/__init__.py +25 -0
- ragsec-0.1.0/ragguard/scanners/auth_gaps.py +77 -0
- ragsec-0.1.0/ragguard/scanners/base.py +16 -0
- ragsec-0.1.0/ragguard/scanners/filter_injection.py +77 -0
- ragsec-0.1.0/ragguard/scanners/nosql_injection.py +68 -0
- ragsec-0.1.0/ragguard/scanners/resource_safety.py +80 -0
- ragsec-0.1.0/ragguard/scanners/secret_logging.py +65 -0
- ragsec-0.1.0/ragguard/scanners/sql_injection.py +80 -0
- ragsec-0.1.0/tests/__init__.py +0 -0
- ragsec-0.1.0/tests/fixtures/safe_filter.py +19 -0
- ragsec-0.1.0/tests/fixtures/safe_nosql.py +13 -0
- ragsec-0.1.0/tests/fixtures/vuln_auth.py +14 -0
- ragsec-0.1.0/tests/fixtures/vuln_filter.py +13 -0
- ragsec-0.1.0/tests/fixtures/vuln_nosql.py +6 -0
- ragsec-0.1.0/tests/fixtures/vuln_resource.py +12 -0
- ragsec-0.1.0/tests/fixtures/vuln_secrets.py +8 -0
- ragsec-0.1.0/tests/fixtures/vuln_sql.py +8 -0
- ragsec-0.1.0/tests/test_scanners.py +89 -0
ragsec-0.1.0/.gitignore
ADDED
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,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
|
|
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> · {{ 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 %} · {{ 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>
|