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.
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/PKG-INFO +6 -11
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/README.md +5 -10
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/cli/scanner.py +16 -3
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/analyzer.py +44 -17
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/attack_simulation.py +3 -2
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/auth.py +7 -4
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/database.py +10 -10
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/models.py +7 -7
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/parser.py +5 -5
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/rag.py +6 -4
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/remediation.py +21 -10
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/reporting.py +34 -19
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/rule_fusion.py +14 -14
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/sbom.py +6 -6
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/settings.py +2 -1
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/valuation.py +6 -5
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/plugins/__init__.py +5 -4
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/scanners/adapter.py +6 -5
- devsecops_radar-0.4.0/devsecops_radar/scanners/base.py +12 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/scanners/gitleaks.py +8 -5
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/scanners/poutine.py +8 -5
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/scanners/semgrep.py +8 -5
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/scanners/trivy.py +8 -5
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/scanners/zizmor.py +8 -5
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/app.py +8 -6
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/attack_paths/routes.py +3 -2
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/dashboard/routes.py +115 -54
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/sentry/routes.py +2 -2
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/summary/routes.py +3 -2
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/topology/routes.py +3 -2
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar.egg-info/PKG-INFO +6 -11
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/pyproject.toml +6 -1
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/tests/test_analyzer.py +20 -4
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/tests/test_api.py +5 -2
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/tests/test_cli.py +4 -2
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/tests/test_database.py +5 -7
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/tests/test_rule_fusion.py +4 -2
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/tests/test_scanners.py +52 -9
- devsecops_radar-0.3.10/devsecops_radar/scanners/base.py +0 -11
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/LICENSE +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/__init__.py +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/cli/__init__.py +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/core/__init__.py +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/__init__.py +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/attack_paths/__init__.py +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/dashboard/__init__.py +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/static/css/bootstrap.min.css +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/static/css/style.css +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/static/js/bootstrap.bundle.min.js +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/static/js/chart.umd.min.js +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/static/js/dashboard.js +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/summary/__init__.py +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/templates/index.html +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar/web/topology/__init__.py +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar.egg-info/SOURCES.txt +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar.egg-info/dependency_links.txt +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar.egg-info/entry_points.txt +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar.egg-info/requires.txt +0 -0
- {devsecops_radar-0.3.10 → devsecops_radar-0.4.0}/devsecops_radar.egg-info/top_level.txt +0 -0
- {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
|
+
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:**
|
|
133
|
-
|
|
127
|
+
> **📌 Diagram Placeholder:**
|
|
128
|
+

|
|
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:**
|
|
494
|
-
|
|
488
|
+
> **📌 Diagram Placeholder:**
|
|
489
|
+

|
|
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
|
[](https://github.com/ReverseForge)
|
|
575
570
|
[](https://github.com/Mehrdoost)
|
|
576
|
-
[](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:**
|
|
99
|
-
|
|
93
|
+
> **📌 Diagram Placeholder:**
|
|
94
|
+

|
|
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:**
|
|
460
|
-
|
|
454
|
+
> **📌 Diagram Placeholder:**
|
|
455
|
+

|
|
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
|
[](https://github.com/ReverseForge)
|
|
541
536
|
[](https://github.com/Mehrdoost)
|
|
542
|
-
[](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
|
-
|
|
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
|
-
|
|
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":
|
|
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":
|
|
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":
|
|
43
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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:
|
|
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() ->
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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:
|
|
17
|
-
line:
|
|
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
|
|
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) ->
|
|
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) ->
|
|
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) ->
|
|
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
|
|
2
|
-
from typing import List, Dict, Any
|
|
1
|
+
from typing import Any
|
|
3
2
|
|
|
4
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
56
|
+
commands.append(
|
|
57
|
+
f"# Update {target}\npip install --upgrade {finding.get('package', '')}"
|
|
58
|
+
)
|
|
54
59
|
elif 'package.json' in target:
|
|
55
|
-
commands.append(
|
|
60
|
+
commands.append(
|
|
61
|
+
f"# Update {target}\nnpm update {finding.get('package', '')}"
|
|
62
|
+
)
|
|
56
63
|
elif 'dockerfile' in target.lower():
|
|
57
|
-
commands.append(
|
|
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}")
|