devsecops-radar 0.3.2__tar.gz → 0.3.4__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 (63) hide show
  1. {devsecops_radar-0.3.2/devsecops_radar.egg-info → devsecops_radar-0.3.4}/PKG-INFO +1 -1
  2. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/cli/scanner.py +3 -5
  3. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/core/analyzer.py +2 -4
  4. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/core/models.py +1 -7
  5. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/core/parser.py +2 -4
  6. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/core/reporting.py +1 -7
  7. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/core/rule_fusion.py +1 -23
  8. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/core/sbom.py +1 -2
  9. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/web/app.py +1 -1
  10. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/web/dashboard/routes.py +1 -1
  11. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/web/sentry/routes.py +0 -2
  12. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4/devsecops_radar.egg-info}/PKG-INFO +1 -1
  13. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar.egg-info/SOURCES.txt +3 -0
  14. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/pyproject.toml +1 -1
  15. devsecops_radar-0.3.4/tests/test_analyzer.py +37 -0
  16. devsecops_radar-0.3.4/tests/test_api.py +28 -0
  17. devsecops_radar-0.3.4/tests/test_cli.py +30 -0
  18. devsecops_radar-0.3.4/tests/test_database.py +43 -0
  19. devsecops_radar-0.3.4/tests/test_rule_fusion.py +30 -0
  20. devsecops_radar-0.3.4/tests/test_scanners.py +80 -0
  21. devsecops_radar-0.3.2/tests/test_api.py +0 -16
  22. devsecops_radar-0.3.2/tests/test_cli.py +0 -0
  23. devsecops_radar-0.3.2/tests/test_scanners.py +0 -26
  24. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/LICENSE +0 -0
  25. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/MANIFEST.in +0 -0
  26. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/README.md +0 -0
  27. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/__init__.py +0 -0
  28. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/cli/__init__.py +0 -0
  29. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/core/__init__.py +0 -0
  30. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/core/attack_simulation.py +0 -0
  31. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/core/auth.py +0 -0
  32. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/core/database.py +0 -0
  33. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/core/rag.py +0 -0
  34. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/core/remediation.py +0 -0
  35. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/core/settings.py +0 -0
  36. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/core/valuation.py +0 -0
  37. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/plugins/__init__.py +0 -0
  38. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/scanners/adapter.py +0 -0
  39. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/scanners/base.py +0 -0
  40. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/scanners/gitleaks.py +0 -0
  41. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/scanners/poutine.py +0 -0
  42. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/scanners/semgrep.py +0 -0
  43. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/scanners/trivy.py +0 -0
  44. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/scanners/zizmor.py +0 -0
  45. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/web/__init__.py +0 -0
  46. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/web/attack_paths/__init__.py +0 -0
  47. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/web/attack_paths/routes.py +0 -0
  48. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/web/dashboard/__init__.py +0 -0
  49. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/web/static/css/bootstrap.min.css +0 -0
  50. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/web/static/css/style.css +0 -0
  51. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/web/static/js/bootstrap.bundle.min.js +0 -0
  52. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/web/static/js/chart.umd.min.js +0 -0
  53. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/web/static/js/dashboard.js +0 -0
  54. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/web/summary/__init__.py +0 -0
  55. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/web/summary/routes.py +0 -0
  56. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/web/templates/index.html +0 -0
  57. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/web/topology/__init__.py +0 -0
  58. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar/web/topology/routes.py +0 -0
  59. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar.egg-info/dependency_links.txt +0 -0
  60. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar.egg-info/entry_points.txt +0 -0
  61. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar.egg-info/requires.txt +0 -0
  62. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/devsecops_radar.egg-info/top_level.txt +0 -0
  63. {devsecops_radar-0.3.2 → devsecops_radar-0.3.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devsecops-radar
3
- Version: 0.3.2
3
+ Version: 0.3.4
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
@@ -123,17 +123,16 @@ def wizard():
123
123
  print("🛡️ Welcome to Pipeline Sentinel – Quick Setup Wizard")
124
124
  print("This will install necessary components.\n")
125
125
  import subprocess
126
- # 1. Ollama check
127
126
  try:
128
127
  subprocess.run(['ollama', '--version'], capture_output=True, check=True)
129
128
  print("[✔] Ollama found.")
130
- except:
129
+ except FileNotFoundError:
131
130
  print("[!] Ollama not found. Installing...")
132
131
  subprocess.run('curl -fsSL https://ollama.com/install.sh | sh', shell=True)
133
- # 2. Pull AI model
132
+ except Exception:
133
+ print("[!] Could not verify Ollama. Please install manually.")
134
134
  print("📥 Pulling AI model (llama3.2)...")
135
135
  subprocess.run(['ollama', 'pull', 'llama3.2:latest'])
136
- # 3. Suggestions for optional tools
137
136
  if subprocess.run(['which', 'semgrep'], capture_output=True).returncode == 0:
138
137
  print("[✔] Semgrep available.")
139
138
  else:
@@ -142,7 +141,6 @@ def wizard():
142
141
  print("[✔] Docker available.")
143
142
  else:
144
143
  print("[ ] Docker not found (optional).")
145
- # 4. Final instructions
146
144
  print("\n✅ Setup complete! You can now run:")
147
145
  print(" devsecops-radar --trivy sample_trivy.json --semgrep sample_semgrep.json")
148
146
  print(" devsecops-radar-web")
@@ -4,9 +4,8 @@ import re
4
4
  import requests
5
5
  from requests.adapters import HTTPAdapter
6
6
  from urllib3.util.retry import Retry
7
- from typing import List, Dict, Any, Optional
7
+ from typing import List, Dict, Any
8
8
 
9
- # --- Retry logic for LLM calls ---
10
9
  def _session_with_retries(total=3, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503, 504]):
11
10
  session = requests.Session()
12
11
  retries = Retry(
@@ -20,7 +19,6 @@ def _session_with_retries(total=3, backoff_factor=0.5, status_forcelist=[429, 50
20
19
  session.mount('https://', adapter)
21
20
  return session
22
21
 
23
- # Default maximum findings sent to LLM (configurable via env)
24
22
  MAX_ANALYZER_FINDINGS = int(os.environ.get("ANALYZER_MAX_FINDINGS", "100"))
25
23
 
26
24
  FEW_SHOT_EXAMPLE = {
@@ -60,7 +58,7 @@ def extract_json(text: str) -> Dict[str, Any]:
60
58
  if match:
61
59
  try:
62
60
  return json.loads(match.group(0))
63
- except:
61
+ except json.JSONDecodeError:
64
62
  pass
65
63
  return {"executive_summary": text, "attack_paths": [], "top_remediations": []}
66
64
 
@@ -1,6 +1,6 @@
1
1
  from sqlalchemy import create_engine, Column, Integer, String, DateTime, JSON, ForeignKey
2
2
  from sqlalchemy.orm import declarative_base, sessionmaker, relationship
3
- from pydantic import BaseModel, Field, validator
3
+ from pydantic import BaseModel, validator
4
4
  from typing import List, Optional
5
5
  import datetime
6
6
  import os
@@ -20,11 +20,6 @@ class FindingSchema(BaseModel):
20
20
  def severity_upper(cls, v):
21
21
  return v.upper()
22
22
 
23
- class ScanMetadata(BaseModel):
24
- findings: List[FindingSchema]
25
- scan_id: Optional[int] = None
26
- timestamp: Optional[str] = None
27
-
28
23
  class Scan(Base):
29
24
  __tablename__ = 'scans'
30
25
  id = Column(Integer, primary_key=True)
@@ -52,7 +47,6 @@ def init_db():
52
47
  Base.metadata.create_all(engine)
53
48
 
54
49
  def save_scan_to_db(findings: list):
55
- # Validate with Pydantic before storing
56
50
  validated = [FindingSchema(**f) for f in findings]
57
51
  init_db()
58
52
  session = SessionLocal()
@@ -1,4 +1,6 @@
1
+ import json
1
2
  import warnings
3
+ from typing import List, Dict, Any
2
4
 
3
5
  warnings.warn(
4
6
  "devsecops_radar.core.parser is deprecated and will be removed in v0.3.0. "
@@ -7,10 +9,6 @@ warnings.warn(
7
9
  stacklevel=2,
8
10
  )
9
11
 
10
- import json
11
- from typing import List, Dict, Any
12
-
13
-
14
12
  def parse_trivy_json(file_path: str) -> List[Dict[str, Any]]:
15
13
  with open(file_path) as f:
16
14
  data = json.load(f)
@@ -1,6 +1,6 @@
1
1
  import re
2
2
  import datetime
3
- from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph
3
+ from reportlab.platypus import Table, TableStyle, Paragraph
4
4
  from reportlab.lib.pagesizes import A4
5
5
  from reportlab.lib.styles import getSampleStyleSheet
6
6
  from reportlab.lib import colors
@@ -18,12 +18,6 @@ def redact_sensitive(text: str, patterns: List[str] = None) -> str:
18
18
  return text
19
19
 
20
20
  def generate_pdf_report(findings: List[Dict[str, Any]], ai_summary: Dict[str, Any], output_file: str = "report.pdf", redact: bool = True):
21
- try:
22
- from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph
23
- except ImportError:
24
- print("[ERROR] reportlab not installed.")
25
- return
26
-
27
21
  doc = SimpleDocTemplate(output_file, pagesize=A4)
28
22
  elements = []
29
23
  styles = getSampleStyleSheet()
@@ -2,7 +2,7 @@ import json
2
2
  import os
3
3
  import subprocess
4
4
  from pathlib import Path
5
- from typing import List, Dict, Any, Optional, Tuple
5
+ from typing import List, Dict, Any, Tuple
6
6
 
7
7
 
8
8
  class RuleFusion:
@@ -26,10 +26,7 @@ class RuleFusion:
26
26
  )
27
27
  self.findings: List[Dict[str, Any]] = []
28
28
 
29
- # ── public API ──────────────────────────────────────────────
30
-
31
29
  def load_all_rules(self) -> List[Dict[str, Any]]:
32
- """Load rules from both local and community sources."""
33
30
  if self.local_rules_path and self.local_rules_path.exists():
34
31
  self._load_from_directory(self.local_rules_path)
35
32
 
@@ -40,7 +37,6 @@ class RuleFusion:
40
37
  return self.findings
41
38
 
42
39
  def update_community_rules(self) -> None:
43
- """Clone or pull the latest community rules repository."""
44
40
  target_dir = Path.home() / ".devsecops-radar" / "community-rules"
45
41
  target_dir.parent.mkdir(parents=True, exist_ok=True)
46
42
 
@@ -63,7 +59,6 @@ class RuleFusion:
63
59
  )
64
60
 
65
61
  def generate_template(self, scanner_name: str) -> str:
66
- """Generate a sample rule file for the user to start with."""
67
62
  template = {
68
63
  "findings": [
69
64
  {
@@ -82,18 +77,10 @@ class RuleFusion:
82
77
  }
83
78
  return json.dumps(template, indent=2)
84
79
 
85
- # ── policy engine ─────────────────────────────────────────
86
-
87
80
  @staticmethod
88
81
  def evaluate_policy(
89
82
  findings: List[Dict[str, Any]], policy_file: str
90
83
  ) -> Tuple[bool, str]:
91
- """
92
- Evaluate a policy file against the findings.
93
- Returns (pass, message).
94
- The policy file is a JSON object with conditions.
95
- Example: {"max_critical": 5, "on_violation": "fail"}
96
- """
97
84
  if not os.path.exists(policy_file):
98
85
  return True, (
99
86
  f"Policy file '{policy_file}' not found. "
@@ -120,10 +107,7 @@ class RuleFusion:
120
107
 
121
108
  return True, "Policy checks passed."
122
109
 
123
- # ── internal helpers ────────────────────────────────────────
124
-
125
110
  def _load_from_directory(self, directory: Path) -> None:
126
- """Recursively load all JSON files from a directory."""
127
111
  for json_file in sorted(directory.rglob("*.json")):
128
112
  try:
129
113
  with open(json_file, "r", encoding="utf-8") as f:
@@ -143,7 +127,6 @@ class RuleFusion:
143
127
  print(f"📄 Loaded {len(parsed)} findings from {json_file.name}")
144
128
 
145
129
  def _validate_json(self, data: Any, filename: str) -> bool:
146
- """Structural validation with better list handling."""
147
130
  if isinstance(data, list):
148
131
  if len(data) == 0:
149
132
  print(f"[WARNING] {filename}: empty list, skipping")
@@ -171,10 +154,8 @@ class RuleFusion:
171
154
  def _parse_scanner_output(
172
155
  self, data: Any, filename: str
173
156
  ) -> List[Dict[str, Any]]:
174
- """Parse any known scanner format."""
175
157
  findings: List[Dict[str, Any]] = []
176
158
 
177
- # Already a plain list of findings
178
159
  if isinstance(data, list):
179
160
  for item in data:
180
161
  if isinstance(item, dict) and self._is_finding(item):
@@ -184,7 +165,6 @@ class RuleFusion:
184
165
  if not isinstance(data, dict):
185
166
  return findings
186
167
 
187
- # Trivy format
188
168
  for result in data.get("Results", []):
189
169
  for vuln in result.get("Vulnerabilities", []):
190
170
  findings.append(
@@ -203,7 +183,6 @@ class RuleFusion:
203
183
  }
204
184
  )
205
185
 
206
- # Semgrep format
207
186
  for result in data.get("results", []):
208
187
  findings.append(
209
188
  {
@@ -222,7 +201,6 @@ class RuleFusion:
222
201
  }
223
202
  )
224
203
 
225
- # Poutine / Zizmor / Generic format
226
204
  for item in data.get("findings", []):
227
205
  if isinstance(item, dict):
228
206
  findings.append(self._normalize(item, filename))
@@ -1,7 +1,6 @@
1
1
  import subprocess
2
2
  import json
3
- import os
4
- from typing import List, Dict, Any, Optional
3
+ from typing import List, Dict, Optional
5
4
 
6
5
  def generate_sbom(target_dir: str, output_file: str = "sbom.json") -> Optional[Dict]:
7
6
  try:
@@ -4,7 +4,7 @@ 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
7
+ from devsecops_radar.core.auth import create_token
8
8
  from devsecops_radar.core.settings import settings
9
9
 
10
10
  def create_app():
@@ -39,7 +39,7 @@ DASHBOARD_HTML = r"""
39
39
  <nav class="navbar navbar-dark border-bottom border-secondary mb-4" style="background:#1e293b;">
40
40
  <div class="container-fluid">
41
41
  <span class="navbar-brand mb-0 h1">🛡️ Pipeline Sentinel</span>
42
- <span class="text-muted">v0.3.2</span>
42
+ <span class="text-muted">v0.3.3</span>
43
43
  </div>
44
44
  </nav>
45
45
 
@@ -1,6 +1,4 @@
1
1
  from flask import Blueprint, request, jsonify
2
- import json
3
- import os
4
2
 
5
3
  sentry_bp = Blueprint('sentry', __name__)
6
4
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devsecops-radar
3
- Version: 0.3.2
3
+ Version: 0.3.4
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
@@ -50,6 +50,9 @@ devsecops_radar/web/summary/routes.py
50
50
  devsecops_radar/web/templates/index.html
51
51
  devsecops_radar/web/topology/__init__.py
52
52
  devsecops_radar/web/topology/routes.py
53
+ tests/test_analyzer.py
53
54
  tests/test_api.py
54
55
  tests/test_cli.py
56
+ tests/test_database.py
57
+ tests/test_rule_fusion.py
55
58
  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.3.2"
7
+ version = "0.3.4"
8
8
  description = "Unified CI/CD Security Dashboard — Pipeline Sentinel"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,37 @@
1
+ from unittest.mock import patch, MagicMock
2
+ from devsecops_radar.core.analyzer import OllamaAnalyzer, extract_json, select_findings_for_llm
3
+
4
+ def test_extract_json_plain():
5
+ text = '{"executive_summary": "test", "attack_paths": [], "top_remediations": []}'
6
+ result = extract_json(text)
7
+ assert result["executive_summary"] == "test"
8
+
9
+ def test_extract_json_malformed():
10
+ result = extract_json("some text {invalid")
11
+ assert "executive_summary" in result
12
+
13
+ def test_select_findings_for_llm():
14
+ findings = [{"severity": "CRITICAL"}] * 120 + [{"severity": "LOW"}] * 50
15
+ selected = select_findings_for_llm(findings, max_items=100)
16
+ assert len(selected) == 100
17
+ criticals = [f for f in selected if f["severity"] == "CRITICAL"]
18
+ assert len(criticals) == 100
19
+
20
+ @patch('requests.Session.post')
21
+ def test_ollama_analyzer_success(mock_post):
22
+ mock_response = MagicMock()
23
+ mock_response.json.return_value = {"response": '{"executive_summary": "ok", "attack_paths": [], "top_remediations": []}'}
24
+ mock_response.raise_for_status.return_value = None
25
+ mock_post.return_value = mock_response
26
+ analyzer = OllamaAnalyzer()
27
+ findings = [{"severity": "CRITICAL", "id": "1", "tool": "test"}]
28
+ analysis = analyzer.analyze(findings)
29
+ assert analysis["executive_summary"] == "ok"
30
+
31
+ @patch('requests.Session.post')
32
+ def test_ollama_analyzer_network_error(mock_post):
33
+ mock_post.side_effect = Exception("Network down")
34
+ analyzer = OllamaAnalyzer()
35
+ findings = [{"severity": "CRITICAL", "id": "1", "tool": "test"}]
36
+ analysis = analyzer.analyze(findings)
37
+ assert "AI failed" in analysis["executive_summary"]
@@ -0,0 +1,28 @@
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_page(client):
15
+ resp = client.get('/')
16
+ assert resp.status_code == 200
17
+
18
+ def test_findings_api_requires_key_when_set(monkeypatch, client):
19
+ monkeypatch.setenv("PIPELINE_API_KEY", "secret")
20
+ resp = client.get('/api/findings')
21
+ assert resp.status_code == 401
22
+ resp = client.get('/api/findings', headers={"X-API-Key": "secret"})
23
+ assert resp.status_code == 200
24
+
25
+ def test_findings_api_open_when_disabled(monkeypatch, client):
26
+ monkeypatch.setenv("PIPELINE_API_KEY", "disabled")
27
+ resp = client.get('/api/findings')
28
+ assert resp.status_code == 200
@@ -0,0 +1,30 @@
1
+ import subprocess
2
+ import os
3
+ import tempfile
4
+ import pytest
5
+
6
+ def test_cli_help():
7
+ result = subprocess.run(['devsecops-radar', '--help'], capture_output=True, text=True)
8
+ assert result.returncode == 0
9
+ assert '--trivy' in result.stdout
10
+
11
+ def test_cli_wizard_flag():
12
+ result = subprocess.run(['devsecops-radar', '--wizard'], capture_output=True, text=True)
13
+ assert result.returncode == 0
14
+ assert 'Quick Setup Wizard' in result.stdout or 'Welcome' in result.stdout
15
+
16
+ def test_cli_merge_sample_files():
17
+ sample_dir = os.path.join(os.path.dirname(__file__), '..')
18
+ trivy_sample = os.path.join(sample_dir, 'sample_trivy.json')
19
+ semgrep_sample = os.path.join(sample_dir, 'sample_semgrep.json')
20
+ if not os.path.exists(trivy_sample) or not os.path.exists(semgrep_sample):
21
+ pytest.skip("Sample files not found")
22
+ with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as outfile:
23
+ outpath = outfile.name
24
+ result = subprocess.run(
25
+ ['devsecops-radar', '--trivy', trivy_sample, '--semgrep', semgrep_sample, '--output', outpath],
26
+ capture_output=True, text=True
27
+ )
28
+ assert result.returncode == 0
29
+ assert 'Merged' in result.stdout
30
+ os.unlink(outpath)
@@ -0,0 +1,43 @@
1
+ import os
2
+ import tempfile
3
+ import pytest
4
+
5
+ # Set a temporary database BEFORE importing any devsecops_radar modules
6
+ @pytest.fixture(scope="module")
7
+ def temp_db():
8
+ # Create a unique temp file path for the database
9
+ fd, tmpfile = tempfile.mkstemp(suffix='.db')
10
+ os.close(fd)
11
+ old_val = os.environ.get("DATABASE_URL")
12
+ os.environ["DATABASE_URL"] = f"sqlite:///{tmpfile}"
13
+
14
+ # Now import the database functions — they will use the temp DB
15
+ from devsecops_radar.core.database import save_scan, get_all_scans, get_findings_paginated
16
+
17
+ yield tmpfile, save_scan, get_all_scans, get_findings_paginated
18
+
19
+ # Cleanup
20
+ os.unlink(tmpfile)
21
+ if old_val is None:
22
+ del os.environ["DATABASE_URL"]
23
+ else:
24
+ os.environ["DATABASE_URL"] = old_val
25
+
26
+
27
+ def test_save_and_retrieve(temp_db):
28
+ tmpfile, save_scan, get_all_scans, get_findings_paginated = temp_db
29
+ findings = [
30
+ {
31
+ "tool": "test",
32
+ "severity": "HIGH",
33
+ "id": "1",
34
+ "target": "t",
35
+ "title": "t",
36
+ "description": "d"
37
+ }
38
+ ]
39
+ save_scan(findings)
40
+ scans = get_all_scans()
41
+ assert len(scans) > 0
42
+ paginated = get_findings_paginated(1, 10)
43
+ assert paginated["total"] >= 1
@@ -0,0 +1,30 @@
1
+ import json
2
+ import tempfile
3
+ import os
4
+ from devsecops_radar.core.rule_fusion import RuleFusion
5
+
6
+ class TestPolicyEvaluation:
7
+ def test_policy_pass(self):
8
+ findings = [{"severity": "CRITICAL"}] * 3
9
+ policy = {"max_critical": 5, "on_violation": "fail"}
10
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
11
+ json.dump(policy, f)
12
+ f.flush()
13
+ passed, msg = RuleFusion.evaluate_policy(findings, f.name)
14
+ os.unlink(f.name)
15
+ assert passed
16
+ assert "passed" in msg
17
+
18
+ def test_policy_fail(self):
19
+ findings = [{"severity": "CRITICAL"}] * 6
20
+ policy = {"max_critical": 5, "on_violation": "fail"}
21
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
22
+ json.dump(policy, f)
23
+ f.flush()
24
+ passed, msg = RuleFusion.evaluate_policy(findings, f.name)
25
+ os.unlink(f.name)
26
+ assert not passed
27
+
28
+ def test_policy_file_not_found(self):
29
+ passed, msg = RuleFusion.evaluate_policy([], "/nonexistent/policy.json")
30
+ assert passed
@@ -0,0 +1,80 @@
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
+ from devsecops_radar.scanners.zizmor import ZizmorScanner
8
+ from devsecops_radar.scanners.gitleaks import GitleaksScanner
9
+
10
+ def write_temp_json(data):
11
+ tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
12
+ json.dump(data, tmp)
13
+ tmp.close()
14
+ return tmp.name
15
+
16
+ class TestTrivyScanner:
17
+ def test_valid_json(self):
18
+ data = {"Results": [{"Target": "img", "Vulnerabilities": [{"VulnerabilityID": "CVE-1", "Severity": "HIGH"}]}]}
19
+ path = write_temp_json(data)
20
+ findings = TrivyScanner().parse(path)
21
+ os.unlink(path)
22
+ assert len(findings) == 1
23
+ assert findings[0]["severity"] == "HIGH"
24
+
25
+ def test_invalid_json(self):
26
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
27
+ f.write("not json")
28
+ path = f.name
29
+ findings = TrivyScanner().parse(path)
30
+ os.unlink(path)
31
+ assert findings == []
32
+
33
+ def test_missing_results(self):
34
+ data = {"not_results": []}
35
+ path = write_temp_json(data)
36
+ findings = TrivyScanner().parse(path)
37
+ os.unlink(path)
38
+ assert findings == []
39
+
40
+ class TestSemgrepScanner:
41
+ def test_valid(self):
42
+ data = {"results": [{"path": "a.py", "check_id": "x", "extra": {"severity": "ERROR", "message": "bad"}}]}
43
+ path = write_temp_json(data)
44
+ findings = SemgrepScanner().parse(path)
45
+ os.unlink(path)
46
+ assert len(findings) == 1
47
+ assert findings[0]["severity"] == "ERROR"
48
+
49
+ def test_invalid(self):
50
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
51
+ f.write("{invalid")
52
+ path = f.name
53
+ findings = SemgrepScanner().parse(path)
54
+ os.unlink(path)
55
+ assert findings == []
56
+
57
+ class TestPoutineScanner:
58
+ def test_valid(self):
59
+ data = {"findings": [{"rule_id": "x", "severity": "HIGH", "message": "bad", "location": {"file": "f", "line": 1}}]}
60
+ path = write_temp_json(data)
61
+ findings = PoutineScanner().parse(path)
62
+ os.unlink(path)
63
+ assert findings[0]["severity"] == "HIGH"
64
+
65
+ class TestZizmorScanner:
66
+ def test_valid(self):
67
+ data = {"findings": [{"rule_id": "z1", "severity": "LOW", "message": "m", "path": "p", "location": {"line": 2}}]}
68
+ path = write_temp_json(data)
69
+ findings = ZizmorScanner().parse(path)
70
+ os.unlink(path)
71
+ assert findings[0]["severity"] == "LOW"
72
+
73
+ class TestGitleaksScanner:
74
+ def test_valid_list(self):
75
+ data = [{"file": "f", "ruleID": "r", "description": "d", "line": 1}]
76
+ path = write_temp_json(data)
77
+ findings = GitleaksScanner().parse(path)
78
+ os.unlink(path)
79
+ assert len(findings) == 1
80
+ assert findings[0]["severity"] == "HIGH"
@@ -1,16 +0,0 @@
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
File without changes
@@ -1,26 +0,0 @@
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"
File without changes