devsecops-radar 0.3.3__tar.gz → 0.3.5__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.3/devsecops_radar.egg-info → devsecops_radar-0.3.5}/PKG-INFO +1 -1
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/cli/scanner.py +3 -5
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/core/analyzer.py +2 -4
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/core/models.py +2 -8
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/core/parser.py +2 -4
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/core/reporting.py +0 -6
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/core/rule_fusion.py +1 -23
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/core/sbom.py +1 -2
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/app.py +1 -1
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/sentry/routes.py +0 -2
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5/devsecops_radar.egg-info}/PKG-INFO +1 -1
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/pyproject.toml +1 -1
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/tests/test_analyzer.py +1 -3
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/tests/test_api.py +0 -1
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/tests/test_scanners.py +0 -1
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/LICENSE +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/MANIFEST.in +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/README.md +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/__init__.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/cli/__init__.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/core/__init__.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/core/attack_simulation.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/core/auth.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/core/database.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/core/rag.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/core/remediation.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/core/settings.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/core/valuation.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/plugins/__init__.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/scanners/adapter.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/scanners/base.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/scanners/gitleaks.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/scanners/poutine.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/scanners/semgrep.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/scanners/trivy.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/scanners/zizmor.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/__init__.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/attack_paths/__init__.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/attack_paths/routes.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/dashboard/__init__.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/dashboard/routes.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/static/css/bootstrap.min.css +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/static/css/style.css +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/static/js/bootstrap.bundle.min.js +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/static/js/chart.umd.min.js +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/static/js/dashboard.js +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/summary/__init__.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/summary/routes.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/templates/index.html +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/topology/__init__.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/topology/routes.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar.egg-info/SOURCES.txt +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar.egg-info/dependency_links.txt +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar.egg-info/entry_points.txt +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar.egg-info/requires.txt +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar.egg-info/top_level.txt +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/setup.cfg +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/tests/test_cli.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/tests/test_database.py +0 -0
- {devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/tests/test_rule_fusion.py +0 -0
|
@@ -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
|
-
|
|
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
|
|
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,7 +1,7 @@
|
|
|
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,
|
|
4
|
-
from typing import
|
|
3
|
+
from pydantic import BaseModel, validator
|
|
4
|
+
from typing import Optional
|
|
5
5
|
import datetime
|
|
6
6
|
import os
|
|
7
7
|
|
|
@@ -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)
|
|
@@ -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,
|
|
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))
|
|
@@ -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
|
|
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():
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import pytest
|
|
2
1
|
from unittest.mock import patch, MagicMock
|
|
3
2
|
from devsecops_radar.core.analyzer import OllamaAnalyzer, extract_json, select_findings_for_llm
|
|
4
3
|
|
|
@@ -9,13 +8,12 @@ def test_extract_json_plain():
|
|
|
9
8
|
|
|
10
9
|
def test_extract_json_malformed():
|
|
11
10
|
result = extract_json("some text {invalid")
|
|
12
|
-
assert "executive_summary" in result
|
|
11
|
+
assert "executive_summary" in result
|
|
13
12
|
|
|
14
13
|
def test_select_findings_for_llm():
|
|
15
14
|
findings = [{"severity": "CRITICAL"}] * 120 + [{"severity": "LOW"}] * 50
|
|
16
15
|
selected = select_findings_for_llm(findings, max_items=100)
|
|
17
16
|
assert len(selected) == 100
|
|
18
|
-
# All criticals should be included
|
|
19
17
|
criticals = [f for f in selected if f["severity"] == "CRITICAL"]
|
|
20
18
|
assert len(criticals) == 100
|
|
21
19
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/attack_paths/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/static/css/bootstrap.min.css
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar/web/static/js/chart.umd.min.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{devsecops_radar-0.3.3 → devsecops_radar-0.3.5}/devsecops_radar.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|