devsecops-radar 0.1.0__py3-none-any.whl
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/__init__.py +0 -0
- devsecops_radar/cli/__init__.py +0 -0
- devsecops_radar/cli/scanner.py +72 -0
- devsecops_radar/core/__init__.py +0 -0
- devsecops_radar/core/analyzer.py +45 -0
- devsecops_radar/core/database.py +54 -0
- devsecops_radar/core/parser.py +44 -0
- devsecops_radar/scanners/base.py +11 -0
- devsecops_radar/scanners/poutine.py +33 -0
- devsecops_radar/scanners/semgrep.py +33 -0
- devsecops_radar/scanners/trivy.py +37 -0
- devsecops_radar/scanners/zizmor.py +33 -0
- devsecops_radar/web/__init__.py +0 -0
- devsecops_radar/web/app.py +33 -0
- devsecops_radar-0.1.0.dist-info/METADATA +191 -0
- devsecops_radar-0.1.0.dist-info/RECORD +20 -0
- devsecops_radar-0.1.0.dist-info/WHEEL +5 -0
- devsecops_radar-0.1.0.dist-info/entry_points.txt +3 -0
- devsecops_radar-0.1.0.dist-info/licenses/LICENSE +21 -0
- devsecops_radar-0.1.0.dist-info/top_level.txt +1 -0
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
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.core.analyzer import OllamaAnalyzer
|
|
9
|
+
from devsecops_radar.core.database import save_scan
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
parser = argparse.ArgumentParser(description='DevSecOps Radar - Collect and view security findings.')
|
|
13
|
+
parser.add_argument('--trivy', type=str, help='Path to Trivy JSON output file or image name to scan directly (e.g., nginx:latest)')
|
|
14
|
+
parser.add_argument('--semgrep', type=str, help='Path to Semgrep JSON output file or target directory to scan directly')
|
|
15
|
+
parser.add_argument('--poutine', type=str, help='Path to Poutine JSON output file or repository path to scan directly')
|
|
16
|
+
parser.add_argument('--zizmor', type=str, help='Path to Zizmor JSON output file or repository path to scan directly')
|
|
17
|
+
parser.add_argument('--output', type=str, default='findings.json', help='Output file for merged findings')
|
|
18
|
+
parser.add_argument('--analyze', action='store_true', help='Enable LLM analysis (requires Ollama running locally)')
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
all_findings = []
|
|
22
|
+
|
|
23
|
+
if args.trivy:
|
|
24
|
+
trivy = TrivyScanner()
|
|
25
|
+
if os.path.isfile(args.trivy):
|
|
26
|
+
all_findings.extend(trivy.parse(args.trivy))
|
|
27
|
+
else:
|
|
28
|
+
print(f"Scanning image: {args.trivy}")
|
|
29
|
+
all_findings.extend(trivy.run(args.trivy))
|
|
30
|
+
|
|
31
|
+
if args.semgrep:
|
|
32
|
+
semgrep = SemgrepScanner()
|
|
33
|
+
if os.path.isfile(args.semgrep):
|
|
34
|
+
all_findings.extend(semgrep.parse(args.semgrep))
|
|
35
|
+
else:
|
|
36
|
+
print(f"Scanning directory: {args.semgrep}")
|
|
37
|
+
all_findings.extend(semgrep.run(args.semgrep))
|
|
38
|
+
|
|
39
|
+
if args.poutine:
|
|
40
|
+
poutine = PoutineScanner()
|
|
41
|
+
if os.path.isfile(args.poutine):
|
|
42
|
+
all_findings.extend(poutine.parse(args.poutine))
|
|
43
|
+
else:
|
|
44
|
+
print(f"Scanning repository: {args.poutine}")
|
|
45
|
+
all_findings.extend(poutine.run(args.poutine))
|
|
46
|
+
|
|
47
|
+
if args.zizmor:
|
|
48
|
+
zizmor = ZizmorScanner()
|
|
49
|
+
if os.path.isfile(args.zizmor):
|
|
50
|
+
all_findings.extend(zizmor.parse(args.zizmor))
|
|
51
|
+
else:
|
|
52
|
+
print(f"Scanning repository: {args.zizmor}")
|
|
53
|
+
all_findings.extend(zizmor.run(args.zizmor))
|
|
54
|
+
|
|
55
|
+
with open(args.output, 'w') as f:
|
|
56
|
+
json.dump(all_findings, f, indent=2)
|
|
57
|
+
print(f"Merged {len(all_findings)} findings into {args.output}")
|
|
58
|
+
|
|
59
|
+
# Save to scan history database
|
|
60
|
+
save_scan(all_findings)
|
|
61
|
+
|
|
62
|
+
if args.analyze:
|
|
63
|
+
print("Running AI analysis...")
|
|
64
|
+
analyzer = OllamaAnalyzer()
|
|
65
|
+
summary = analyzer.analyze(all_findings)
|
|
66
|
+
summary_file = args.output.replace('.json', '_ai_summary.md')
|
|
67
|
+
with open(summary_file, 'w', encoding='utf-8') as s:
|
|
68
|
+
s.write(f"# 🛡️ Pipeline Sentinel AI Analysis\n\n{summary}")
|
|
69
|
+
print(f"AI summary saved to {summary_file}")
|
|
70
|
+
|
|
71
|
+
if __name__ == '__main__':
|
|
72
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import requests
|
|
4
|
+
from typing import List, Dict, Any, Optional
|
|
5
|
+
|
|
6
|
+
class BaseAnalyzer:
|
|
7
|
+
def analyze(self, findings: List[Dict[str, Any]]) -> str:
|
|
8
|
+
raise NotImplementedError
|
|
9
|
+
|
|
10
|
+
class OllamaAnalyzer(BaseAnalyzer):
|
|
11
|
+
def __init__(self, model: str = "llama3.2:latest", endpoint: str = "http://localhost:11434/api/generate"):
|
|
12
|
+
self.model = model
|
|
13
|
+
self.endpoint = endpoint
|
|
14
|
+
|
|
15
|
+
def analyze(self, findings: List[Dict[str, Any]]) -> str:
|
|
16
|
+
if not findings:
|
|
17
|
+
return "No findings to analyze."
|
|
18
|
+
|
|
19
|
+
criticals = [f for f in findings if f.get("severity") == "CRITICAL"]
|
|
20
|
+
highs = [f for f in findings if f.get("severity") == "HIGH"]
|
|
21
|
+
mediums = [f for f in findings if f.get("severity") == "MEDIUM"]
|
|
22
|
+
lows = [f for f in findings if f.get("severity") == "LOW"]
|
|
23
|
+
|
|
24
|
+
prompt = f"""You are a DevSecOps security expert. Analyze the following aggregated security findings from CI/CD scanners (Trivy, Semgrep, Poutine, Zizmor).
|
|
25
|
+
Total findings: {len(findings)} (CRITICAL: {len(criticals)}, HIGH: {len(highs)}, MEDIUM: {len(mediums)}, LOW: {len(lows)})
|
|
26
|
+
|
|
27
|
+
List of findings:
|
|
28
|
+
{json.dumps(findings, indent=2)}
|
|
29
|
+
|
|
30
|
+
Provide:
|
|
31
|
+
1. A short executive summary (2-3 sentences) of the overall risk.
|
|
32
|
+
2. Identify possible attack paths (e.g., "a vulnerable package combined with an exposed secret could lead to RCE").
|
|
33
|
+
3. Recommend the top 3 most critical actions to fix.
|
|
34
|
+
Keep the response concise and use bullet points."""
|
|
35
|
+
try:
|
|
36
|
+
resp = requests.post(
|
|
37
|
+
self.endpoint,
|
|
38
|
+
json={"model": self.model, "prompt": prompt, "stream": False},
|
|
39
|
+
timeout=120
|
|
40
|
+
)
|
|
41
|
+
resp.raise_for_status()
|
|
42
|
+
result = resp.json()
|
|
43
|
+
return result.get("response", "No AI response.")
|
|
44
|
+
except Exception as e:
|
|
45
|
+
return f"AI analysis failed: {str(e)}"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
import json
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import List, Dict, Any
|
|
5
|
+
|
|
6
|
+
DB_PATH = "scan_history.db"
|
|
7
|
+
|
|
8
|
+
def init_db():
|
|
9
|
+
conn = sqlite3.connect(DB_PATH)
|
|
10
|
+
c = conn.cursor()
|
|
11
|
+
c.execute('''
|
|
12
|
+
CREATE TABLE IF NOT EXISTS scans (
|
|
13
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
14
|
+
timestamp TEXT NOT NULL,
|
|
15
|
+
findings_json TEXT NOT NULL
|
|
16
|
+
)
|
|
17
|
+
''')
|
|
18
|
+
conn.commit()
|
|
19
|
+
conn.close()
|
|
20
|
+
|
|
21
|
+
def save_scan(findings: List[Dict[str, Any]]):
|
|
22
|
+
init_db()
|
|
23
|
+
conn = sqlite3.connect(DB_PATH)
|
|
24
|
+
c = conn.cursor()
|
|
25
|
+
c.execute('INSERT INTO scans (timestamp, findings_json) VALUES (?, ?)',
|
|
26
|
+
(datetime.utcnow().isoformat(), json.dumps(findings)))
|
|
27
|
+
conn.commit()
|
|
28
|
+
conn.close()
|
|
29
|
+
|
|
30
|
+
def get_all_scans() -> List[Dict[str, Any]]:
|
|
31
|
+
init_db()
|
|
32
|
+
conn = sqlite3.connect(DB_PATH)
|
|
33
|
+
conn.row_factory = sqlite3.Row
|
|
34
|
+
c = conn.cursor()
|
|
35
|
+
c.execute('SELECT id, timestamp, findings_json FROM scans ORDER BY timestamp ASC')
|
|
36
|
+
rows = c.fetchall()
|
|
37
|
+
scans = []
|
|
38
|
+
for row in rows:
|
|
39
|
+
findings = json.loads(row['findings_json'])
|
|
40
|
+
criticals = sum(1 for f in findings if f.get('severity') == 'CRITICAL')
|
|
41
|
+
highs = sum(1 for f in findings if f.get('severity') == 'HIGH')
|
|
42
|
+
mediums = sum(1 for f in findings if f.get('severity') == 'MEDIUM')
|
|
43
|
+
lows = sum(1 for f in findings if f.get('severity') == 'LOW')
|
|
44
|
+
scans.append({
|
|
45
|
+
'id': row['id'],
|
|
46
|
+
'timestamp': row['timestamp'],
|
|
47
|
+
'total': len(findings),
|
|
48
|
+
'critical': criticals,
|
|
49
|
+
'high': highs,
|
|
50
|
+
'medium': mediums,
|
|
51
|
+
'low': lows
|
|
52
|
+
})
|
|
53
|
+
conn.close()
|
|
54
|
+
return scans
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import List, Dict, Any
|
|
3
|
+
|
|
4
|
+
def parse_trivy_json(file_path: str) -> List[Dict[str, Any]]:
|
|
5
|
+
with open(file_path) as f:
|
|
6
|
+
data = json.load(f)
|
|
7
|
+
findings = []
|
|
8
|
+
for result in data.get("Results", []):
|
|
9
|
+
target = result.get("Target", "Unknown")
|
|
10
|
+
for vuln in result.get("Vulnerabilities", []):
|
|
11
|
+
findings.append({
|
|
12
|
+
"tool": "Trivy",
|
|
13
|
+
"target": target,
|
|
14
|
+
"id": vuln.get("VulnerabilityID"),
|
|
15
|
+
"severity": vuln.get("Severity", "UNKNOWN").upper(),
|
|
16
|
+
"title": vuln.get("Title", ""),
|
|
17
|
+
"description": vuln.get("Description", ""),
|
|
18
|
+
"package": vuln.get("PkgName", ""),
|
|
19
|
+
"installed_version": vuln.get("InstalledVersion", ""),
|
|
20
|
+
"fixed_version": vuln.get("FixedVersion", "")
|
|
21
|
+
})
|
|
22
|
+
return findings
|
|
23
|
+
|
|
24
|
+
def parse_semgrep_json(file_path: str) -> List[Dict[str, Any]]:
|
|
25
|
+
with open(file_path) as f:
|
|
26
|
+
data = json.load(f)
|
|
27
|
+
findings = []
|
|
28
|
+
for result in data.get("results", []):
|
|
29
|
+
findings.append({
|
|
30
|
+
"tool": "Semgrep",
|
|
31
|
+
"target": result.get("path", ""),
|
|
32
|
+
"id": result.get("check_id", ""),
|
|
33
|
+
"severity": result.get("extra", {}).get("severity", "WARNING").upper(),
|
|
34
|
+
"title": result.get("check_id", ""),
|
|
35
|
+
"description": result.get("extra", {}).get("message", ""),
|
|
36
|
+
"line": result.get("start", {}).get("line", 0),
|
|
37
|
+
})
|
|
38
|
+
return findings
|
|
39
|
+
|
|
40
|
+
def merge_findings(*finding_lists) -> List[Dict[str, Any]]:
|
|
41
|
+
merged = []
|
|
42
|
+
for lst in finding_lists:
|
|
43
|
+
merged.extend(lst)
|
|
44
|
+
return merged
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import List, Dict, Any
|
|
3
|
+
|
|
4
|
+
class BaseScanner(ABC):
|
|
5
|
+
@abstractmethod
|
|
6
|
+
def run(self, target: str) -> List[Dict[str, Any]]:
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def parse(self, file_path: str) -> List[Dict[str, Any]]:
|
|
11
|
+
pass
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
import tempfile
|
|
4
|
+
import os
|
|
5
|
+
from typing import List, Dict, Any
|
|
6
|
+
from .base import BaseScanner
|
|
7
|
+
|
|
8
|
+
class PoutineScanner(BaseScanner):
|
|
9
|
+
def run(self, target: str) -> List[Dict[str, Any]]:
|
|
10
|
+
with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as tmp:
|
|
11
|
+
outfile = tmp.name
|
|
12
|
+
try:
|
|
13
|
+
subprocess.run(['poutine', 'scan', target, '--format', 'json', '--output', outfile], check=True)
|
|
14
|
+
return self.parse(outfile)
|
|
15
|
+
finally:
|
|
16
|
+
if os.path.exists(outfile):
|
|
17
|
+
os.unlink(outfile)
|
|
18
|
+
|
|
19
|
+
def parse(self, file_path: str) -> List[Dict[str, Any]]:
|
|
20
|
+
with open(file_path) as f:
|
|
21
|
+
data = json.load(f)
|
|
22
|
+
findings = []
|
|
23
|
+
for result in data.get("findings", []):
|
|
24
|
+
findings.append({
|
|
25
|
+
"tool": "Poutine",
|
|
26
|
+
"target": result.get("location", {}).get("file", ""),
|
|
27
|
+
"id": result.get("rule_id", ""),
|
|
28
|
+
"severity": result.get("severity", "UNKNOWN").upper(),
|
|
29
|
+
"title": result.get("message", ""),
|
|
30
|
+
"description": result.get("description", ""),
|
|
31
|
+
"line": result.get("location", {}).get("line", 0),
|
|
32
|
+
})
|
|
33
|
+
return findings
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
import tempfile
|
|
4
|
+
import os
|
|
5
|
+
from typing import List, Dict, Any
|
|
6
|
+
from .base import BaseScanner
|
|
7
|
+
|
|
8
|
+
class SemgrepScanner(BaseScanner):
|
|
9
|
+
def run(self, target: str) -> List[Dict[str, Any]]:
|
|
10
|
+
with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as tmp:
|
|
11
|
+
outfile = tmp.name
|
|
12
|
+
try:
|
|
13
|
+
subprocess.run(['semgrep', '--config=auto', '--json', '--output', outfile, target], check=True)
|
|
14
|
+
return self.parse(outfile)
|
|
15
|
+
finally:
|
|
16
|
+
if os.path.exists(outfile):
|
|
17
|
+
os.unlink(outfile)
|
|
18
|
+
|
|
19
|
+
def parse(self, file_path: str) -> List[Dict[str, Any]]:
|
|
20
|
+
with open(file_path) as f:
|
|
21
|
+
data = json.load(f)
|
|
22
|
+
findings = []
|
|
23
|
+
for result in data.get("results", []):
|
|
24
|
+
findings.append({
|
|
25
|
+
"tool": "Semgrep",
|
|
26
|
+
"target": result.get("path", ""),
|
|
27
|
+
"id": result.get("check_id", ""),
|
|
28
|
+
"severity": result.get("extra", {}).get("severity", "WARNING").upper(),
|
|
29
|
+
"title": result.get("check_id", ""),
|
|
30
|
+
"description": result.get("extra", {}).get("message", ""),
|
|
31
|
+
"line": result.get("start", {}).get("line", 0),
|
|
32
|
+
})
|
|
33
|
+
return findings
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
import tempfile
|
|
4
|
+
import os
|
|
5
|
+
from typing import List, Dict, Any
|
|
6
|
+
from .base import BaseScanner
|
|
7
|
+
|
|
8
|
+
class TrivyScanner(BaseScanner):
|
|
9
|
+
def run(self, target: str) -> List[Dict[str, Any]]:
|
|
10
|
+
with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as tmp:
|
|
11
|
+
outfile = tmp.name
|
|
12
|
+
try:
|
|
13
|
+
subprocess.run(['trivy', 'image', '--format', 'json', '--output', outfile, target], check=True)
|
|
14
|
+
return self.parse(outfile)
|
|
15
|
+
finally:
|
|
16
|
+
if os.path.exists(outfile):
|
|
17
|
+
os.unlink(outfile)
|
|
18
|
+
|
|
19
|
+
def parse(self, file_path: str) -> List[Dict[str, Any]]:
|
|
20
|
+
with open(file_path) as f:
|
|
21
|
+
data = json.load(f)
|
|
22
|
+
findings = []
|
|
23
|
+
for result in data.get("Results", []):
|
|
24
|
+
target_name = result.get("Target", "Unknown")
|
|
25
|
+
for vuln in result.get("Vulnerabilities", []):
|
|
26
|
+
findings.append({
|
|
27
|
+
"tool": "Trivy",
|
|
28
|
+
"target": target_name,
|
|
29
|
+
"id": vuln.get("VulnerabilityID"),
|
|
30
|
+
"severity": vuln.get("Severity", "UNKNOWN").upper(),
|
|
31
|
+
"title": vuln.get("Title", ""),
|
|
32
|
+
"description": vuln.get("Description", ""),
|
|
33
|
+
"package": vuln.get("PkgName", ""),
|
|
34
|
+
"installed_version": vuln.get("InstalledVersion", ""),
|
|
35
|
+
"fixed_version": vuln.get("FixedVersion", "")
|
|
36
|
+
})
|
|
37
|
+
return findings
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
import tempfile
|
|
4
|
+
import os
|
|
5
|
+
from typing import List, Dict, Any
|
|
6
|
+
from .base import BaseScanner
|
|
7
|
+
|
|
8
|
+
class ZizmorScanner(BaseScanner):
|
|
9
|
+
def run(self, target: str) -> List[Dict[str, Any]]:
|
|
10
|
+
with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as tmp:
|
|
11
|
+
outfile = tmp.name
|
|
12
|
+
try:
|
|
13
|
+
subprocess.run(['zizmor', 'scan', target, '--output', outfile, '--format', 'json'], check=True)
|
|
14
|
+
return self.parse(outfile)
|
|
15
|
+
finally:
|
|
16
|
+
if os.path.exists(outfile):
|
|
17
|
+
os.unlink(outfile)
|
|
18
|
+
|
|
19
|
+
def parse(self, file_path: str) -> List[Dict[str, Any]]:
|
|
20
|
+
with open(file_path) as f:
|
|
21
|
+
data = json.load(f)
|
|
22
|
+
findings = []
|
|
23
|
+
for result in data.get("findings", []):
|
|
24
|
+
findings.append({
|
|
25
|
+
"tool": "Zizmor",
|
|
26
|
+
"target": result.get("path", ""),
|
|
27
|
+
"id": result.get("rule_id", ""),
|
|
28
|
+
"severity": result.get("severity", "UNKNOWN").upper(),
|
|
29
|
+
"title": result.get("message", ""),
|
|
30
|
+
"description": result.get("description", ""),
|
|
31
|
+
"line": result.get("location", {}).get("line", 0),
|
|
32
|
+
})
|
|
33
|
+
return findings
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from flask import Flask, render_template, jsonify
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from devsecops_radar.core.database import get_all_scans
|
|
5
|
+
|
|
6
|
+
app = Flask(__name__)
|
|
7
|
+
|
|
8
|
+
FINDINGS_FILE = os.environ.get('FINDINGS_FILE', 'findings.json')
|
|
9
|
+
|
|
10
|
+
def load_findings():
|
|
11
|
+
if not os.path.exists(FINDINGS_FILE):
|
|
12
|
+
return []
|
|
13
|
+
with open(FINDINGS_FILE) as f:
|
|
14
|
+
return json.load(f)
|
|
15
|
+
|
|
16
|
+
@app.route('/')
|
|
17
|
+
def index():
|
|
18
|
+
findings = load_findings()
|
|
19
|
+
return render_template('index.html', findings=findings)
|
|
20
|
+
|
|
21
|
+
@app.route('/api/findings')
|
|
22
|
+
def api_findings():
|
|
23
|
+
return jsonify(load_findings())
|
|
24
|
+
|
|
25
|
+
@app.route('/api/history')
|
|
26
|
+
def api_history():
|
|
27
|
+
return jsonify(get_all_scans())
|
|
28
|
+
|
|
29
|
+
def start_server(host='0.0.0.0', port=8080):
|
|
30
|
+
app.run(host=host, port=port, debug=True)
|
|
31
|
+
|
|
32
|
+
if __name__ == '__main__':
|
|
33
|
+
start_server()
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: devsecops-radar
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unified CI/CD Security Dashboard — Pipeline Sentinel
|
|
5
|
+
Author-email: Mehrdoost <mehrdoost@users.noreply.github.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Mehrdoost/devsecops-radar
|
|
8
|
+
Project-URL: Source, https://github.com/Mehrdoost/devsecops-radar
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Topic :: Security
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: flask>=3.0
|
|
18
|
+
Requires-Dist: semgrep>=1.0
|
|
19
|
+
Requires-Dist: pyyaml>=6.0
|
|
20
|
+
Requires-Dist: requests>=2.31
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# 🛡️ Pipeline Sentinel
|
|
24
|
+
|
|
25
|
+
**Unified CI/CD Security Observability — AI‑Enhanced & Offline‑Ready**
|
|
26
|
+
|
|
27
|
+
Aggregate findings from **Trivy, Semgrep, Poutine, Zizmor** and more into a single, beautiful dashboard. Correlate risks with an **LLM‑powered analysis engine**, track security trends over time, and enforce guardrails – all in one CLI + web UI.
|
|
28
|
+
|
|
29
|
+
[](https://github.com/Mehrdoost/devsecops-radar/stargazers)
|
|
30
|
+
[](LICENSE)
|
|
31
|
+
[](https://hub.docker.com/r/Mehrdoost/devsecops-radar)
|
|
32
|
+
[](https://pypi.org/project/devsecops-radar/)
|
|
33
|
+
[](https://github.com/Mehrdoost/devsecops-radar/releases)
|
|
34
|
+
[](https://github.com/Mehrdoost/devsecops-radar/actions/workflows/test-action.yml)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 🚀 Quick Start
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Install from PyPI
|
|
42
|
+
pip install devsecops-radar
|
|
43
|
+
|
|
44
|
+
# Or install directly from GitHub
|
|
45
|
+
pip install git+[https://github.com/Mehrdoost/devsecops-radar.git](https://github.com/Mehrdoost/devsecops-radar.git)
|
|
46
|
+
|
|
47
|
+
# Run the web dashboard
|
|
48
|
+
devsecops-radar-web
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
🐳 **Docker:** `docker pull ghcr.io/mehrdoost/devsecops-radar:latest` *(see instructions below)*
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## ✨ Key Features
|
|
56
|
+
|
|
57
|
+
| Capability | Description |
|
|
58
|
+
| :--- | :--- |
|
|
59
|
+
| 🔌 **Multi‑Scanner Integration** | Natively parses Trivy, Semgrep, Poutine, Zizmor. More via pluggable architecture. |
|
|
60
|
+
| 🧠 **LLM‑Powered Analysis** | Optional AI correlation, false‑positive reduction, attack‑path identification (Ollama‑backed, offline capable). |
|
|
61
|
+
| 📈 **Scan History & Trends** | SQLite‑powered historical storage. Visual trend chart shows risk evolution over time. |
|
|
62
|
+
| 🤖 **GitHub Action** | One‑step integration into your CI/CD. Summarises findings and optionally comments on PRs. |
|
|
63
|
+
| 🎨 **Beautiful Dark Dashboard** | Severity doughnut, trend line chart, search & filters – works fully offline (all assets bundled). |
|
|
64
|
+
| 🐳 **Docker Native** | Official image on GitHub Container Registry. Just one `docker run` away. |
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 🔧 Supported Scanners
|
|
69
|
+
|
|
70
|
+
| Scanner | What it scans | Status |
|
|
71
|
+
| :--- | :--- | :--- |
|
|
72
|
+
| **Trivy** | Container images & dependencies | ✅ |
|
|
73
|
+
| **Semgrep** | SAST (Static Code Analysis) | ✅ |
|
|
74
|
+
| **Poutine** | GitLab CI/CD configuration security | ✅ |
|
|
75
|
+
| **Zizmor** | GitHub Actions workflow security | ✅ |
|
|
76
|
+
| **Snyk, ZAP, Dependency-Track** | Roadmap | 🔲 |
|
|
77
|
+
|
|
78
|
+
*Adding a new scanner is easy – extend `BaseScanner` and plug it in.*
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
## 📸 Dashboard Preview
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 🤖 GitHub Action
|
|
90
|
+
|
|
91
|
+
Add security analysis to your workflow with a single step:
|
|
92
|
+
|
|
93
|
+
```yaml
|
|
94
|
+
- name: Pipeline Sentinel
|
|
95
|
+
uses: Mehrdoost/devsecops-radar/action@main
|
|
96
|
+
with:
|
|
97
|
+
trivy_report: trivy-results.json
|
|
98
|
+
semgrep_report: semgrep-results.json
|
|
99
|
+
poutine_report: poutine-results.json
|
|
100
|
+
zizmor_report: zizmor-results.json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The action merges findings, creates a job summary, and outputs CRITICAL/HIGH counts.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 📊 Scan History & Trends
|
|
108
|
+
|
|
109
|
+
Every run automatically stores findings in a local `scan_history.db`.
|
|
110
|
+
The dashboard renders a **Trend Over Time** chart so teams can monitor whether security posture is improving.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Multiple scans build history
|
|
114
|
+
devsecops-radar --trivy sample_trivy.json --semgrep sample_semgrep.json
|
|
115
|
+
devsecops-radar --trivy sample_trivy.json --semgrep sample_semgrep.json --poutine sample_poutine.json
|
|
116
|
+
|
|
117
|
+
# Now view the trend in the dashboard
|
|
118
|
+
devsecops-radar-web
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 🧠 AI‑Powered Analysis (Optional)
|
|
124
|
+
|
|
125
|
+
Enable LLM analysis with `--analyze` (requires Ollama running locally):
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
ollama pull llama3.2:latest # one-time setup
|
|
129
|
+
devsecops-radar --trivy sample_trivy.json --semgrep sample_semgrep.json --zizmor sample_zizmor.json --analyze
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
*Generates `findings_ai_summary.md` with executive summary, attack paths, and remediation tips.*
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 🛠️ Usage
|
|
137
|
+
|
|
138
|
+
### From Source (Python)
|
|
139
|
+
```bash
|
|
140
|
+
pip install -e .
|
|
141
|
+
devsecops-radar --trivy trivy.json --semgrep semgrep.json
|
|
142
|
+
devsecops-radar-web
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Docker
|
|
146
|
+
```bash
|
|
147
|
+
docker pull ghcr.io/mehrdoost/devsecops-radar:latest
|
|
148
|
+
docker run -p 8080:8080 -v $(pwd)/findings.json:/data/findings.json ghcr.io/mehrdoost/devsecops-radar:latest
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Using Sample Data
|
|
152
|
+
```bash
|
|
153
|
+
devsecops-radar --trivy sample_trivy.json --semgrep sample_semgrep.json --poutine sample_poutine.json --zizmor sample_zizmor.json
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 🗺️ Roadmap
|
|
159
|
+
|
|
160
|
+
- [x] Multi‑scanner engine (Trivy, Semgrep, Poutine, Zizmor)
|
|
161
|
+
- [x] AI correlation & analysis
|
|
162
|
+
- [x] Scan history & trend visualisation
|
|
163
|
+
- [x] GitHub Action (composite)
|
|
164
|
+
- [x] Docker image (GitHub Container Registry)
|
|
165
|
+
- [ ] Security guardrail policies (`policy.yml`)
|
|
166
|
+
- [ ] AI remediation advisor (detailed fix guidance)
|
|
167
|
+
- [ ] Findings diff/compare between branches
|
|
168
|
+
- [ ] Jira / Slack integration
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## 🤝 Contributing
|
|
173
|
+
|
|
174
|
+
Pull requests and issues are warmly welcome!
|
|
175
|
+
If you’d like to integrate a new scanner, open an issue with a sample of its JSON output.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## 👨💻 Author
|
|
180
|
+
|
|
181
|
+
**Mehrdoost**
|
|
182
|
+
|
|
183
|
+
[](https://github.com/Mehrdoost)
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## 📜 License
|
|
188
|
+
|
|
189
|
+
MIT – see [LICENSE](LICENSE) file.
|
|
190
|
+
|
|
191
|
+
⭐ **If this project helps your team ship more secure software, please drop a star!**
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
devsecops_radar/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
devsecops_radar/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
devsecops_radar/cli/scanner.py,sha256=xJFCTkup0LY2MQErtAsucYH8eWL79Ew8tu5XT5CeanE,3083
|
|
4
|
+
devsecops_radar/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
devsecops_radar/core/analyzer.py,sha256=_CVJNl6-itFTRZWni9Dz8rJ9WovUl8q3wyokoNZY_Ik,1875
|
|
6
|
+
devsecops_radar/core/database.py,sha256=2UNB6HtPL9CVKDnAuWoZ5MSSs_KEPvdnPW9bpfPGER4,1679
|
|
7
|
+
devsecops_radar/core/parser.py,sha256=Fw7cCQJtmZD66EUt6EVHe8lmP4Hqfrc9KxRw6uhl6v4,1659
|
|
8
|
+
devsecops_radar/scanners/base.py,sha256=1JtjYurfRJo8zWhGrsQTwKiIL51pRvktE3NP7CwixgI,279
|
|
9
|
+
devsecops_radar/scanners/poutine.py,sha256=ZgJuMiJlAZqoaklofhi4bR0bU91Z6cXzBhm8uFKnjRY,1249
|
|
10
|
+
devsecops_radar/scanners/semgrep.py,sha256=qJx0ZyIxBEI5ZVYkw73GN_oCAWLYhMy56U8cP_RxRDE,1256
|
|
11
|
+
devsecops_radar/scanners/trivy.py,sha256=0svnD9sLtEQui6bPS6eH1taytmzIKUpI6vesVxkIEN4,1485
|
|
12
|
+
devsecops_radar/scanners/zizmor.py,sha256=V77xyCLkX1GvrgJBgbaoV-u2vnlZ9AOyb9lFMPnBvkY,1226
|
|
13
|
+
devsecops_radar/web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
devsecops_radar/web/app.py,sha256=NJ6zdrrhs07d8zX_9zwDJnDolDGS7uC_DdoYESskPZM,789
|
|
15
|
+
devsecops_radar-0.1.0.dist-info/licenses/LICENSE,sha256=edRlxmSpczeN0DTvVwhcteN4rntObHjK-U9e_LtEncA,1060
|
|
16
|
+
devsecops_radar-0.1.0.dist-info/METADATA,sha256=jrQsgYe4LeCglscTFXSLm3LFuHOhlFv3vyMVnmmfdkE,6428
|
|
17
|
+
devsecops_radar-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
18
|
+
devsecops_radar-0.1.0.dist-info/entry_points.txt,sha256=vxlRrTXBjREf8fvya3bfS8pZbYeWtlaSAAPshkAdaVM,128
|
|
19
|
+
devsecops_radar-0.1.0.dist-info/top_level.txt,sha256=XT3Yuf6hNm9qU40AL0F3UafQzOwBXQ4Ub3Ji5Ts0g04,16
|
|
20
|
+
devsecops_radar-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mdm
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
devsecops_radar
|