devsecops-radar 0.2.9__tar.gz → 0.3.2__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.
- {devsecops_radar-0.2.9/devsecops_radar.egg-info → devsecops_radar-0.3.2}/PKG-INFO +4 -1
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/cli/scanner.py +46 -31
- devsecops_radar-0.3.2/devsecops_radar/core/auth.py +28 -0
- devsecops_radar-0.3.2/devsecops_radar/core/settings.py +19 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/scanners/gitleaks.py +12 -4
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/scanners/poutine.py +8 -3
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/scanners/semgrep.py +8 -3
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/scanners/trivy.py +9 -4
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/scanners/zizmor.py +8 -3
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/app.py +14 -3
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/dashboard/routes.py +17 -2
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2/devsecops_radar.egg-info}/PKG-INFO +4 -1
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar.egg-info/SOURCES.txt +3 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar.egg-info/requires.txt +3 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/pyproject.toml +4 -1
- devsecops_radar-0.3.2/tests/test_api.py +16 -0
- devsecops_radar-0.3.2/tests/test_scanners.py +26 -0
- devsecops_radar-0.2.9/tests/test_scanners.py +0 -70
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/LICENSE +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/MANIFEST.in +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/README.md +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/__init__.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/cli/__init__.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/__init__.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/analyzer.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/attack_simulation.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/database.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/models.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/parser.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/rag.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/remediation.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/reporting.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/rule_fusion.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/sbom.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/valuation.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/plugins/__init__.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/scanners/adapter.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/scanners/base.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/__init__.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/attack_paths/__init__.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/attack_paths/routes.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/dashboard/__init__.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/sentry/routes.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/static/css/bootstrap.min.css +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/static/css/style.css +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/static/js/bootstrap.bundle.min.js +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/static/js/chart.umd.min.js +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/static/js/dashboard.js +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/summary/__init__.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/summary/routes.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/templates/index.html +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/topology/__init__.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/topology/routes.py +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar.egg-info/dependency_links.txt +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar.egg-info/entry_points.txt +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar.egg-info/top_level.txt +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/setup.cfg +0 -0
- {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/tests/test_cli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devsecops-radar
|
|
3
|
-
Version: 0.2
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Unified CI/CD Security Dashboard — Pipeline Sentinel
|
|
5
5
|
Author-email: Mehrdoost <70381337+Mehrdoost@users.noreply.github.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -22,6 +22,9 @@ Requires-Dist: reportlab>=4.0
|
|
|
22
22
|
Requires-Dist: litellm>=1.50
|
|
23
23
|
Requires-Dist: sqlalchemy>=2.0
|
|
24
24
|
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Requires-Dist: pyjwt>=2.8
|
|
26
|
+
Requires-Dist: pytest>=8.0
|
|
27
|
+
Requires-Dist: pytest-flask>=1.3
|
|
25
28
|
Dynamic: license-file
|
|
26
29
|
|
|
27
30
|
<!-- markdownlint-disable MD033 MD041 -->
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import argparse
|
|
2
|
+
import asyncio
|
|
2
3
|
import json
|
|
3
4
|
import os
|
|
4
5
|
import sys
|
|
@@ -40,8 +41,20 @@ def parse_args():
|
|
|
40
41
|
parser.add_argument('--wizard', action='store_true', help='Interactive first-time setup wizard')
|
|
41
42
|
return parser.parse_args()
|
|
42
43
|
|
|
43
|
-
def
|
|
44
|
-
|
|
44
|
+
async def run_scanner_async(name, target, adapter):
|
|
45
|
+
try:
|
|
46
|
+
if os.path.isfile(target):
|
|
47
|
+
logger.info(f"Parsing {name} JSON file: {target}")
|
|
48
|
+
validated = await asyncio.to_thread(adapter.parse, target)
|
|
49
|
+
else:
|
|
50
|
+
logger.info(f"Running {name} on: {target}")
|
|
51
|
+
validated = await asyncio.to_thread(adapter.run, target)
|
|
52
|
+
return [v.dict() for v in validated]
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.error(f"{name} failed: {e}")
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
async def run_scans(args, plugins):
|
|
45
58
|
scanner_targets = {
|
|
46
59
|
'trivy': args.trivy,
|
|
47
60
|
'semgrep': args.semgrep,
|
|
@@ -49,21 +62,20 @@ def run_scans(args, plugins):
|
|
|
49
62
|
'zizmor': args.zizmor,
|
|
50
63
|
'gitleaks': getattr(args, 'gitleaks', None),
|
|
51
64
|
}
|
|
65
|
+
tasks = []
|
|
52
66
|
for name, target in scanner_targets.items():
|
|
53
67
|
if target:
|
|
54
|
-
|
|
55
|
-
if
|
|
56
|
-
adapter = ScannerAdapter(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
except Exception as e:
|
|
66
|
-
logger.error(f"{name} failed: {e}")
|
|
68
|
+
plugin = plugins.get(name)
|
|
69
|
+
if plugin:
|
|
70
|
+
adapter = ScannerAdapter(plugin)
|
|
71
|
+
tasks.append(run_scanner_async(name, target, adapter))
|
|
72
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
73
|
+
all_findings = []
|
|
74
|
+
for res in results:
|
|
75
|
+
if isinstance(res, list):
|
|
76
|
+
all_findings.extend(res)
|
|
77
|
+
elif isinstance(res, Exception):
|
|
78
|
+
logger.error(f"Scan task failed with exception: {res}")
|
|
67
79
|
return all_findings
|
|
68
80
|
|
|
69
81
|
def load_custom_rules(args):
|
|
@@ -109,27 +121,32 @@ def run_analysis(args, findings, topology=None):
|
|
|
109
121
|
def wizard():
|
|
110
122
|
"""Interactive setup wizard for first-time users."""
|
|
111
123
|
print("🛡️ Welcome to Pipeline Sentinel – Quick Setup Wizard")
|
|
112
|
-
print("This will
|
|
113
|
-
# 1. Ollama check
|
|
124
|
+
print("This will install necessary components.\n")
|
|
114
125
|
import subprocess
|
|
126
|
+
# 1. Ollama check
|
|
115
127
|
try:
|
|
116
128
|
subprocess.run(['ollama', '--version'], capture_output=True, check=True)
|
|
129
|
+
print("[✔] Ollama found.")
|
|
117
130
|
except:
|
|
118
131
|
print("[!] Ollama not found. Installing...")
|
|
119
|
-
subprocess.run(
|
|
120
|
-
# 2. Pull model
|
|
121
|
-
print("📥 Pulling AI model llama3.2
|
|
132
|
+
subprocess.run('curl -fsSL https://ollama.com/install.sh | sh', shell=True)
|
|
133
|
+
# 2. Pull AI model
|
|
134
|
+
print("📥 Pulling AI model (llama3.2)...")
|
|
122
135
|
subprocess.run(['ollama', 'pull', 'llama3.2:latest'])
|
|
123
|
-
# 3.
|
|
124
|
-
print("🔍 Scanning current directory with Semgrep...")
|
|
136
|
+
# 3. Suggestions for optional tools
|
|
125
137
|
if subprocess.run(['which', 'semgrep'], capture_output=True).returncode == 0:
|
|
126
|
-
|
|
127
|
-
|
|
138
|
+
print("[✔] Semgrep available.")
|
|
139
|
+
else:
|
|
140
|
+
print("[ ] Semgrep not found (optional).")
|
|
128
141
|
if subprocess.run(['which', 'docker'], capture_output=True).returncode == 0:
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
142
|
+
print("[✔] Docker available.")
|
|
143
|
+
else:
|
|
144
|
+
print("[ ] Docker not found (optional).")
|
|
145
|
+
# 4. Final instructions
|
|
146
|
+
print("\n✅ Setup complete! You can now run:")
|
|
147
|
+
print(" devsecops-radar --trivy sample_trivy.json --semgrep sample_semgrep.json")
|
|
148
|
+
print(" devsecops-radar-web")
|
|
149
|
+
print("\nThen open http://localhost:8080 in your browser.")
|
|
133
150
|
|
|
134
151
|
def main():
|
|
135
152
|
args = parse_args()
|
|
@@ -140,8 +157,7 @@ def main():
|
|
|
140
157
|
logger.add(sys.stderr, format="<green>{time:HH:mm:ss}</green> | <level>{level: <8}</level> | {message}")
|
|
141
158
|
|
|
142
159
|
plugins = discover_plugins()
|
|
143
|
-
findings =
|
|
144
|
-
findings.extend(run_scans(args, plugins))
|
|
160
|
+
findings = asyncio.run(run_scans(args, plugins))
|
|
145
161
|
findings.extend(load_custom_rules(args))
|
|
146
162
|
|
|
147
163
|
if not findings:
|
|
@@ -163,7 +179,6 @@ def main():
|
|
|
163
179
|
|
|
164
180
|
if args.fix and ai_summary:
|
|
165
181
|
if args.review:
|
|
166
|
-
# Human-in-the-loop: show diff and ask confirmation
|
|
167
182
|
from devsecops_radar.core.remediation import generate_fix_commands
|
|
168
183
|
cmds = generate_fix_commands(findings, ai_summary)
|
|
169
184
|
print("Proposed fixes:\n", cmds)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import jwt
|
|
2
|
+
import datetime
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from flask import request, jsonify
|
|
5
|
+
from devsecops_radar.core.settings import settings
|
|
6
|
+
|
|
7
|
+
def create_token(user: str = "admin") -> str:
|
|
8
|
+
payload = {
|
|
9
|
+
"user": user,
|
|
10
|
+
"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=settings.JWT_EXPIRATION_HOURS),
|
|
11
|
+
"iat": datetime.datetime.utcnow()
|
|
12
|
+
}
|
|
13
|
+
return jwt.encode(payload, settings.JWT_SECRET, algorithm=settings.JWT_ALGORITHM)
|
|
14
|
+
|
|
15
|
+
def verify_token(token: str) -> dict:
|
|
16
|
+
return jwt.decode(token, settings.JWT_SECRET, algorithms=[settings.JWT_ALGORITHM])
|
|
17
|
+
|
|
18
|
+
def login_required(f):
|
|
19
|
+
@wraps(f)
|
|
20
|
+
def decorated(*args, **kwargs):
|
|
21
|
+
# Only enforce authentication if the admin has configured an API key.
|
|
22
|
+
if settings.PIPELINE_API_KEY != "disabled":
|
|
23
|
+
key = request.headers.get("X-API-Key")
|
|
24
|
+
if key != settings.PIPELINE_API_KEY:
|
|
25
|
+
return jsonify({"error": "API key required"}), 401
|
|
26
|
+
# Without an API key, all requests are permitted (default for local use).
|
|
27
|
+
return f(*args, **kwargs)
|
|
28
|
+
return decorated
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import secrets
|
|
3
|
+
|
|
4
|
+
class Settings:
|
|
5
|
+
JWT_SECRET: str = os.environ.get("JWT_SECRET", "")
|
|
6
|
+
JWT_ALGORITHM: str = "HS256"
|
|
7
|
+
JWT_EXPIRATION_HOURS: int = 24
|
|
8
|
+
PIPELINE_API_KEY: str = os.environ.get("PIPELINE_API_KEY", "disabled")
|
|
9
|
+
DEBUG: bool = os.environ.get("DEBUG", "false").lower() == "true"
|
|
10
|
+
HOST: str = os.environ.get("HOST", "127.0.0.1")
|
|
11
|
+
PORT: int = int(os.environ.get("PORT", "8080"))
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
if not self.JWT_SECRET:
|
|
15
|
+
self.JWT_SECRET = secrets.token_hex(32)
|
|
16
|
+
print("WARNING: JWT_SECRET not set. Using temporary secret:", self.JWT_SECRET)
|
|
17
|
+
print("Set the JWT_SECRET environment variable for production use.")
|
|
18
|
+
|
|
19
|
+
settings = Settings()
|
|
@@ -3,6 +3,7 @@ import subprocess
|
|
|
3
3
|
import tempfile
|
|
4
4
|
import os
|
|
5
5
|
from typing import List, Dict, Any
|
|
6
|
+
from loguru import logger
|
|
6
7
|
from devsecops_radar.plugins import ScannerPlugin
|
|
7
8
|
|
|
8
9
|
class GitleaksScanner(ScannerPlugin):
|
|
@@ -15,17 +16,24 @@ class GitleaksScanner(ScannerPlugin):
|
|
|
15
16
|
with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as tmp:
|
|
16
17
|
outfile = tmp.name
|
|
17
18
|
try:
|
|
18
|
-
subprocess.run(
|
|
19
|
+
subprocess.run(
|
|
20
|
+
['gitleaks', 'detect', '--source', target, '--report-format', 'json', '--report-path', outfile],
|
|
21
|
+
check=True
|
|
22
|
+
)
|
|
19
23
|
return self.parse(outfile)
|
|
20
24
|
finally:
|
|
21
25
|
if os.path.exists(outfile):
|
|
22
26
|
os.unlink(outfile)
|
|
23
27
|
|
|
24
28
|
def parse(self, file_path: str) -> List[Dict[str, Any]]:
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
try:
|
|
30
|
+
with open(file_path) as f:
|
|
31
|
+
data = json.load(f)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
logger.error(f"Could not parse Gitleaks output: {e}")
|
|
34
|
+
return []
|
|
27
35
|
findings = []
|
|
28
|
-
for item in data:
|
|
36
|
+
for item in data if isinstance(data, list) else data.get("Findings", []):
|
|
29
37
|
findings.append({
|
|
30
38
|
"tool": "Gitleaks",
|
|
31
39
|
"target": item.get("file", ""),
|
|
@@ -3,6 +3,7 @@ import subprocess
|
|
|
3
3
|
import tempfile
|
|
4
4
|
import os
|
|
5
5
|
from typing import List, Dict, Any
|
|
6
|
+
from loguru import logger
|
|
6
7
|
from devsecops_radar.plugins import ScannerPlugin
|
|
7
8
|
|
|
8
9
|
class PoutineScanner(ScannerPlugin):
|
|
@@ -25,15 +26,19 @@ class PoutineScanner(ScannerPlugin):
|
|
|
25
26
|
os.unlink(outfile)
|
|
26
27
|
|
|
27
28
|
def parse(self, file_path: str) -> List[Dict[str, Any]]:
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
try:
|
|
30
|
+
with open(file_path) as f:
|
|
31
|
+
data = json.load(f)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
logger.error(f"Could not parse Poutine output: {e}")
|
|
34
|
+
return []
|
|
30
35
|
findings = []
|
|
31
36
|
for result in data.get("findings", []):
|
|
32
37
|
findings.append({
|
|
33
38
|
"tool": "Poutine",
|
|
34
39
|
"target": result.get("location", {}).get("file", ""),
|
|
35
40
|
"id": result.get("rule_id", ""),
|
|
36
|
-
"severity": result.get("severity", "UNKNOWN").upper(),
|
|
41
|
+
"severity": (result.get("severity", "UNKNOWN") or "UNKNOWN").upper(),
|
|
37
42
|
"title": result.get("message", ""),
|
|
38
43
|
"description": result.get("description", ""),
|
|
39
44
|
"line": result.get("location", {}).get("line", 0)
|
|
@@ -3,6 +3,7 @@ import subprocess
|
|
|
3
3
|
import tempfile
|
|
4
4
|
import os
|
|
5
5
|
from typing import List, Dict, Any
|
|
6
|
+
from loguru import logger
|
|
6
7
|
from devsecops_radar.plugins import ScannerPlugin
|
|
7
8
|
|
|
8
9
|
class SemgrepScanner(ScannerPlugin):
|
|
@@ -25,15 +26,19 @@ class SemgrepScanner(ScannerPlugin):
|
|
|
25
26
|
os.unlink(outfile)
|
|
26
27
|
|
|
27
28
|
def parse(self, file_path: str) -> List[Dict[str, Any]]:
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
try:
|
|
30
|
+
with open(file_path) as f:
|
|
31
|
+
data = json.load(f)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
logger.error(f"Could not parse Semgrep output: {e}")
|
|
34
|
+
return []
|
|
30
35
|
findings = []
|
|
31
36
|
for result in data.get("results", []):
|
|
32
37
|
findings.append({
|
|
33
38
|
"tool": "Semgrep",
|
|
34
39
|
"target": result.get("path", ""),
|
|
35
40
|
"id": result.get("check_id", ""),
|
|
36
|
-
"severity": result.get("extra", {}).get("severity", "WARNING").upper(),
|
|
41
|
+
"severity": (result.get("extra", {}).get("severity", "WARNING") or "WARNING").upper(),
|
|
37
42
|
"title": result.get("check_id", ""),
|
|
38
43
|
"description": result.get("extra", {}).get("message", ""),
|
|
39
44
|
"line": result.get("start", {}).get("line", 0)
|
|
@@ -3,6 +3,7 @@ import subprocess
|
|
|
3
3
|
import tempfile
|
|
4
4
|
import os
|
|
5
5
|
from typing import List, Dict, Any
|
|
6
|
+
from loguru import logger
|
|
6
7
|
from devsecops_radar.plugins import ScannerPlugin
|
|
7
8
|
|
|
8
9
|
class TrivyScanner(ScannerPlugin):
|
|
@@ -25,8 +26,12 @@ class TrivyScanner(ScannerPlugin):
|
|
|
25
26
|
os.unlink(outfile)
|
|
26
27
|
|
|
27
28
|
def parse(self, file_path: str) -> List[Dict[str, Any]]:
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
try:
|
|
30
|
+
with open(file_path) as f:
|
|
31
|
+
data = json.load(f)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
logger.error(f"Could not parse Trivy output: {e}")
|
|
34
|
+
return []
|
|
30
35
|
findings = []
|
|
31
36
|
for result in data.get("Results", []):
|
|
32
37
|
target_name = result.get("Target", "Unknown")
|
|
@@ -34,8 +39,8 @@ class TrivyScanner(ScannerPlugin):
|
|
|
34
39
|
findings.append({
|
|
35
40
|
"tool": "Trivy",
|
|
36
41
|
"target": target_name,
|
|
37
|
-
"id": vuln.get("VulnerabilityID"),
|
|
38
|
-
"severity": vuln.get("Severity", "UNKNOWN").upper(),
|
|
42
|
+
"id": vuln.get("VulnerabilityID", ""),
|
|
43
|
+
"severity": (vuln.get("Severity", "UNKNOWN") or "UNKNOWN").upper(),
|
|
39
44
|
"title": vuln.get("Title", ""),
|
|
40
45
|
"description": vuln.get("Description", ""),
|
|
41
46
|
"package": vuln.get("PkgName", ""),
|
|
@@ -3,6 +3,7 @@ import subprocess
|
|
|
3
3
|
import tempfile
|
|
4
4
|
import os
|
|
5
5
|
from typing import List, Dict, Any
|
|
6
|
+
from loguru import logger
|
|
6
7
|
from devsecops_radar.plugins import ScannerPlugin
|
|
7
8
|
|
|
8
9
|
class ZizmorScanner(ScannerPlugin):
|
|
@@ -25,15 +26,19 @@ class ZizmorScanner(ScannerPlugin):
|
|
|
25
26
|
os.unlink(outfile)
|
|
26
27
|
|
|
27
28
|
def parse(self, file_path: str) -> List[Dict[str, Any]]:
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
try:
|
|
30
|
+
with open(file_path) as f:
|
|
31
|
+
data = json.load(f)
|
|
32
|
+
except Exception as e:
|
|
33
|
+
logger.error(f"Could not parse Zizmor output: {e}")
|
|
34
|
+
return []
|
|
30
35
|
findings = []
|
|
31
36
|
for result in data.get("findings", []):
|
|
32
37
|
findings.append({
|
|
33
38
|
"tool": "Zizmor",
|
|
34
39
|
"target": result.get("path", ""),
|
|
35
40
|
"id": result.get("rule_id", ""),
|
|
36
|
-
"severity": result.get("severity", "UNKNOWN").upper(),
|
|
41
|
+
"severity": (result.get("severity", "UNKNOWN") or "UNKNOWN").upper(),
|
|
37
42
|
"title": result.get("message", ""),
|
|
38
43
|
"description": result.get("description", ""),
|
|
39
44
|
"line": result.get("location", {}).get("line", 0)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
from flask import Flask
|
|
1
|
+
from flask import Flask, jsonify, request
|
|
2
2
|
from devsecops_radar.web.dashboard.routes import dashboard_bp
|
|
3
3
|
from devsecops_radar.web.attack_paths.routes import attack_paths_bp
|
|
4
4
|
from devsecops_radar.web.topology.routes import topology_bp
|
|
5
5
|
from devsecops_radar.web.summary.routes import summary_bp
|
|
6
6
|
from devsecops_radar.web.sentry.routes import sentry_bp
|
|
7
|
+
from devsecops_radar.core.auth import login_required, create_token
|
|
8
|
+
from devsecops_radar.core.settings import settings
|
|
7
9
|
|
|
8
10
|
def create_app():
|
|
9
11
|
app = Flask(__name__)
|
|
@@ -12,11 +14,20 @@ def create_app():
|
|
|
12
14
|
app.register_blueprint(topology_bp)
|
|
13
15
|
app.register_blueprint(summary_bp)
|
|
14
16
|
app.register_blueprint(sentry_bp)
|
|
17
|
+
|
|
18
|
+
@app.route('/api/auth/login', methods=['POST'])
|
|
19
|
+
def login():
|
|
20
|
+
data = request.get_json() or {}
|
|
21
|
+
if data.get("password") == settings.PIPELINE_API_KEY:
|
|
22
|
+
token = create_token()
|
|
23
|
+
return jsonify({"token": token})
|
|
24
|
+
return jsonify({"error": "Invalid credentials"}), 403
|
|
25
|
+
|
|
15
26
|
return app
|
|
16
27
|
|
|
17
|
-
def start_server(
|
|
28
|
+
def start_server():
|
|
18
29
|
app = create_app()
|
|
19
|
-
app.run(host=
|
|
30
|
+
app.run(host=settings.HOST, port=settings.PORT, debug=settings.DEBUG)
|
|
20
31
|
|
|
21
32
|
if __name__ == '__main__':
|
|
22
33
|
start_server()
|
|
@@ -3,6 +3,7 @@ import json
|
|
|
3
3
|
import os
|
|
4
4
|
from devsecops_radar.core.database import get_all_scans, get_findings_paginated
|
|
5
5
|
from devsecops_radar.core.rag import rag_search
|
|
6
|
+
from devsecops_radar.core.auth import login_required
|
|
6
7
|
|
|
7
8
|
dashboard_bp = Blueprint('dashboard', __name__)
|
|
8
9
|
|
|
@@ -174,10 +175,16 @@ DASHBOARD_HTML = r"""
|
|
|
174
175
|
<script>
|
|
175
176
|
const API_KEY = "{{ api_key }}";
|
|
176
177
|
function getHeaders() {
|
|
178
|
+
const headers = {};
|
|
177
179
|
if (API_KEY && API_KEY !== 'disabled') {
|
|
178
|
-
|
|
180
|
+
headers['X-API-Key'] = API_KEY;
|
|
179
181
|
}
|
|
180
|
-
|
|
182
|
+
// Optionally add JWT token if stored
|
|
183
|
+
const token = localStorage.getItem('jwt_token');
|
|
184
|
+
if (token) {
|
|
185
|
+
headers['Authorization'] = 'Bearer ' + token;
|
|
186
|
+
}
|
|
187
|
+
return headers;
|
|
181
188
|
}
|
|
182
189
|
|
|
183
190
|
let allFindings = [];
|
|
@@ -226,6 +233,7 @@ DASHBOARD_HTML = r"""
|
|
|
226
233
|
.then(data => {
|
|
227
234
|
allFindings = data.items;
|
|
228
235
|
renderTable(allFindings);
|
|
236
|
+
|
|
229
237
|
const counts = {CRITICAL:0, HIGH:0, MEDIUM:0, LOW:0};
|
|
230
238
|
allFindings.forEach(f => {
|
|
231
239
|
const sev = f.severity.toUpperCase();
|
|
@@ -242,6 +250,7 @@ DASHBOARD_HTML = r"""
|
|
|
242
250
|
},
|
|
243
251
|
options: { plugins: { legend: { labels: { color: 'white' } } } }
|
|
244
252
|
});
|
|
253
|
+
|
|
245
254
|
const pipeline = allFindings.filter(f => f.tool === 'Poutine' || f.tool === 'Zizmor');
|
|
246
255
|
const pCounts = {CRITICAL:0, HIGH:0, MEDIUM:0, LOW:0};
|
|
247
256
|
pipeline.forEach(f => {
|
|
@@ -252,6 +261,7 @@ DASHBOARD_HTML = r"""
|
|
|
252
261
|
document.getElementById('pipeline-high').textContent = pCounts.HIGH;
|
|
253
262
|
document.getElementById('pipeline-medium').textContent = pCounts.MEDIUM;
|
|
254
263
|
document.getElementById('pipeline-low').textContent = pCounts.LOW;
|
|
264
|
+
|
|
255
265
|
document.getElementById('searchInput').addEventListener('input', applyFilters);
|
|
256
266
|
document.getElementById('toolFilter').addEventListener('change', applyFilters);
|
|
257
267
|
document.getElementById('severityFilter').addEventListener('change', applyFilters);
|
|
@@ -290,6 +300,7 @@ DASHBOARD_HTML = r"""
|
|
|
290
300
|
}
|
|
291
301
|
if (!data.nodes || data.nodes.length === 0) return;
|
|
292
302
|
const container = document.getElementById('attack-graph');
|
|
303
|
+
container.innerHTML = '';
|
|
293
304
|
const width = container.clientWidth;
|
|
294
305
|
const height = container.clientHeight;
|
|
295
306
|
const svg = d3.select('#attack-graph')
|
|
@@ -372,6 +383,7 @@ DASHBOARD_HTML = r"""
|
|
|
372
383
|
const nodes = topo.servers.map(s => ({ id: s.name, group: s.ip }));
|
|
373
384
|
const links = topo.connections.map(c => ({ source: c.source, target: c.target, label: c.protocol }));
|
|
374
385
|
const container = document.getElementById('topology-graph');
|
|
386
|
+
container.innerHTML = '';
|
|
375
387
|
const width = container.clientWidth;
|
|
376
388
|
const height = container.clientHeight;
|
|
377
389
|
const svg = d3.select('#topology-graph')
|
|
@@ -435,16 +447,19 @@ def index():
|
|
|
435
447
|
)
|
|
436
448
|
|
|
437
449
|
@dashboard_bp.route('/api/findings')
|
|
450
|
+
@login_required
|
|
438
451
|
def api_findings():
|
|
439
452
|
page = request.args.get('page', 1, type=int)
|
|
440
453
|
per_page = request.args.get('per_page', 50, type=int)
|
|
441
454
|
return jsonify(get_findings_paginated(page, per_page))
|
|
442
455
|
|
|
443
456
|
@dashboard_bp.route('/api/history')
|
|
457
|
+
@login_required
|
|
444
458
|
def api_history():
|
|
445
459
|
return jsonify(get_all_scans())
|
|
446
460
|
|
|
447
461
|
@dashboard_bp.route('/api/rag')
|
|
462
|
+
@login_required
|
|
448
463
|
def api_rag():
|
|
449
464
|
q = request.args.get('q', '')
|
|
450
465
|
if not q:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devsecops-radar
|
|
3
|
-
Version: 0.2
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Unified CI/CD Security Dashboard — Pipeline Sentinel
|
|
5
5
|
Author-email: Mehrdoost <70381337+Mehrdoost@users.noreply.github.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -22,6 +22,9 @@ Requires-Dist: reportlab>=4.0
|
|
|
22
22
|
Requires-Dist: litellm>=1.50
|
|
23
23
|
Requires-Dist: sqlalchemy>=2.0
|
|
24
24
|
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Requires-Dist: pyjwt>=2.8
|
|
26
|
+
Requires-Dist: pytest>=8.0
|
|
27
|
+
Requires-Dist: pytest-flask>=1.3
|
|
25
28
|
Dynamic: license-file
|
|
26
29
|
|
|
27
30
|
<!-- markdownlint-disable MD033 MD041 -->
|
|
@@ -14,6 +14,7 @@ devsecops_radar/cli/scanner.py
|
|
|
14
14
|
devsecops_radar/core/__init__.py
|
|
15
15
|
devsecops_radar/core/analyzer.py
|
|
16
16
|
devsecops_radar/core/attack_simulation.py
|
|
17
|
+
devsecops_radar/core/auth.py
|
|
17
18
|
devsecops_radar/core/database.py
|
|
18
19
|
devsecops_radar/core/models.py
|
|
19
20
|
devsecops_radar/core/parser.py
|
|
@@ -22,6 +23,7 @@ devsecops_radar/core/remediation.py
|
|
|
22
23
|
devsecops_radar/core/reporting.py
|
|
23
24
|
devsecops_radar/core/rule_fusion.py
|
|
24
25
|
devsecops_radar/core/sbom.py
|
|
26
|
+
devsecops_radar/core/settings.py
|
|
25
27
|
devsecops_radar/core/valuation.py
|
|
26
28
|
devsecops_radar/plugins/__init__.py
|
|
27
29
|
devsecops_radar/scanners/adapter.py
|
|
@@ -48,5 +50,6 @@ devsecops_radar/web/summary/routes.py
|
|
|
48
50
|
devsecops_radar/web/templates/index.html
|
|
49
51
|
devsecops_radar/web/topology/__init__.py
|
|
50
52
|
devsecops_radar/web/topology/routes.py
|
|
53
|
+
tests/test_api.py
|
|
51
54
|
tests/test_cli.py
|
|
52
55
|
tests/test_scanners.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "devsecops-radar"
|
|
7
|
-
version = "0.2
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Unified CI/CD Security Dashboard — Pipeline Sentinel"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -28,6 +28,9 @@ dependencies = [
|
|
|
28
28
|
"litellm>=1.50",
|
|
29
29
|
"sqlalchemy>=2.0",
|
|
30
30
|
"pydantic>=2.0",
|
|
31
|
+
"pyjwt>=2.8",
|
|
32
|
+
"pytest>=8.0",
|
|
33
|
+
"pytest-flask>=1.3",
|
|
31
34
|
]
|
|
32
35
|
|
|
33
36
|
[project.urls]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from devsecops_radar.web.app import create_app
|
|
3
|
+
|
|
4
|
+
@pytest.fixture
|
|
5
|
+
def app():
|
|
6
|
+
app = create_app()
|
|
7
|
+
app.config['TESTING'] = True
|
|
8
|
+
return app
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def client(app):
|
|
12
|
+
return app.test_client()
|
|
13
|
+
|
|
14
|
+
def test_dashboard_route(client):
|
|
15
|
+
response = client.get('/')
|
|
16
|
+
assert response.status_code == 200
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import tempfile
|
|
3
|
+
import os
|
|
4
|
+
from devsecops_radar.scanners.trivy import TrivyScanner
|
|
5
|
+
|
|
6
|
+
def test_trivy_parse():
|
|
7
|
+
scanner = TrivyScanner()
|
|
8
|
+
data = {
|
|
9
|
+
"Results": [{
|
|
10
|
+
"Target": "test-image",
|
|
11
|
+
"Vulnerabilities": [{
|
|
12
|
+
"VulnerabilityID": "CVE-2026-0001",
|
|
13
|
+
"Severity": "HIGH",
|
|
14
|
+
"Title": "Test vuln",
|
|
15
|
+
"Description": "Test"
|
|
16
|
+
}]
|
|
17
|
+
}]
|
|
18
|
+
}
|
|
19
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
|
20
|
+
json.dump(data, f)
|
|
21
|
+
f.flush()
|
|
22
|
+
findings = scanner.parse(f.name)
|
|
23
|
+
os.unlink(f.name)
|
|
24
|
+
assert len(findings) == 1
|
|
25
|
+
assert findings[0]["severity"] == "HIGH"
|
|
26
|
+
assert findings[0]["id"] == "CVE-2026-0001"
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import tempfile
|
|
3
|
-
import os
|
|
4
|
-
from devsecops_radar.scanners.trivy import TrivyScanner
|
|
5
|
-
from devsecops_radar.scanners.semgrep import SemgrepScanner
|
|
6
|
-
from devsecops_radar.scanners.poutine import PoutineScanner
|
|
7
|
-
|
|
8
|
-
sample_trivy = {
|
|
9
|
-
"Results": [{
|
|
10
|
-
"Target": "test-app",
|
|
11
|
-
"Vulnerabilities": [{
|
|
12
|
-
"VulnerabilityID": "CVE-TEST-1",
|
|
13
|
-
"Severity": "CRITICAL",
|
|
14
|
-
"Title": "Test vuln",
|
|
15
|
-
"Description": "Test description",
|
|
16
|
-
"PkgName": "test-pkg",
|
|
17
|
-
"InstalledVersion": "1.0",
|
|
18
|
-
"FixedVersion": "2.0"
|
|
19
|
-
}]
|
|
20
|
-
}]
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
sample_semgrep = {
|
|
24
|
-
"results": [{
|
|
25
|
-
"path": "test.py",
|
|
26
|
-
"check_id": "test.rule",
|
|
27
|
-
"extra": {"severity": "HIGH", "message": "Test finding"},
|
|
28
|
-
"start": {"line": 10}
|
|
29
|
-
}]
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
sample_poutine = {
|
|
33
|
-
"findings": [{
|
|
34
|
-
"rule_id": "test-rule",
|
|
35
|
-
"severity": "MEDIUM",
|
|
36
|
-
"message": "Test poutine",
|
|
37
|
-
"description": "Desc",
|
|
38
|
-
"location": {"file": ".gitlab-ci.yml", "line": 1}
|
|
39
|
-
}]
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
def write_temp(data):
|
|
43
|
-
tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
|
|
44
|
-
json.dump(data, tmp)
|
|
45
|
-
tmp.close()
|
|
46
|
-
return tmp.name
|
|
47
|
-
|
|
48
|
-
def test_trivy_parse():
|
|
49
|
-
path = write_temp(sample_trivy)
|
|
50
|
-
findings = TrivyScanner().parse(path)
|
|
51
|
-
os.unlink(path)
|
|
52
|
-
assert len(findings) == 1
|
|
53
|
-
assert findings[0]['tool'] == 'Trivy'
|
|
54
|
-
assert findings[0]['severity'] == 'CRITICAL'
|
|
55
|
-
|
|
56
|
-
def test_semgrep_parse():
|
|
57
|
-
path = write_temp(sample_semgrep)
|
|
58
|
-
findings = SemgrepScanner().parse(path)
|
|
59
|
-
os.unlink(path)
|
|
60
|
-
assert len(findings) == 1
|
|
61
|
-
assert findings[0]['tool'] == 'Semgrep'
|
|
62
|
-
assert findings[0]['severity'] == 'HIGH'
|
|
63
|
-
|
|
64
|
-
def test_poutine_parse():
|
|
65
|
-
path = write_temp(sample_poutine)
|
|
66
|
-
findings = PoutineScanner().parse(path)
|
|
67
|
-
os.unlink(path)
|
|
68
|
-
assert len(findings) == 1
|
|
69
|
-
assert findings[0]['tool'] == 'Poutine'
|
|
70
|
-
assert findings[0]['severity'] == 'MEDIUM'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/attack_paths/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/static/css/bootstrap.min.css
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/static/js/chart.umd.min.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|