devsecops-radar 0.3.10__tar.gz → 0.4.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.
Files changed (60) hide show
  1. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/PKG-INFO +6 -11
  2. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/README.md +5 -10
  3. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/cli/scanner.py +16 -3
  4. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/analyzer.py +44 -17
  5. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/attack_simulation.py +3 -2
  6. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/auth.py +7 -4
  7. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/database.py +10 -10
  8. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/models.py +7 -7
  9. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/parser.py +5 -5
  10. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/rag.py +6 -4
  11. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/remediation.py +21 -10
  12. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/reporting.py +34 -19
  13. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/rule_fusion.py +14 -14
  14. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/sbom.py +6 -6
  15. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/settings.py +2 -1
  16. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/valuation.py +6 -5
  17. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/plugins/__init__.py +5 -4
  18. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/scanners/adapter.py +6 -5
  19. devsecops_radar-0.4.0/devsecops_radar/scanners/base.py +12 -0
  20. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/scanners/gitleaks.py +8 -5
  21. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/scanners/poutine.py +8 -5
  22. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/scanners/semgrep.py +8 -5
  23. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/scanners/trivy.py +8 -5
  24. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/scanners/zizmor.py +8 -5
  25. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/app.py +8 -6
  26. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/attack_paths/routes.py +3 -2
  27. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/dashboard/routes.py +115 -54
  28. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/sentry/routes.py +2 -2
  29. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/summary/routes.py +3 -2
  30. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/topology/routes.py +3 -2
  31. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar.egg-info/PKG-INFO +6 -11
  32. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/pyproject.toml +6 -1
  33. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/tests/test_analyzer.py +20 -4
  34. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/tests/test_api.py +5 -2
  35. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/tests/test_cli.py +4 -2
  36. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/tests/test_database.py +5 -7
  37. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/tests/test_rule_fusion.py +4 -2
  38. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/tests/test_scanners.py +52 -9
  39. devsecops_radar-0.3.10/devsecops_radar/scanners/base.py +0 -11
  40. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/LICENSE +0 -0
  41. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/__init__.py +0 -0
  42. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/cli/__init__.py +0 -0
  43. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/__init__.py +0 -0
  44. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/__init__.py +0 -0
  45. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/attack_paths/__init__.py +0 -0
  46. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/dashboard/__init__.py +0 -0
  47. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/static/css/bootstrap.min.css +0 -0
  48. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/static/css/style.css +0 -0
  49. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/static/js/bootstrap.bundle.min.js +0 -0
  50. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/static/js/chart.umd.min.js +0 -0
  51. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/static/js/dashboard.js +0 -0
  52. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/summary/__init__.py +0 -0
  53. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/templates/index.html +0 -0
  54. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/topology/__init__.py +0 -0
  55. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar.egg-info/SOURCES.txt +0 -0
  56. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar.egg-info/dependency_links.txt +0 -0
  57. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar.egg-info/entry_points.txt +0 -0
  58. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar.egg-info/requires.txt +0 -0
  59. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar.egg-info/top_level.txt +0 -0
  60. {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devsecops-radar
3
- Version: 0.3.10
3
+ Version: 0.4.0
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
@@ -32,11 +32,6 @@ Requires-Dist: mypy>=1.9; extra == "dev"
32
32
  Requires-Dist: pre-commit>=3.5; extra == "dev"
33
33
  Dynamic: license-file
34
34
 
35
- Here is the fully fixed, standardized, and perfectly formatted English version of your comprehensive `README.md` file. I have corrected the Markdown syntax errors, repaired the broken code blocks, properly aligned the tables, and integrated the new features (such as OPA Rego policies, What-If simulation, Codecov, and VEX support).
36
-
37
- You can copy the entire block below using the **Copy** button and paste it directly into your file:
38
-
39
- ```markdown
40
35
  <div align="center">
41
36
 
42
37
  # 🛡️ Pipeline Sentinel
@@ -129,8 +124,8 @@ Pipeline Sentinel is designed to be **flexible** — you decide where it fits be
129
124
  [Gitleaks scan] ┘
130
125
  ```
131
126
 
132
- > **📌 Diagram Placeholder:** Add your network flow diagram here as `docs/network_flow.png`.
133
- > `![Network Flow Diagram](docs/network_flow.png)`
127
+ > **📌 Diagram Placeholder:**
128
+ ![Network Flow Diagram](docs/network_flow.png)
134
129
 
135
130
  ---
136
131
 
@@ -490,8 +485,8 @@ devsecops_radar/
490
485
  └── sentry/ # Live webhook agent for CI/CD
491
486
  ```
492
487
 
493
- > **📌 Diagram Placeholder:** Add your architecture diagram here as `docs/architecture.png`.
494
- > `![Architecture Diagram](docs/architecture.png)`
488
+ > **📌 Diagram Placeholder:**
489
+ ![Network Flow Diagram](docs/network_flow.png)
495
490
 
496
491
  ---
497
492
 
@@ -573,7 +568,7 @@ This project adheres to the Contributor Covenant Code of Conduct. By participati
573
568
 
574
569
  [![GitHub](https://img.shields.io/badge/GitHub-ReverseForge-181717?logo=github)](https://github.com/ReverseForge)
575
570
  [![GitHub](https://img.shields.io/badge/GitHub-Mehrdoost-181717?logo=github)](https://github.com/Mehrdoost)
576
- [![GitHub](https://img.shields.io/badge/GitHub-miora-sora?logo=github)](https://github.com/miora-sora)
571
+ [![GitHub](https://img.shields.io/badge/GitHub-miora-soraزمس?logo=github)](https://github.com/miora-sora)
577
572
 
578
573
  ---
579
574
 
@@ -1,8 +1,3 @@
1
- Here is the fully fixed, standardized, and perfectly formatted English version of your comprehensive `README.md` file. I have corrected the Markdown syntax errors, repaired the broken code blocks, properly aligned the tables, and integrated the new features (such as OPA Rego policies, What-If simulation, Codecov, and VEX support).
2
-
3
- You can copy the entire block below using the **Copy** button and paste it directly into your file:
4
-
5
- ```markdown
6
1
  <div align="center">
7
2
 
8
3
  # 🛡️ Pipeline Sentinel
@@ -95,8 +90,8 @@ Pipeline Sentinel is designed to be **flexible** — you decide where it fits be
95
90
  [Gitleaks scan] ┘
96
91
  ```
97
92
 
98
- > **📌 Diagram Placeholder:** Add your network flow diagram here as `docs/network_flow.png`.
99
- > `![Network Flow Diagram](docs/network_flow.png)`
93
+ > **📌 Diagram Placeholder:**
94
+ ![Network Flow Diagram](docs/network_flow.png)
100
95
 
101
96
  ---
102
97
 
@@ -456,8 +451,8 @@ devsecops_radar/
456
451
  └── sentry/ # Live webhook agent for CI/CD
457
452
  ```
458
453
 
459
- > **📌 Diagram Placeholder:** Add your architecture diagram here as `docs/architecture.png`.
460
- > `![Architecture Diagram](docs/architecture.png)`
454
+ > **📌 Diagram Placeholder:**
455
+ ![Network Flow Diagram](docs/network_flow.png)
461
456
 
462
457
  ---
463
458
 
@@ -539,7 +534,7 @@ This project adheres to the Contributor Covenant Code of Conduct. By participati
539
534
 
540
535
  [![GitHub](https://img.shields.io/badge/GitHub-ReverseForge-181717?logo=github)](https://github.com/ReverseForge)
541
536
  [![GitHub](https://img.shields.io/badge/GitHub-Mehrdoost-181717?logo=github)](https://github.com/Mehrdoost)
542
- [![GitHub](https://img.shields.io/badge/GitHub-miora-sora?logo=github)](https://github.com/miora-sora)
537
+ [![GitHub](https://img.shields.io/badge/GitHub-miora-soraزمس?logo=github)](https://github.com/miora-sora)
543
538
 
544
539
  ---
545
540
 
@@ -4,14 +4,17 @@ import json
4
4
  import os
5
5
  import sys
6
6
  from importlib.metadata import entry_points
7
+
7
8
  from loguru import logger
8
- from devsecops_radar.scanners.adapter import ScannerAdapter
9
+
9
10
  from devsecops_radar.core.analyzer import get_analyzer
10
11
  from devsecops_radar.core.database import save_scan
11
- from devsecops_radar.core.rule_fusion import RuleFusion
12
12
  from devsecops_radar.core.remediation import auto_fix, generate_pr
13
13
  from devsecops_radar.core.reporting import generate_pdf_report
14
+ from devsecops_radar.core.rule_fusion import RuleFusion
14
15
  from devsecops_radar.core.valuation import compute_dynamic_risk_score
16
+ from devsecops_radar.scanners.adapter import ScannerAdapter
17
+
15
18
 
16
19
  def discover_plugins():
17
20
  plugins = {}
@@ -20,6 +23,7 @@ def discover_plugins():
20
23
  plugins[cls.name] = cls()
21
24
  return plugins
22
25
 
26
+
23
27
  def parse_args():
24
28
  parser = argparse.ArgumentParser(description='Pipeline Sentinel - Unified CI/CD Security Dashboard')
25
29
  parser.add_argument('--trivy', type=str)
@@ -42,6 +46,7 @@ def parse_args():
42
46
  parser.add_argument('--wizard', action='store_true', help='Interactive first-time setup wizard')
43
47
  return parser.parse_args()
44
48
 
49
+
45
50
  async def run_scanner_async(name, target, adapter):
46
51
  try:
47
52
  if os.path.isfile(target):
@@ -55,6 +60,7 @@ async def run_scanner_async(name, target, adapter):
55
60
  logger.error(f"{name} failed: {e}")
56
61
  return []
57
62
 
63
+
58
64
  async def run_scans(args, plugins):
59
65
  scanner_targets = {
60
66
  'trivy': args.trivy,
@@ -79,6 +85,7 @@ async def run_scans(args, plugins):
79
85
  logger.error(f"Scan task failed with exception: {res}")
80
86
  return all_findings
81
87
 
88
+
82
89
  def load_custom_rules(args):
83
90
  if args.rules:
84
91
  try:
@@ -90,6 +97,7 @@ def load_custom_rules(args):
90
97
  logger.error(f"Failed to load rules: {e}")
91
98
  return []
92
99
 
100
+
93
101
  def run_policy_check(args, findings):
94
102
  if args.policy:
95
103
  passed, msg = RuleFusion.evaluate_policy(findings, args.policy)
@@ -104,6 +112,7 @@ def run_policy_check(args, findings):
104
112
  logger.error("Rego policy check failed.")
105
113
  sys.exit(1)
106
114
 
115
+
107
116
  def save_results(args, findings):
108
117
  with open(args.output, 'w') as f:
109
118
  json.dump(findings, f, indent=2)
@@ -113,6 +122,7 @@ def save_results(args, findings):
113
122
  except Exception as e:
114
123
  logger.warning(f"Could not save scan history: {e}")
115
124
 
125
+
116
126
  def run_analysis(args, findings, topology=None):
117
127
  if not args.analyze:
118
128
  return {}
@@ -125,6 +135,7 @@ def run_analysis(args, findings, topology=None):
125
135
  logger.success(f"AI summary saved to {summary_file}")
126
136
  return analysis
127
137
 
138
+
128
139
  def wizard():
129
140
  print("🛡️ Welcome to Pipeline Sentinel – Quick Setup Wizard")
130
141
  print("This will install necessary components.\n")
@@ -152,6 +163,7 @@ def wizard():
152
163
  print(" devsecops-radar-web")
153
164
  print("\nThen open http://localhost:8080 in your browser.")
154
165
 
166
+
155
167
  def main():
156
168
  args = parse_args()
157
169
  if args.wizard:
@@ -198,5 +210,6 @@ def main():
198
210
  if args.report:
199
211
  generate_pdf_report(findings, ai_summary, args.report)
200
212
 
213
+
201
214
  if __name__ == '__main__':
202
- main()
215
+ main()
@@ -1,12 +1,16 @@
1
1
  import json
2
2
  import os
3
3
  import re
4
+ from typing import Any
5
+
4
6
  import requests
5
7
  from requests.adapters import HTTPAdapter
6
8
  from urllib3.util.retry import Retry
7
- from typing import List, Dict, Any
8
9
 
9
- def _session_with_retries(total=3, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503, 504]):
10
+
11
+ def _session_with_retries(total=3, backoff_factor=0.5, status_forcelist=None):
12
+ if status_forcelist is None:
13
+ status_forcelist = [429, 500, 502, 503, 504]
10
14
  session = requests.Session()
11
15
  retries = Retry(
12
16
  total=total,
@@ -19,15 +23,24 @@ def _session_with_retries(total=3, backoff_factor=0.5, status_forcelist=[429, 50
19
23
  session.mount('https://', adapter)
20
24
  return session
21
25
 
26
+
22
27
  MAX_ANALYZER_FINDINGS = int(os.environ.get("ANALYZER_MAX_FINDINGS", "100"))
23
28
 
24
29
  FEW_SHOT_EXAMPLE = {
25
- "executive_summary": "A leaked CI/CD credential combined with an unpatched container image creates a critical supply chain attack path. Immediate action is required.",
30
+ "executive_summary": (
31
+ "A leaked CI/CD credential combined with an unpatched container image "
32
+ "creates a critical supply chain attack path. Immediate action is required."
33
+ ),
26
34
  "risk_score": 92,
27
35
  "attack_paths": [
28
36
  {
29
37
  "name": "Supply Chain Compromise via Credential Leak",
30
- "description": "An exposed GitHub Actions secret (ID: SECRET-001) allows an attacker to push malicious images to the container registry. Combined with a known RCE vulnerability in the web server (CVE-2026-1234), this chain grants full control over the production environment.",
38
+ "description": (
39
+ "An exposed GitHub Actions secret (ID: SECRET-001) allows an attacker "
40
+ "to push malicious images to the container registry. Combined with a known "
41
+ "RCE vulnerability in the web server (CVE-2026-1234), this chain grants "
42
+ "full control over the production environment."
43
+ ),
31
44
  "involved_findings": ["SECRET-001", "CVE-2026-1234"],
32
45
  "mitre_tactics": ["TA0001", "TA0042"],
33
46
  "mitre_techniques": ["T1078", "T1578"],
@@ -39,18 +52,28 @@ FEW_SHOT_EXAMPLE = {
39
52
  {
40
53
  "priority": 1,
41
54
  "finding_id": "SECRET-001",
42
- "action": "Rotate the exposed secret and remove it from the workflow log. Use GitHub's masked variables.",
43
- "fix_diff": "--- a/.github/workflows/deploy.yml\n+++ b/.github/workflows/deploy.yml\n- run: echo ${{ secrets.DEPLOY_KEY }}\n+ run: echo '**redacted**'"
55
+ "action": (
56
+ "Rotate the exposed secret and remove it from the workflow log. "
57
+ "Use GitHub's masked variables."
58
+ ),
59
+ "fix_diff": (
60
+ "--- a/.github/workflows/deploy.yml\n"
61
+ "+++ b/.github/workflows/deploy.yml\n"
62
+ "- run: echo ${{ secrets.DEPLOY_KEY }}\n"
63
+ "+ run: echo '**redacted**'"
64
+ )
44
65
  }
45
66
  ],
46
67
  "false_positives_likely": []
47
68
  }
48
69
 
70
+
49
71
  class BaseAnalyzer:
50
- def analyze(self, findings: List[Dict[str, Any]], topology: Dict[str, Any] = None) -> Dict[str, Any]:
72
+ def analyze(self, findings: list[dict[str, Any]], topology: dict[str, Any] | None = None) -> dict[str, Any]:
51
73
  raise NotImplementedError
52
74
 
53
- def extract_json(text: str) -> Dict[str, Any]:
75
+
76
+ def extract_json(text: str) -> dict[str, Any]:
54
77
  try:
55
78
  return json.loads(text)
56
79
  except json.JSONDecodeError:
@@ -62,7 +85,8 @@ def extract_json(text: str) -> Dict[str, Any]:
62
85
  pass
63
86
  return {"executive_summary": text, "attack_paths": [], "top_remediations": []}
64
87
 
65
- def select_findings_for_llm(findings: List[Dict], max_items: int = MAX_ANALYZER_FINDINGS) -> List[Dict]:
88
+
89
+ def select_findings_for_llm(findings: list[dict], max_items: int = MAX_ANALYZER_FINDINGS) -> list[dict]:
66
90
  if len(findings) <= max_items:
67
91
  return findings
68
92
  critical_high = [f for f in findings if f.get('severity') in ('CRITICAL', 'HIGH')]
@@ -73,13 +97,14 @@ def select_findings_for_llm(findings: List[Dict], max_items: int = MAX_ANALYZER_
73
97
  selected.extend(others[:remaining])
74
98
  return selected
75
99
 
100
+
76
101
  class OllamaAnalyzer(BaseAnalyzer):
77
- def __init__(self, model: str = None, endpoint: str = None):
102
+ def __init__(self, model: str | None = None, endpoint: str | None = None):
78
103
  self.model = model or os.environ.get("PIPELINE_LLM_MODEL", "llama3.2:latest")
79
104
  self.endpoint = endpoint or os.environ.get("OPENAI_API_BASE", "http://localhost:11434/api/generate")
80
105
  self.session = _session_with_retries()
81
106
 
82
- def analyze(self, findings: List[Dict[str, Any]], topology: Dict[str, Any] = None) -> Dict[str, Any]:
107
+ def analyze(self, findings: list[dict[str, Any]], topology: dict[str, Any] | None = None) -> dict[str, Any]:
83
108
  if not findings:
84
109
  return {"executive_summary": "No findings.", "attack_paths": [], "top_remediations": []}
85
110
 
@@ -112,16 +137,17 @@ Respond ONLY with valid JSON in the same format as the example."""
112
137
  except Exception as e:
113
138
  return {"executive_summary": f"AI failed: {str(e)}", "attack_paths": [], "top_remediations": []}
114
139
 
140
+
115
141
  class LiteLLMAnalyzer(BaseAnalyzer):
116
- def __init__(self, model: str = None):
142
+ def __init__(self, model: str | None = None):
117
143
  try:
118
144
  import litellm
119
145
  self.litellm = litellm
120
- except ImportError:
121
- raise ImportError("Install litellm: pip install litellm")
146
+ except ImportError as err:
147
+ raise ImportError("Install litellm: pip install litellm") from err
122
148
  self.model = model or os.environ.get("PIPELINE_LLM_MODEL", "gpt-4o-mini")
123
149
 
124
- def analyze(self, findings: List[Dict[str, Any]], topology: Dict[str, Any] = None) -> Dict[str, Any]:
150
+ def analyze(self, findings: list[dict[str, Any]], topology: dict[str, Any] | None = None) -> dict[str, Any]:
125
151
  if not findings:
126
152
  return {"executive_summary": "No findings.", "attack_paths": [], "top_remediations": []}
127
153
 
@@ -150,7 +176,8 @@ Respond ONLY with JSON like the example."""
150
176
  except Exception as e:
151
177
  return {"executive_summary": f"AI failed: {str(e)}", "attack_paths": [], "top_remediations": []}
152
178
 
153
- def get_analyzer(backend: str = "ollama", model: str = None) -> BaseAnalyzer:
179
+
180
+ def get_analyzer(backend: str = "ollama", model: str | None = None) -> BaseAnalyzer:
154
181
  if backend == "litellm":
155
182
  return LiteLLMAnalyzer(model=model)
156
- return OllamaAnalyzer(model=model)
183
+ return OllamaAnalyzer(model=model)
@@ -1,6 +1,7 @@
1
+ import os
1
2
  import subprocess
2
3
  import tempfile
3
- import os
4
+
4
5
 
5
6
  def simulate_attack(finding: dict) -> str:
6
7
  script = f"#!/bin/bash\n# PoC for {finding.get('id')}\necho 'Simulating {finding.get('title')}'"
@@ -19,4 +20,4 @@ def run_sandboxed_poc(script_path: str) -> str:
19
20
  )
20
21
  return result.stdout
21
22
  except Exception as e:
22
- return f"Sandbox execution failed: {e}"
23
+ return f"Sandbox execution failed: {e}"
@@ -1,10 +1,13 @@
1
- import os
2
- import jwt
3
1
  import datetime
2
+ import os
4
3
  from functools import wraps
5
- from flask import request, jsonify
4
+
5
+ import jwt
6
+ from flask import jsonify, request
7
+
6
8
  from devsecops_radar.core.settings import settings
7
9
 
10
+
8
11
  def create_token(user: str = "admin") -> str:
9
12
  payload = {
10
13
  "user": user,
@@ -26,4 +29,4 @@ def login_required(f):
26
29
  if key != api_key:
27
30
  return jsonify({"error": "API key required"}), 401
28
31
  return f(*args, **kwargs)
29
- return decorated
32
+ return decorated
@@ -1,8 +1,8 @@
1
1
  from contextlib import contextmanager
2
- from devsecops_radar.core.models import (
3
- init_db, SessionLocal, Scan, Finding
4
- )
5
- from typing import List, Dict, Any, Optional
2
+ from typing import Any
3
+
4
+ from devsecops_radar.core.models import Finding, Scan, SessionLocal, init_db
5
+
6
6
 
7
7
  @contextmanager
8
8
  def get_session():
@@ -16,11 +16,11 @@ def get_session():
16
16
  finally:
17
17
  session.close()
18
18
 
19
- def save_scan(findings: List[Dict[str, Any]]):
19
+ def save_scan(findings: list[dict[str, Any]]):
20
20
  from devsecops_radar.core.models import save_scan_to_db
21
21
  save_scan_to_db(findings)
22
22
 
23
- def get_all_scans() -> List[Dict[str, Any]]:
23
+ def get_all_scans() -> list[dict[str, Any]]:
24
24
  init_db()
25
25
  with get_session() as session:
26
26
  scans = []
@@ -41,7 +41,7 @@ def get_all_scans() -> List[Dict[str, Any]]:
41
41
  })
42
42
  return scans
43
43
 
44
- def get_scan_by_id(scan_id: int) -> Optional[Dict[str, Any]]:
44
+ def get_scan_by_id(scan_id: int) -> dict[str, Any] | None:
45
45
  with get_session() as session:
46
46
  scan = session.query(Scan).filter(Scan.id == scan_id).first()
47
47
  if not scan:
@@ -65,7 +65,7 @@ def get_scan_by_id(scan_id: int) -> Optional[Dict[str, Any]]:
65
65
  "total": len(findings_list)
66
66
  }
67
67
 
68
- def compare_scans(scan_id_1: int, scan_id_2: int) -> Dict[str, Any]:
68
+ def compare_scans(scan_id_1: int, scan_id_2: int) -> dict[str, Any]:
69
69
  scan1 = get_scan_by_id(scan_id_1)
70
70
  scan2 = get_scan_by_id(scan_id_2)
71
71
  if not scan1 or not scan2:
@@ -84,7 +84,7 @@ def compare_scans(scan_id_1: int, scan_id_2: int) -> Dict[str, Any]:
84
84
  "removed_findings": removed,
85
85
  }
86
86
 
87
- def get_findings_paginated(page: int = 1, per_page: int = 50) -> Dict[str, Any]:
87
+ def get_findings_paginated(page: int = 1, per_page: int = 50) -> dict[str, Any]:
88
88
  with get_session() as session:
89
89
  total = session.query(Finding).count()
90
90
  findings = session.query(Finding).order_by(Finding.id.desc()).offset(
@@ -101,4 +101,4 @@ def get_findings_paginated(page: int = 1, per_page: int = 50) -> Dict[str, Any]:
101
101
  "description": f.description,
102
102
  "line": f.line
103
103
  })
104
- return {"items": items, "total": total, "page": page, "per_page": per_page}
104
+ return {"items": items, "total": total, "page": page, "per_page": per_page}
@@ -1,10 +1,10 @@
1
- from sqlalchemy import create_engine, Column, Integer, String, DateTime, JSON, ForeignKey
2
- from sqlalchemy.orm import declarative_base, sessionmaker, relationship
3
- from pydantic import BaseModel, field_validator
4
- from typing import Optional
5
1
  import datetime
6
2
  import os
7
3
 
4
+ from pydantic import BaseModel, field_validator
5
+ from sqlalchemy import JSON, Column, DateTime, ForeignKey, Integer, String, create_engine
6
+ from sqlalchemy.orm import declarative_base, relationship, sessionmaker
7
+
8
8
  Base = declarative_base()
9
9
 
10
10
  class FindingSchema(BaseModel):
@@ -13,8 +13,8 @@ class FindingSchema(BaseModel):
13
13
  severity: str
14
14
  target: str
15
15
  title: str
16
- description: Optional[str] = ""
17
- line: Optional[int] = None
16
+ description: str | None = ""
17
+ line: int | None = None
18
18
 
19
19
  @field_validator('severity')
20
20
  @classmethod
@@ -70,4 +70,4 @@ def save_scan_to_db(findings: list):
70
70
  session.rollback()
71
71
  raise
72
72
  finally:
73
- session.close()
73
+ session.close()
@@ -1,6 +1,6 @@
1
1
  import json
2
2
  import warnings
3
- from typing import List, Dict, Any
3
+ from typing import Any
4
4
 
5
5
  warnings.warn(
6
6
  "devsecops_radar.core.parser is deprecated and will be removed in v0.3.0. "
@@ -9,7 +9,7 @@ warnings.warn(
9
9
  stacklevel=2,
10
10
  )
11
11
 
12
- def parse_trivy_json(file_path: str) -> List[Dict[str, Any]]:
12
+ def parse_trivy_json(file_path: str) -> list[dict[str, Any]]:
13
13
  with open(file_path) as f:
14
14
  data = json.load(f)
15
15
  findings = []
@@ -32,7 +32,7 @@ def parse_trivy_json(file_path: str) -> List[Dict[str, Any]]:
32
32
  return findings
33
33
 
34
34
 
35
- def parse_semgrep_json(file_path: str) -> List[Dict[str, Any]]:
35
+ def parse_semgrep_json(file_path: str) -> list[dict[str, Any]]:
36
36
  with open(file_path) as f:
37
37
  data = json.load(f)
38
38
  findings = []
@@ -53,8 +53,8 @@ def parse_semgrep_json(file_path: str) -> List[Dict[str, Any]]:
53
53
  return findings
54
54
 
55
55
 
56
- def merge_findings(*finding_lists) -> List[Dict[str, Any]]:
56
+ def merge_findings(*finding_lists) -> list[dict[str, Any]]:
57
57
  merged = []
58
58
  for lst in finding_lists:
59
59
  merged.extend(lst)
60
- return merged
60
+ return merged
@@ -1,7 +1,9 @@
1
- from devsecops_radar.core.models import SessionLocal, Finding
2
- from typing import List, Dict, Any
1
+ from typing import Any
3
2
 
4
- def rag_search(query: str, limit: int = 5) -> List[Dict[str, Any]]:
3
+ from devsecops_radar.core.models import Finding, SessionLocal
4
+
5
+
6
+ def rag_search(query: str, limit: int = 5) -> list[dict[str, Any]]:
5
7
  session = SessionLocal()
6
8
  results = session.query(Finding).filter(
7
9
  Finding.title.ilike(f'%{query}%') | Finding.description.ilike(f'%{query}%')
@@ -18,4 +20,4 @@ def rag_search(query: str, limit: int = 5) -> List[Dict[str, Any]]:
18
20
  "line": f.line
19
21
  })
20
22
  session.close()
21
- return findings
23
+ return findings
@@ -2,23 +2,24 @@ import os
2
2
  import shutil
3
3
  import subprocess
4
4
  from pathlib import Path
5
- from typing import List, Dict, Any
5
+ from typing import Any
6
6
 
7
7
  BACKUP_DIR = Path.home() / ".devsecops-radar" / "backups"
8
8
 
9
+
9
10
  def _backup_file(target_file: str):
10
- """Create a backup of the file before modifying it."""
11
11
  backup_path = BACKUP_DIR / (Path(target_file).name + ".bak")
12
12
  BACKUP_DIR.mkdir(parents=True, exist_ok=True)
13
13
  shutil.copy2(target_file, backup_path)
14
14
 
15
- def apply_remediation(finding: Dict[str, Any], ai_fix: str) -> bool:
15
+
16
+ def apply_remediation(finding: dict[str, Any], ai_fix: str) -> bool:
16
17
  print(f"[FIX] Applying fix for {finding['id']}...")
17
18
  target_file = finding.get('target', '')
18
19
  line = finding.get('line')
19
20
  if target_file and line and os.path.exists(target_file):
20
21
  _backup_file(target_file)
21
- with open(target_file, 'r') as f:
22
+ with open(target_file) as f:
22
23
  lines = f.readlines()
23
24
  line_index = line - 1
24
25
  if 0 <= line_index < len(lines):
@@ -28,7 +29,8 @@ def apply_remediation(finding: Dict[str, Any], ai_fix: str) -> bool:
28
29
  return True
29
30
  return False
30
31
 
31
- def auto_fix(findings: List[Dict[str, Any]], ai_summary: Dict[str, Any]) -> List[str]:
32
+
33
+ def auto_fix(findings: list[dict[str, Any]], ai_summary: dict[str, Any]) -> list[str]:
32
34
  fixed = []
33
35
  remediations = ai_summary.get('top_remediations', [])
34
36
  for rem in remediations:
@@ -41,7 +43,8 @@ def auto_fix(findings: List[Dict[str, Any]], ai_summary: Dict[str, Any]) -> List
41
43
  fixed.append(fid)
42
44
  return fixed
43
45
 
44
- def generate_fix_commands(findings: List[Dict[str, Any]], ai_summary: Dict[str, Any]) -> str:
46
+
47
+ def generate_fix_commands(findings: list[dict[str, Any]], ai_summary: dict[str, Any]) -> str:
45
48
  commands = []
46
49
  for rem in ai_summary.get('top_remediations', []):
47
50
  fid = rem.get('finding_id')
@@ -50,15 +53,23 @@ def generate_fix_commands(findings: List[Dict[str, Any]], ai_summary: Dict[str,
50
53
  if finding:
51
54
  target = finding.get('target', '')
52
55
  if 'requirements.txt' in target:
53
- commands.append(f"# Update {target}\npip install --upgrade {finding.get('package', '')}")
56
+ commands.append(
57
+ f"# Update {target}\npip install --upgrade {finding.get('package', '')}"
58
+ )
54
59
  elif 'package.json' in target:
55
- commands.append(f"# Update {target}\nnpm update {finding.get('package', '')}")
60
+ commands.append(
61
+ f"# Update {target}\nnpm update {finding.get('package', '')}"
62
+ )
56
63
  elif 'dockerfile' in target.lower():
57
- commands.append(f"# Fix {target}\nsed -i 's/{finding.get('installed_version')}/{finding.get('fixed_version')}/' {target}")
64
+ commands.append(
65
+ f"# Fix {target}\nsed -i "
66
+ f"'s/{finding.get('installed_version')}/{finding.get('fixed_version')}/' {target}"
67
+ )
58
68
  else:
59
69
  commands.append(f"# Manual fix for {fid}: {action}")
60
70
  return '\n'.join(commands)
61
71
 
72
+
62
73
  def generate_pr(findings_file: str, branch: str = "auto-fix"):
63
74
  try:
64
75
  subprocess.run(['git', 'checkout', '-b', branch], check=True)
@@ -67,4 +78,4 @@ def generate_pr(findings_file: str, branch: str = "auto-fix"):
67
78
  subprocess.run(['git', 'push', 'origin', branch], check=True)
68
79
  print(f"[FIX] Branch '{branch}' pushed. Create a PR manually or via GitHub CLI.")
69
80
  except Exception as e:
70
- print(f"[FIX] Failed to create PR: {e}")
81
+ print(f"[FIX] Failed to create PR: {e}")