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.
Files changed (58) hide show
  1. {devsecops_radar-0.2.9/devsecops_radar.egg-info → devsecops_radar-0.3.2}/PKG-INFO +4 -1
  2. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/cli/scanner.py +46 -31
  3. devsecops_radar-0.3.2/devsecops_radar/core/auth.py +28 -0
  4. devsecops_radar-0.3.2/devsecops_radar/core/settings.py +19 -0
  5. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/scanners/gitleaks.py +12 -4
  6. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/scanners/poutine.py +8 -3
  7. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/scanners/semgrep.py +8 -3
  8. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/scanners/trivy.py +9 -4
  9. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/scanners/zizmor.py +8 -3
  10. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/app.py +14 -3
  11. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/dashboard/routes.py +17 -2
  12. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2/devsecops_radar.egg-info}/PKG-INFO +4 -1
  13. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar.egg-info/SOURCES.txt +3 -0
  14. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar.egg-info/requires.txt +3 -0
  15. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/pyproject.toml +4 -1
  16. devsecops_radar-0.3.2/tests/test_api.py +16 -0
  17. devsecops_radar-0.3.2/tests/test_scanners.py +26 -0
  18. devsecops_radar-0.2.9/tests/test_scanners.py +0 -70
  19. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/LICENSE +0 -0
  20. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/MANIFEST.in +0 -0
  21. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/README.md +0 -0
  22. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/__init__.py +0 -0
  23. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/cli/__init__.py +0 -0
  24. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/__init__.py +0 -0
  25. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/analyzer.py +0 -0
  26. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/attack_simulation.py +0 -0
  27. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/database.py +0 -0
  28. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/models.py +0 -0
  29. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/parser.py +0 -0
  30. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/rag.py +0 -0
  31. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/remediation.py +0 -0
  32. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/reporting.py +0 -0
  33. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/rule_fusion.py +0 -0
  34. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/sbom.py +0 -0
  35. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/core/valuation.py +0 -0
  36. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/plugins/__init__.py +0 -0
  37. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/scanners/adapter.py +0 -0
  38. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/scanners/base.py +0 -0
  39. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/__init__.py +0 -0
  40. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/attack_paths/__init__.py +0 -0
  41. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/attack_paths/routes.py +0 -0
  42. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/dashboard/__init__.py +0 -0
  43. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/sentry/routes.py +0 -0
  44. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/static/css/bootstrap.min.css +0 -0
  45. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/static/css/style.css +0 -0
  46. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/static/js/bootstrap.bundle.min.js +0 -0
  47. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/static/js/chart.umd.min.js +0 -0
  48. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/static/js/dashboard.js +0 -0
  49. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/summary/__init__.py +0 -0
  50. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/summary/routes.py +0 -0
  51. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/templates/index.html +0 -0
  52. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/topology/__init__.py +0 -0
  53. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar/web/topology/routes.py +0 -0
  54. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar.egg-info/dependency_links.txt +0 -0
  55. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar.egg-info/entry_points.txt +0 -0
  56. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/devsecops_radar.egg-info/top_level.txt +0 -0
  57. {devsecops_radar-0.2.9 → devsecops_radar-0.3.2}/setup.cfg +0 -0
  58. {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.9
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 run_scans(args, plugins):
44
- all_findings = []
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
- scanner = plugins.get(name)
55
- if scanner:
56
- adapter = ScannerAdapter(scanner)
57
- try:
58
- if os.path.isfile(target):
59
- logger.info(f"Parsing {name} JSON file: {target}")
60
- validated = adapter.parse(target)
61
- else:
62
- logger.info(f"Running {name} on: {target}")
63
- validated = adapter.run(target)
64
- all_findings.extend([v.dict() for v in validated])
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 guide you through scanning a sample project.\n")
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(['curl', '-fsSL', 'https://ollama.com/install.sh', '|', 'sh'], shell=True)
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. Scan current directory with Semgrep and Trivy if available
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
- subprocess.run(['semgrep', '--config=auto', '--json', '--output', 'semgrep.json', '.'], check=False)
127
- print("🐳 Scanning with Trivy (if Docker installed)...")
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
- subprocess.run(['trivy', 'image', '--format', 'json', '--output', 'trivy.json', 'alpine:latest'], check=False)
130
- # 4. Merge and start dashboard
131
- os.system('devsecops-radar --trivy trivy.json --semgrep semgrep.json')
132
- os.system('devsecops-radar-web')
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(['gitleaks', 'detect', '--source', target, '--report-format', 'json', '--report-path', outfile], check=True)
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
- with open(file_path) as f:
26
- data = json.load(f)
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
- with open(file_path) as f:
29
- data = json.load(f)
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
- with open(file_path) as f:
29
- data = json.load(f)
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
- with open(file_path) as f:
29
- data = json.load(f)
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
- with open(file_path) as f:
29
- data = json.load(f)
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(host='0.0.0.0', port=8080):
28
+ def start_server():
18
29
  app = create_app()
19
- app.run(host=host, port=port, debug=True)
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
- return { 'X-API-Key': API_KEY };
180
+ headers['X-API-Key'] = API_KEY;
179
181
  }
180
- return {};
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.9
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
@@ -7,3 +7,6 @@ reportlab>=4.0
7
7
  litellm>=1.50
8
8
  sqlalchemy>=2.0
9
9
  pydantic>=2.0
10
+ pyjwt>=2.8
11
+ pytest>=8.0
12
+ pytest-flask>=1.3
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devsecops-radar"
7
- version = "0.2.9"
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