devsecops-radar 0.3.2__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. {devsecops_radar-0.3.2/devsecops_radar.egg-info → devsecops_radar-0.3.3}/PKG-INFO +1 -1
  2. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/web/dashboard/routes.py +1 -1
  3. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3/devsecops_radar.egg-info}/PKG-INFO +1 -1
  4. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar.egg-info/SOURCES.txt +3 -0
  5. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/pyproject.toml +1 -1
  6. devsecops_radar-0.3.3/tests/test_analyzer.py +39 -0
  7. devsecops_radar-0.3.3/tests/test_api.py +29 -0
  8. devsecops_radar-0.3.3/tests/test_cli.py +30 -0
  9. devsecops_radar-0.3.3/tests/test_database.py +43 -0
  10. devsecops_radar-0.3.3/tests/test_rule_fusion.py +30 -0
  11. devsecops_radar-0.3.3/tests/test_scanners.py +81 -0
  12. devsecops_radar-0.3.2/tests/test_api.py +0 -16
  13. devsecops_radar-0.3.2/tests/test_cli.py +0 -0
  14. devsecops_radar-0.3.2/tests/test_scanners.py +0 -26
  15. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/LICENSE +0 -0
  16. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/MANIFEST.in +0 -0
  17. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/README.md +0 -0
  18. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/__init__.py +0 -0
  19. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/cli/__init__.py +0 -0
  20. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/cli/scanner.py +0 -0
  21. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/core/__init__.py +0 -0
  22. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/core/analyzer.py +0 -0
  23. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/core/attack_simulation.py +0 -0
  24. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/core/auth.py +0 -0
  25. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/core/database.py +0 -0
  26. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/core/models.py +0 -0
  27. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/core/parser.py +0 -0
  28. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/core/rag.py +0 -0
  29. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/core/remediation.py +0 -0
  30. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/core/reporting.py +0 -0
  31. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/core/rule_fusion.py +0 -0
  32. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/core/sbom.py +0 -0
  33. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/core/settings.py +0 -0
  34. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/core/valuation.py +0 -0
  35. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/plugins/__init__.py +0 -0
  36. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/scanners/adapter.py +0 -0
  37. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/scanners/base.py +0 -0
  38. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/scanners/gitleaks.py +0 -0
  39. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/scanners/poutine.py +0 -0
  40. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/scanners/semgrep.py +0 -0
  41. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/scanners/trivy.py +0 -0
  42. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/scanners/zizmor.py +0 -0
  43. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/web/__init__.py +0 -0
  44. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/web/app.py +0 -0
  45. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/web/attack_paths/__init__.py +0 -0
  46. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/web/attack_paths/routes.py +0 -0
  47. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/web/dashboard/__init__.py +0 -0
  48. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/web/sentry/routes.py +0 -0
  49. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/web/static/css/bootstrap.min.css +0 -0
  50. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/web/static/css/style.css +0 -0
  51. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/web/static/js/bootstrap.bundle.min.js +0 -0
  52. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/web/static/js/chart.umd.min.js +0 -0
  53. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/web/static/js/dashboard.js +0 -0
  54. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/web/summary/__init__.py +0 -0
  55. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/web/summary/routes.py +0 -0
  56. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/web/templates/index.html +0 -0
  57. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/web/topology/__init__.py +0 -0
  58. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar/web/topology/routes.py +0 -0
  59. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar.egg-info/dependency_links.txt +0 -0
  60. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar.egg-info/entry_points.txt +0 -0
  61. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar.egg-info/requires.txt +0 -0
  62. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/devsecops_radar.egg-info/top_level.txt +0 -0
  63. {devsecops_radar-0.3.2 → devsecops_radar-0.3.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devsecops-radar
3
- Version: 0.3.2
3
+ Version: 0.3.3
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
@@ -39,7 +39,7 @@ DASHBOARD_HTML = r"""
39
39
  <nav class="navbar navbar-dark border-bottom border-secondary mb-4" style="background:#1e293b;">
40
40
  <div class="container-fluid">
41
41
  <span class="navbar-brand mb-0 h1">🛡️ Pipeline Sentinel</span>
42
- <span class="text-muted">v0.3.2</span>
42
+ <span class="text-muted">v0.3.3</span>
43
43
  </div>
44
44
  </nav>
45
45
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devsecops-radar
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Unified CI/CD Security Dashboard — Pipeline Sentinel
5
5
  Author-email: Mehrdoost <70381337+Mehrdoost@users.noreply.github.com>
6
6
  License-Expression: MIT
@@ -50,6 +50,9 @@ devsecops_radar/web/summary/routes.py
50
50
  devsecops_radar/web/templates/index.html
51
51
  devsecops_radar/web/topology/__init__.py
52
52
  devsecops_radar/web/topology/routes.py
53
+ tests/test_analyzer.py
53
54
  tests/test_api.py
54
55
  tests/test_cli.py
56
+ tests/test_database.py
57
+ tests/test_rule_fusion.py
55
58
  tests/test_scanners.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devsecops-radar"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "Unified CI/CD Security Dashboard — Pipeline Sentinel"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,39 @@
1
+ import pytest
2
+ from unittest.mock import patch, MagicMock
3
+ from devsecops_radar.core.analyzer import OllamaAnalyzer, extract_json, select_findings_for_llm
4
+
5
+ def test_extract_json_plain():
6
+ text = '{"executive_summary": "test", "attack_paths": [], "top_remediations": []}'
7
+ result = extract_json(text)
8
+ assert result["executive_summary"] == "test"
9
+
10
+ def test_extract_json_malformed():
11
+ result = extract_json("some text {invalid")
12
+ assert "executive_summary" in result # falls back to wrapping the text
13
+
14
+ def test_select_findings_for_llm():
15
+ findings = [{"severity": "CRITICAL"}] * 120 + [{"severity": "LOW"}] * 50
16
+ selected = select_findings_for_llm(findings, max_items=100)
17
+ assert len(selected) == 100
18
+ # All criticals should be included
19
+ criticals = [f for f in selected if f["severity"] == "CRITICAL"]
20
+ assert len(criticals) == 100
21
+
22
+ @patch('requests.Session.post')
23
+ def test_ollama_analyzer_success(mock_post):
24
+ mock_response = MagicMock()
25
+ mock_response.json.return_value = {"response": '{"executive_summary": "ok", "attack_paths": [], "top_remediations": []}'}
26
+ mock_response.raise_for_status.return_value = None
27
+ mock_post.return_value = mock_response
28
+ analyzer = OllamaAnalyzer()
29
+ findings = [{"severity": "CRITICAL", "id": "1", "tool": "test"}]
30
+ analysis = analyzer.analyze(findings)
31
+ assert analysis["executive_summary"] == "ok"
32
+
33
+ @patch('requests.Session.post')
34
+ def test_ollama_analyzer_network_error(mock_post):
35
+ mock_post.side_effect = Exception("Network down")
36
+ analyzer = OllamaAnalyzer()
37
+ findings = [{"severity": "CRITICAL", "id": "1", "tool": "test"}]
38
+ analysis = analyzer.analyze(findings)
39
+ assert "AI failed" in analysis["executive_summary"]
@@ -0,0 +1,29 @@
1
+ import pytest
2
+ from devsecops_radar.web.app import create_app
3
+ import os
4
+
5
+ @pytest.fixture
6
+ def app():
7
+ app = create_app()
8
+ app.config['TESTING'] = True
9
+ return app
10
+
11
+ @pytest.fixture
12
+ def client(app):
13
+ return app.test_client()
14
+
15
+ def test_dashboard_page(client):
16
+ resp = client.get('/')
17
+ assert resp.status_code == 200
18
+
19
+ def test_findings_api_requires_key_when_set(monkeypatch, client):
20
+ monkeypatch.setenv("PIPELINE_API_KEY", "secret")
21
+ resp = client.get('/api/findings')
22
+ assert resp.status_code == 401
23
+ resp = client.get('/api/findings', headers={"X-API-Key": "secret"})
24
+ assert resp.status_code == 200
25
+
26
+ def test_findings_api_open_when_disabled(monkeypatch, client):
27
+ monkeypatch.setenv("PIPELINE_API_KEY", "disabled")
28
+ resp = client.get('/api/findings')
29
+ assert resp.status_code == 200
@@ -0,0 +1,30 @@
1
+ import subprocess
2
+ import os
3
+ import tempfile
4
+ import pytest
5
+
6
+ def test_cli_help():
7
+ result = subprocess.run(['devsecops-radar', '--help'], capture_output=True, text=True)
8
+ assert result.returncode == 0
9
+ assert '--trivy' in result.stdout
10
+
11
+ def test_cli_wizard_flag():
12
+ result = subprocess.run(['devsecops-radar', '--wizard'], capture_output=True, text=True)
13
+ assert result.returncode == 0
14
+ assert 'Quick Setup Wizard' in result.stdout or 'Welcome' in result.stdout
15
+
16
+ def test_cli_merge_sample_files():
17
+ sample_dir = os.path.join(os.path.dirname(__file__), '..')
18
+ trivy_sample = os.path.join(sample_dir, 'sample_trivy.json')
19
+ semgrep_sample = os.path.join(sample_dir, 'sample_semgrep.json')
20
+ if not os.path.exists(trivy_sample) or not os.path.exists(semgrep_sample):
21
+ pytest.skip("Sample files not found")
22
+ with tempfile.NamedTemporaryFile(suffix='.json', delete=False) as outfile:
23
+ outpath = outfile.name
24
+ result = subprocess.run(
25
+ ['devsecops-radar', '--trivy', trivy_sample, '--semgrep', semgrep_sample, '--output', outpath],
26
+ capture_output=True, text=True
27
+ )
28
+ assert result.returncode == 0
29
+ assert 'Merged' in result.stdout
30
+ os.unlink(outpath)
@@ -0,0 +1,43 @@
1
+ import os
2
+ import tempfile
3
+ import pytest
4
+
5
+ # Set a temporary database BEFORE importing any devsecops_radar modules
6
+ @pytest.fixture(scope="module")
7
+ def temp_db():
8
+ # Create a unique temp file path for the database
9
+ fd, tmpfile = tempfile.mkstemp(suffix='.db')
10
+ os.close(fd)
11
+ old_val = os.environ.get("DATABASE_URL")
12
+ os.environ["DATABASE_URL"] = f"sqlite:///{tmpfile}"
13
+
14
+ # Now import the database functions — they will use the temp DB
15
+ from devsecops_radar.core.database import save_scan, get_all_scans, get_findings_paginated
16
+
17
+ yield tmpfile, save_scan, get_all_scans, get_findings_paginated
18
+
19
+ # Cleanup
20
+ os.unlink(tmpfile)
21
+ if old_val is None:
22
+ del os.environ["DATABASE_URL"]
23
+ else:
24
+ os.environ["DATABASE_URL"] = old_val
25
+
26
+
27
+ def test_save_and_retrieve(temp_db):
28
+ tmpfile, save_scan, get_all_scans, get_findings_paginated = temp_db
29
+ findings = [
30
+ {
31
+ "tool": "test",
32
+ "severity": "HIGH",
33
+ "id": "1",
34
+ "target": "t",
35
+ "title": "t",
36
+ "description": "d"
37
+ }
38
+ ]
39
+ save_scan(findings)
40
+ scans = get_all_scans()
41
+ assert len(scans) > 0
42
+ paginated = get_findings_paginated(1, 10)
43
+ assert paginated["total"] >= 1
@@ -0,0 +1,30 @@
1
+ import json
2
+ import tempfile
3
+ import os
4
+ from devsecops_radar.core.rule_fusion import RuleFusion
5
+
6
+ class TestPolicyEvaluation:
7
+ def test_policy_pass(self):
8
+ findings = [{"severity": "CRITICAL"}] * 3
9
+ policy = {"max_critical": 5, "on_violation": "fail"}
10
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
11
+ json.dump(policy, f)
12
+ f.flush()
13
+ passed, msg = RuleFusion.evaluate_policy(findings, f.name)
14
+ os.unlink(f.name)
15
+ assert passed
16
+ assert "passed" in msg
17
+
18
+ def test_policy_fail(self):
19
+ findings = [{"severity": "CRITICAL"}] * 6
20
+ policy = {"max_critical": 5, "on_violation": "fail"}
21
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
22
+ json.dump(policy, f)
23
+ f.flush()
24
+ passed, msg = RuleFusion.evaluate_policy(findings, f.name)
25
+ os.unlink(f.name)
26
+ assert not passed
27
+
28
+ def test_policy_file_not_found(self):
29
+ passed, msg = RuleFusion.evaluate_policy([], "/nonexistent/policy.json")
30
+ assert passed
@@ -0,0 +1,81 @@
1
+ import json
2
+ import tempfile
3
+ import os
4
+ import pytest
5
+ from devsecops_radar.scanners.trivy import TrivyScanner
6
+ from devsecops_radar.scanners.semgrep import SemgrepScanner
7
+ from devsecops_radar.scanners.poutine import PoutineScanner
8
+ from devsecops_radar.scanners.zizmor import ZizmorScanner
9
+ from devsecops_radar.scanners.gitleaks import GitleaksScanner
10
+
11
+ def write_temp_json(data):
12
+ tmp = tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False)
13
+ json.dump(data, tmp)
14
+ tmp.close()
15
+ return tmp.name
16
+
17
+ class TestTrivyScanner:
18
+ def test_valid_json(self):
19
+ data = {"Results": [{"Target": "img", "Vulnerabilities": [{"VulnerabilityID": "CVE-1", "Severity": "HIGH"}]}]}
20
+ path = write_temp_json(data)
21
+ findings = TrivyScanner().parse(path)
22
+ os.unlink(path)
23
+ assert len(findings) == 1
24
+ assert findings[0]["severity"] == "HIGH"
25
+
26
+ def test_invalid_json(self):
27
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
28
+ f.write("not json")
29
+ path = f.name
30
+ findings = TrivyScanner().parse(path)
31
+ os.unlink(path)
32
+ assert findings == []
33
+
34
+ def test_missing_results(self):
35
+ data = {"not_results": []}
36
+ path = write_temp_json(data)
37
+ findings = TrivyScanner().parse(path)
38
+ os.unlink(path)
39
+ assert findings == []
40
+
41
+ class TestSemgrepScanner:
42
+ def test_valid(self):
43
+ data = {"results": [{"path": "a.py", "check_id": "x", "extra": {"severity": "ERROR", "message": "bad"}}]}
44
+ path = write_temp_json(data)
45
+ findings = SemgrepScanner().parse(path)
46
+ os.unlink(path)
47
+ assert len(findings) == 1
48
+ assert findings[0]["severity"] == "ERROR"
49
+
50
+ def test_invalid(self):
51
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
52
+ f.write("{invalid")
53
+ path = f.name
54
+ findings = SemgrepScanner().parse(path)
55
+ os.unlink(path)
56
+ assert findings == []
57
+
58
+ class TestPoutineScanner:
59
+ def test_valid(self):
60
+ data = {"findings": [{"rule_id": "x", "severity": "HIGH", "message": "bad", "location": {"file": "f", "line": 1}}]}
61
+ path = write_temp_json(data)
62
+ findings = PoutineScanner().parse(path)
63
+ os.unlink(path)
64
+ assert findings[0]["severity"] == "HIGH"
65
+
66
+ class TestZizmorScanner:
67
+ def test_valid(self):
68
+ data = {"findings": [{"rule_id": "z1", "severity": "LOW", "message": "m", "path": "p", "location": {"line": 2}}]}
69
+ path = write_temp_json(data)
70
+ findings = ZizmorScanner().parse(path)
71
+ os.unlink(path)
72
+ assert findings[0]["severity"] == "LOW"
73
+
74
+ class TestGitleaksScanner:
75
+ def test_valid_list(self):
76
+ data = [{"file": "f", "ruleID": "r", "description": "d", "line": 1}]
77
+ path = write_temp_json(data)
78
+ findings = GitleaksScanner().parse(path)
79
+ os.unlink(path)
80
+ assert len(findings) == 1
81
+ assert findings[0]["severity"] == "HIGH"
@@ -1,16 +0,0 @@
1
- import pytest
2
- from devsecops_radar.web.app import create_app
3
-
4
- @pytest.fixture
5
- def app():
6
- app = create_app()
7
- app.config['TESTING'] = True
8
- return app
9
-
10
- @pytest.fixture
11
- def client(app):
12
- return app.test_client()
13
-
14
- def test_dashboard_route(client):
15
- response = client.get('/')
16
- assert response.status_code == 200
File without changes
@@ -1,26 +0,0 @@
1
- import json
2
- import tempfile
3
- import os
4
- from devsecops_radar.scanners.trivy import TrivyScanner
5
-
6
- def test_trivy_parse():
7
- scanner = TrivyScanner()
8
- data = {
9
- "Results": [{
10
- "Target": "test-image",
11
- "Vulnerabilities": [{
12
- "VulnerabilityID": "CVE-2026-0001",
13
- "Severity": "HIGH",
14
- "Title": "Test vuln",
15
- "Description": "Test"
16
- }]
17
- }]
18
- }
19
- with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
20
- json.dump(data, f)
21
- f.flush()
22
- findings = scanner.parse(f.name)
23
- os.unlink(f.name)
24
- assert len(findings) == 1
25
- assert findings[0]["severity"] == "HIGH"
26
- assert findings[0]["id"] == "CVE-2026-0001"
File without changes