kekkai-cli 1.1.0__py3-none-any.whl → 1.1.1__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.
- kekkai/cli.py +124 -33
- kekkai/dojo_import.py +9 -1
- kekkai/output.py +1 -1
- kekkai/report/unified.py +226 -0
- kekkai/triage/__init__.py +54 -1
- kekkai/triage/loader.py +196 -0
- {kekkai_cli-1.1.0.dist-info → kekkai_cli-1.1.1.dist-info}/METADATA +33 -13
- {kekkai_cli-1.1.0.dist-info → kekkai_cli-1.1.1.dist-info}/RECORD +11 -27
- {kekkai_cli-1.1.0.dist-info → kekkai_cli-1.1.1.dist-info}/entry_points.txt +0 -1
- {kekkai_cli-1.1.0.dist-info → kekkai_cli-1.1.1.dist-info}/top_level.txt +0 -1
- portal/__init__.py +0 -19
- portal/api.py +0 -155
- portal/auth.py +0 -103
- portal/enterprise/__init__.py +0 -45
- portal/enterprise/audit.py +0 -435
- portal/enterprise/licensing.py +0 -408
- portal/enterprise/rbac.py +0 -276
- portal/enterprise/saml.py +0 -595
- portal/ops/__init__.py +0 -53
- portal/ops/backup.py +0 -553
- portal/ops/log_shipper.py +0 -469
- portal/ops/monitoring.py +0 -517
- portal/ops/restore.py +0 -469
- portal/ops/secrets.py +0 -408
- portal/ops/upgrade.py +0 -591
- portal/tenants.py +0 -340
- portal/uploads.py +0 -259
- portal/web.py +0 -393
- {kekkai_cli-1.1.0.dist-info → kekkai_cli-1.1.1.dist-info}/WHEEL +0 -0
kekkai/triage/loader.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Triage findings loader with scanner format detection.
|
|
2
|
+
|
|
3
|
+
Supports loading findings from:
|
|
4
|
+
- Native triage JSON (list or {"findings": [...]})
|
|
5
|
+
- Raw scanner outputs (Semgrep/Trivy/Gitleaks)
|
|
6
|
+
- Run directories (aggregates all *-results.json)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ..scanners.base import Finding
|
|
17
|
+
from ..scanners.base import Severity as ScannerSeverity
|
|
18
|
+
|
|
19
|
+
from .models import FindingEntry
|
|
20
|
+
from .models import Severity as TriageSeverity
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"load_findings_from_path",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
# Size limits for DoS mitigation (ASVS V10.3.3)
|
|
27
|
+
MAX_FILE_SIZE_MB = 200
|
|
28
|
+
WARN_FILE_SIZE_MB = 50
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_findings_from_path(
|
|
32
|
+
path: Path,
|
|
33
|
+
) -> tuple[list[FindingEntry], list[str]]:
|
|
34
|
+
"""Load findings from file or directory.
|
|
35
|
+
|
|
36
|
+
Supports:
|
|
37
|
+
- Unified report (kekkai-report.json) - PREFERRED
|
|
38
|
+
- Native triage JSON (list or {"findings": [...]})
|
|
39
|
+
- Raw scanner outputs (Semgrep/Trivy/Gitleaks)
|
|
40
|
+
- Run directories (aggregates all *-results.json)
|
|
41
|
+
|
|
42
|
+
Priority:
|
|
43
|
+
1. kekkai-report.json (unified report)
|
|
44
|
+
2. *-results.json (individual scanner outputs)
|
|
45
|
+
3. Any other JSON files (excluding metadata)
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
path: Path to findings file or run directory.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Tuple of (findings, error_messages).
|
|
52
|
+
Error messages include filename only (no full paths) per ASVS V7.4.1.
|
|
53
|
+
"""
|
|
54
|
+
errors: list[str] = []
|
|
55
|
+
|
|
56
|
+
# Determine input type
|
|
57
|
+
if path.is_dir():
|
|
58
|
+
# Priority 1: Check for unified report first
|
|
59
|
+
unified_report = path / "kekkai-report.json"
|
|
60
|
+
if unified_report.exists():
|
|
61
|
+
files = [unified_report]
|
|
62
|
+
else:
|
|
63
|
+
# Priority 2: Prefer canonical scan outputs
|
|
64
|
+
files = sorted(path.glob("*-results.json"))
|
|
65
|
+
if not files:
|
|
66
|
+
# Priority 3: Fallback to all JSON (excluding metadata files)
|
|
67
|
+
files = sorted(
|
|
68
|
+
[
|
|
69
|
+
p
|
|
70
|
+
for p in path.glob("*.json")
|
|
71
|
+
if p.name not in ("run.json", "policy-result.json")
|
|
72
|
+
]
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
files = [path]
|
|
76
|
+
|
|
77
|
+
findings: list[FindingEntry] = []
|
|
78
|
+
for file in files:
|
|
79
|
+
# Check if file exists first
|
|
80
|
+
if not file.exists():
|
|
81
|
+
errors.append(f"{file.name}: OSError")
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
# Size check (DoS mitigation per ASVS V10.3.3)
|
|
85
|
+
size_mb = file.stat().st_size / (1024 * 1024)
|
|
86
|
+
if size_mb > MAX_FILE_SIZE_MB:
|
|
87
|
+
msg = f"{file.name}: file too large ({size_mb:.1f} MB, max {MAX_FILE_SIZE_MB} MB)"
|
|
88
|
+
errors.append(msg)
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
content = file.read_text(encoding="utf-8")
|
|
93
|
+
if not content.strip():
|
|
94
|
+
continue
|
|
95
|
+
data = json.loads(content)
|
|
96
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
97
|
+
# ASVS V7.4.1: Don't leak full path, only filename
|
|
98
|
+
errors.append(f"{file.name}: {type(exc).__name__}")
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
# Detect format and parse
|
|
102
|
+
try:
|
|
103
|
+
batch = _parse_findings(data, file.stem)
|
|
104
|
+
findings.extend(batch)
|
|
105
|
+
except Exception as exc:
|
|
106
|
+
errors.append(f"{file.name}: unsupported format ({str(exc)[:50]})")
|
|
107
|
+
|
|
108
|
+
# Deduplicate by stable key
|
|
109
|
+
seen: set[str] = set()
|
|
110
|
+
deduped: list[FindingEntry] = []
|
|
111
|
+
for f in findings:
|
|
112
|
+
key = f"{f.scanner}:{f.rule_id}:{f.file_path}:{f.line}"
|
|
113
|
+
if key not in seen:
|
|
114
|
+
seen.add(key)
|
|
115
|
+
deduped.append(f)
|
|
116
|
+
|
|
117
|
+
return deduped, errors
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _parse_findings(data: Any, stem: str) -> list[FindingEntry]:
|
|
121
|
+
"""Parse findings from JSON data.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
data: Parsed JSON data.
|
|
125
|
+
stem: File stem (used to detect scanner type).
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
List of FindingEntry objects.
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
ValueError: If format is unknown or scanner not found.
|
|
132
|
+
"""
|
|
133
|
+
# Try native triage format first (ASVS V5.1.2: strongly typed validation)
|
|
134
|
+
if isinstance(data, list) and data and isinstance(data[0], dict) and "scanner" in data[0]:
|
|
135
|
+
return [FindingEntry.from_dict(item) for item in data]
|
|
136
|
+
|
|
137
|
+
if isinstance(data, dict) and "findings" in data:
|
|
138
|
+
findings_data = data["findings"]
|
|
139
|
+
if isinstance(findings_data, list):
|
|
140
|
+
return [FindingEntry.from_dict(item) for item in findings_data]
|
|
141
|
+
|
|
142
|
+
# Try scanner-specific format
|
|
143
|
+
scanner_name = stem.replace("-results", "")
|
|
144
|
+
|
|
145
|
+
# Lazy import to avoid circular dependency
|
|
146
|
+
from ..cli import _create_scanner
|
|
147
|
+
|
|
148
|
+
scanner = _create_scanner(scanner_name)
|
|
149
|
+
if not scanner:
|
|
150
|
+
raise ValueError(f"Unknown scanner: {scanner_name}")
|
|
151
|
+
|
|
152
|
+
# Use canonical scanner parser (reuses validated logic)
|
|
153
|
+
raw_json = json.dumps(data)
|
|
154
|
+
canonical_findings = scanner.parse(raw_json)
|
|
155
|
+
|
|
156
|
+
# Convert to triage format
|
|
157
|
+
return [_finding_to_entry(f) for f in canonical_findings]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _finding_to_entry(f: Finding) -> FindingEntry:
|
|
161
|
+
"""Convert scanner Finding to triage FindingEntry.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
f: Scanner Finding object.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Triage FindingEntry object.
|
|
168
|
+
"""
|
|
169
|
+
return FindingEntry(
|
|
170
|
+
id=f.dedupe_hash(),
|
|
171
|
+
title=f.title,
|
|
172
|
+
severity=_map_severity(f.severity),
|
|
173
|
+
scanner=f.scanner,
|
|
174
|
+
file_path=f.file_path or "",
|
|
175
|
+
line=f.line,
|
|
176
|
+
description=f.description,
|
|
177
|
+
rule_id=f.rule_id or "",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _map_severity(s: ScannerSeverity) -> TriageSeverity:
|
|
182
|
+
"""Map scanner Severity to triage Severity.
|
|
183
|
+
|
|
184
|
+
Both use the same enum values, just different type namespaces.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
s: Scanner severity enum.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Triage severity enum.
|
|
191
|
+
"""
|
|
192
|
+
try:
|
|
193
|
+
return TriageSeverity(s.value)
|
|
194
|
+
except ValueError:
|
|
195
|
+
# Fallback to INFO for unknown severities
|
|
196
|
+
return TriageSeverity.INFO
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kekkai-cli
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: Kekkai monorepo (local-first AppSec orchestration + compliance checker)
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -28,7 +28,7 @@ Requires-Dist: httpx>=0.24.0
|
|
|
28
28
|
|
|
29
29
|
Stop juggling security tools. **Kekkai orchestrates your entire AppSec lifecycle** — from AI-powered threat modeling to vulnerability management — in a single CLI.
|
|
30
30
|
|
|
31
|
-

|
|
32
32
|
|
|
33
33
|
---
|
|
34
34
|
|
|
@@ -104,6 +104,8 @@ kekkai upload
|
|
|
104
104
|
|
|
105
105
|
Generate STRIDE-aligned threat models and Mermaid.js Data Flow Diagrams from your codebase.
|
|
106
106
|
|
|
107
|
+

|
|
108
|
+
|
|
107
109
|
```bash
|
|
108
110
|
# Ollama (recommended - easy setup, privacy-preserving)
|
|
109
111
|
ollama pull mistral
|
|
@@ -177,6 +179,10 @@ kekkai triage
|
|
|
177
179
|
|
|
178
180
|
Automate security enforcement in your pipelines.
|
|
179
181
|
|
|
182
|
+
<p align="center">
|
|
183
|
+
<img src="https://raw.githubusercontent.com/kademoslabs/assets/main/screenshots/kekkai-scan.png" alt="Kekkai Scanning" width="650"/>
|
|
184
|
+
</p>
|
|
185
|
+
|
|
180
186
|
```bash
|
|
181
187
|
# Fail on any critical or high findings
|
|
182
188
|
kekkai scan --ci --fail-on high
|
|
@@ -212,6 +218,10 @@ kekkai scan --ci --fail-on medium --max-findings 5
|
|
|
212
218
|
|
|
213
219
|
Spin up a complete vulnerability management platform locally.
|
|
214
220
|
|
|
221
|
+
<p align="center">
|
|
222
|
+
<img src="https://raw.githubusercontent.com/kademoslabs/assets/main/screenshots/kekkai-dojo.png" alt="Kekkai Dojo" width="650"/>
|
|
223
|
+
</p>
|
|
224
|
+
|
|
215
225
|
```bash
|
|
216
226
|
kekkai dojo up --wait # Start DefectDojo (Nginx, Postgres, Redis, Celery)
|
|
217
227
|
kekkai dojo status # Check service health
|
|
@@ -225,6 +235,14 @@ kekkai dojo down # Stop and clean up (removes volumes)
|
|
|
225
235
|
- Pre-configured for Kekkai imports
|
|
226
236
|
- Clean teardown (no orphaned volumes)
|
|
227
237
|
|
|
238
|
+
<p align="center">
|
|
239
|
+
<img src="https://raw.githubusercontent.com/kademoslabs/assets/main/screenshots/Active-Engagements-kekkai-dojo.png" alt="Kekkai Dojo" width="850"/>
|
|
240
|
+
</p>
|
|
241
|
+
|
|
242
|
+
<p align="center">
|
|
243
|
+
<img src="https://raw.githubusercontent.com/kademoslabs/assets/main/screenshots/kekkai-dojo-dashboard-findings.png" alt="Kekkai Dojo" width="850"/>
|
|
244
|
+
</p
|
|
245
|
+
|
|
228
246
|
[Full Dojo Documentation →](docs/dojo/dojo.md)
|
|
229
247
|
|
|
230
248
|
---
|
|
@@ -283,24 +301,26 @@ pip install kekkai-cli
|
|
|
283
301
|
|
|
284
302
|
---
|
|
285
303
|
|
|
286
|
-
## Enterprise Features
|
|
304
|
+
## Enterprise Features
|
|
287
305
|
|
|
288
|
-
For
|
|
306
|
+
For organizations that need advanced capabilities, **Kekkai Enterprise** provides:
|
|
289
307
|
|
|
290
308
|
| Feature | Description |
|
|
291
309
|
|---------|-------------|
|
|
292
|
-
| **
|
|
293
|
-
| **
|
|
294
|
-
| **
|
|
295
|
-
| **
|
|
310
|
+
| **Multi-Tenant Portal** | Web dashboard for managing multiple teams/projects ([Learn More](docs/portal/README.md)) |
|
|
311
|
+
| **SAML 2.0 SSO** | Integrate with Okta, Azure AD, Google Workspace |
|
|
312
|
+
| **Role-Based Access Control** | Fine-grained permissions per team/project |
|
|
313
|
+
| **Advanced Operations** | Automated backup/restore, monitoring, zero-downtime upgrades ([Learn More](docs/ops/README.md)) |
|
|
314
|
+
| **Compliance Reporting** | Map findings to OWASP, PCI-DSS, HIPAA, SOC 2 |
|
|
296
315
|
| **Audit Logging** | Cryptographically signed compliance trails |
|
|
297
316
|
|
|
298
|
-
**
|
|
299
|
-
- CLI
|
|
300
|
-
-
|
|
301
|
-
-
|
|
317
|
+
**Architecture:**
|
|
318
|
+
- Open-source CLI remains fully functional standalone
|
|
319
|
+
- Enterprise features available in separate private repository for licensed customers
|
|
320
|
+
- Optional integration: CLI can sync results to enterprise portal
|
|
321
|
+
- Self-hosted or Kademos-managed deployment options
|
|
302
322
|
|
|
303
|
-
[Contact us for
|
|
323
|
+
[Contact us for enterprise access →](mailto:sales@kademos.org)
|
|
304
324
|
|
|
305
325
|
---
|
|
306
326
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
kekkai/__init__.py,sha256=_VrBvJRyqHiXs31S8HOhATk_O2iy-ac0_9X7rHH75j8,143
|
|
2
|
-
kekkai/cli.py,sha256
|
|
2
|
+
kekkai/cli.py,sha256=-Kix3HMEsroCgtOLu-QCtPwi7OqOSi3YzXzboT5tGvU,63529
|
|
3
3
|
kekkai/config.py,sha256=LE7bKsmv5dim5KnZya0V7_LtviNQ1V0pMN_6FyAsMpc,13084
|
|
4
4
|
kekkai/dojo.py,sha256=erLdTMOioTyzVhXYW8xgdbU5Ro-KQx1OcTQN7_zemmY,18634
|
|
5
|
-
kekkai/dojo_import.py,sha256=
|
|
5
|
+
kekkai/dojo_import.py,sha256=D0ZQM_0JYHqUqJA3l4nKD-RkpvcOcgj-4zv59HRcQ6k,7274
|
|
6
6
|
kekkai/manifest.py,sha256=Ph5xGDKuVxMW1GVIisRhxUelaiVZQe-W5sZWsq4lHqs,1887
|
|
7
|
-
kekkai/output.py,sha256=
|
|
7
|
+
kekkai/output.py,sha256=nPKsf3FjWtWf_nHj4HVpgqeZtLpPOtZoLJElVLSyPK4,5500
|
|
8
8
|
kekkai/paths.py,sha256=EcyG3CEOQFQygowu7O5Mp85dKkXWWvnm1h0j_BetGxY,1190
|
|
9
9
|
kekkai/policy.py,sha256=0XCUH-SbnO1PsM-exjSFHYHRnLkiNa50QfkyPakwNko,9792
|
|
10
10
|
kekkai/runner.py,sha256=MBFUiJ4sSVEGNbJ6cv-8p1WHaHqjio6yWEfr_K4GuTs,2037
|
|
@@ -35,6 +35,7 @@ kekkai/report/compliance_matrix.py,sha256=WOz7Fr6Hkfl7agY2DKea7Ir0z6PtC2qT0RQgfU
|
|
|
35
35
|
kekkai/report/generator.py,sha256=E1hMqUm_tB1jFLa6yWQFytukl4w-LIgTQ9gsA1LpCsc,11893
|
|
36
36
|
kekkai/report/html.py,sha256=6VJoyW08qPUWotzA0pDoO3s1Ll8E-2ypA7ldwBD_Pig,2363
|
|
37
37
|
kekkai/report/pdf.py,sha256=zGwfEQo6419MpNz2TeB5sgLG_bsLsok0v6ellCMd0FA,1751
|
|
38
|
+
kekkai/report/unified.py,sha256=2MaqwTuNRAQtGl-CtSXmsaxSLjuxh7aN1kTe7eD0mRM,6623
|
|
38
39
|
kekkai/scanners/__init__.py,sha256=uKJqnBgcf47eJlDB3mvHpLsobR6M6N6uO2L0Dor1MaE,1552
|
|
39
40
|
kekkai/scanners/base.py,sha256=uy7HgOaQxNcp6-X1gfXAecSYpKXaEsuVeluf6SwkbwM,2678
|
|
40
41
|
kekkai/scanners/container.py,sha256=A_qBZkUNVAowWeEQUVn8VPW4obRM8KtOk9rSqX7GQUA,5328
|
|
@@ -57,10 +58,11 @@ kekkai/threatflow/model_adapter.py,sha256=Vl0wBWvBUxEGTmFghjwpp-N7Zt3qkpUSxrPVjK
|
|
|
57
58
|
kekkai/threatflow/prompts.py,sha256=lgbj7FJ1c3UYj4ofGnlLoRmywYBfdAKY0QEHmIB_JFw,8525
|
|
58
59
|
kekkai/threatflow/redaction.py,sha256=mGUcNQB6YPVKArtMrEYcXCWslgUiCkloiowY9IlZ1iY,7622
|
|
59
60
|
kekkai/threatflow/sanitizer.py,sha256=uQsxYZ5VDXutZoj-WMl7fo5T07uHuQZqgVzoVMoaKec,22688
|
|
60
|
-
kekkai/triage/__init__.py,sha256=
|
|
61
|
+
kekkai/triage/__init__.py,sha256=gYf4XPIYZTthU0Q0kaptbgMKulkjLxWQWG0HQvtlu-o,2182
|
|
61
62
|
kekkai/triage/app.py,sha256=MU2tBI50d8sOdDKESGNrWYiREG9bBtrSccaMoiMv5gM,5027
|
|
62
63
|
kekkai/triage/audit.py,sha256=UVaSKKC6tZkHxEoMcnIZkMOT_ngj7QzHWYuDAHas_sc,5842
|
|
63
64
|
kekkai/triage/ignore.py,sha256=uBKM7zKyzORj9LJ5AAnoYWZQTRy57P0ZofSapiDWcfI,7305
|
|
65
|
+
kekkai/triage/loader.py,sha256=vywhS8fcre7PiBX3H2CpKXFxzvO7LcDnIHIB0kzG3R4,5850
|
|
64
66
|
kekkai/triage/models.py,sha256=nRmWtELMqHWHX1NqZ2upH2ZAJVeBxa3Wh8f3kkB9WYo,5384
|
|
65
67
|
kekkai/triage/screens.py,sha256=6eEiHvuuS_gGESS_K3NjPiQx8G7CR18-j9upU1p5nRg,11004
|
|
66
68
|
kekkai/triage/widgets.py,sha256=eOF6Qoo5uBqjxiEkbpgcO1tbIOGBQBKn75wP9Jw_AaE,4733
|
|
@@ -82,26 +84,8 @@ kekkai_core/windows/chocolatey.py,sha256=tF5S5eN-HeENRt6yQ4TZgwng0oRMX_ScskQ3-eb
|
|
|
82
84
|
kekkai_core/windows/installer.py,sha256=MePAywHH3JTIAENv52XtkUMOGqmYqZqkH77VW5PST8o,6945
|
|
83
85
|
kekkai_core/windows/scoop.py,sha256=lvothICrAoB3lGfkvhqVeNTB50eMmVGA0BE7JNCfHdI,5284
|
|
84
86
|
kekkai_core/windows/validators.py,sha256=45xUuAbHcKc0WLIZ-0rByPeDD88MAV8KvopngyYBHpQ,6525
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
portal/web.py,sha256=_9td07YYRiuCZZTpTzeKeoZzRBIwCXfWrjA7RBtJ5_8,14495
|
|
91
|
-
portal/enterprise/__init__.py,sha256=V_JYiIaVv46MynUAhXs_w2aWjfY9x_WZ9tjOqUESaeQ,1000
|
|
92
|
-
portal/enterprise/audit.py,sha256=VTm-M4gVKOxcBREqIJBs4r5wyqqqf1eCOsHi3FFiDcI,13772
|
|
93
|
-
portal/enterprise/licensing.py,sha256=RSs_gPrJ33a3DDfAQY8VDJj51uXg4av43AgNsaGl-1Q,13775
|
|
94
|
-
portal/enterprise/rbac.py,sha256=vrZoyIVmWM0C90CIgZaprwqhiDbAM-ggNNg36Zu-5lU,8548
|
|
95
|
-
portal/enterprise/saml.py,sha256=TXHBbILI7qMe0ertcFPnuSUSPbJzEeBiHmZzhY9-Ix8,20367
|
|
96
|
-
portal/ops/__init__.py,sha256=ZyEYmFM_4LFWfQfgp9Kh2vqmolSjVKFdk1vX1vkhjqc,1391
|
|
97
|
-
portal/ops/backup.py,sha256=eLUnZcUtS0svEoagb0jQQmT7TcAGjBA3fUlM2YoCfLg,20102
|
|
98
|
-
portal/ops/log_shipper.py,sha256=Age3YfvsJ5YWrPQYdHELr4Qa9jJCATHiwv3Q-rMJwJs,15237
|
|
99
|
-
portal/ops/monitoring.py,sha256=xhLbKjVaob709K4x0dEsOo4lh7Ddm2A4UE2ZmhfmMtI,17908
|
|
100
|
-
portal/ops/restore.py,sha256=rgzKoBIilgoPPv5gZhSSBuLKG1skKw5ryoCRR3d7CPQ,17058
|
|
101
|
-
portal/ops/secrets.py,sha256=wu2bUfJGctbGjyuGUgvUc_Y6IH1SCW16dExtqcKu_kg,14338
|
|
102
|
-
portal/ops/upgrade.py,sha256=fXsIXCJYYABdWDECDXkt7F2PidzNtO6Zr-g0Y5PLlVU,20106
|
|
103
|
-
kekkai_cli-1.1.0.dist-info/METADATA,sha256=-5dvVJg243pTFzu4MPaQQPICRaWzIwZTPXMH0h9hvC0,10828
|
|
104
|
-
kekkai_cli-1.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
105
|
-
kekkai_cli-1.1.0.dist-info/entry_points.txt,sha256=WUEX6IISnRcwlQAdhisPfIIV3Us2MYCwtJoyPpLJO44,75
|
|
106
|
-
kekkai_cli-1.1.0.dist-info/top_level.txt,sha256=u0J4T-Rnb0cgs0LfzZAUNt6nx1d5l7wKn8vOuo9FBEY,26
|
|
107
|
-
kekkai_cli-1.1.0.dist-info/RECORD,,
|
|
87
|
+
kekkai_cli-1.1.1.dist-info/METADATA,sha256=_Wt5_uAwnvnEK-Hmc7RxwZozkiwU_6JpLFD_xTpgDTM,11667
|
|
88
|
+
kekkai_cli-1.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
89
|
+
kekkai_cli-1.1.1.dist-info/entry_points.txt,sha256=MBV1OIfxJmT2oJvzeeFKIH1eh8M9kKAn7JqFBeuMfWA,43
|
|
90
|
+
kekkai_cli-1.1.1.dist-info/top_level.txt,sha256=wWwh7GGPaUjcaCRmt70ueL3WQoQbeGa5L0T0hgOh-MY,19
|
|
91
|
+
kekkai_cli-1.1.1.dist-info/RECORD,,
|
portal/__init__.py
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"""Kekkai Hosted Portal - DefectDojo-backed multi-tenant security dashboard."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
__all__ = [
|
|
6
|
-
"AuthMethod",
|
|
7
|
-
"AuthResult",
|
|
8
|
-
"SAMLTenantConfig",
|
|
9
|
-
"Tenant",
|
|
10
|
-
"TenantStore",
|
|
11
|
-
"UploadResult",
|
|
12
|
-
"authenticate_request",
|
|
13
|
-
"process_upload",
|
|
14
|
-
"validate_upload",
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
-
from .auth import AuthResult, authenticate_request
|
|
18
|
-
from .tenants import AuthMethod, SAMLTenantConfig, Tenant, TenantStore
|
|
19
|
-
from .uploads import UploadResult, process_upload, validate_upload
|
portal/api.py
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
"""Portal API endpoints for programmatic access.
|
|
2
|
-
|
|
3
|
-
Provides REST API endpoints that expose the same data visible in the UI.
|
|
4
|
-
All endpoints require authentication and enforce tenant isolation.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import json
|
|
10
|
-
import logging
|
|
11
|
-
import os
|
|
12
|
-
from dataclasses import dataclass
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
from typing import Any
|
|
15
|
-
|
|
16
|
-
from .tenants import Tenant
|
|
17
|
-
|
|
18
|
-
logger = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
@dataclass(frozen=True)
|
|
22
|
-
class UploadInfo:
|
|
23
|
-
"""Information about an upload."""
|
|
24
|
-
|
|
25
|
-
upload_id: str
|
|
26
|
-
filename: str
|
|
27
|
-
timestamp: str
|
|
28
|
-
file_hash: str
|
|
29
|
-
size_bytes: int
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@dataclass(frozen=True)
|
|
33
|
-
class TenantStats:
|
|
34
|
-
"""Statistics for a tenant."""
|
|
35
|
-
|
|
36
|
-
total_uploads: int
|
|
37
|
-
total_size_bytes: int
|
|
38
|
-
last_upload_time: str | None
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def get_tenant_info(tenant: Tenant) -> dict[str, Any]:
|
|
42
|
-
"""Get tenant information for API response.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
tenant: The authenticated tenant
|
|
46
|
-
|
|
47
|
-
Returns:
|
|
48
|
-
Dictionary containing tenant metadata
|
|
49
|
-
"""
|
|
50
|
-
return {
|
|
51
|
-
"id": tenant.id,
|
|
52
|
-
"name": tenant.name,
|
|
53
|
-
"dojo_product_id": tenant.dojo_product_id,
|
|
54
|
-
"dojo_engagement_id": tenant.dojo_engagement_id,
|
|
55
|
-
"enabled": tenant.enabled,
|
|
56
|
-
"max_upload_size_mb": tenant.max_upload_size_mb,
|
|
57
|
-
"auth_method": tenant.auth_method.value,
|
|
58
|
-
"default_role": tenant.default_role,
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def list_uploads(tenant: Tenant, limit: int = 50) -> list[dict[str, Any]]:
|
|
63
|
-
"""List recent uploads for a tenant.
|
|
64
|
-
|
|
65
|
-
Args:
|
|
66
|
-
tenant: The authenticated tenant
|
|
67
|
-
limit: Maximum number of uploads to return
|
|
68
|
-
|
|
69
|
-
Returns:
|
|
70
|
-
List of upload metadata dictionaries
|
|
71
|
-
"""
|
|
72
|
-
upload_dir = Path(os.environ.get("PORTAL_UPLOAD_DIR", "/var/lib/kekkai-portal/uploads"))
|
|
73
|
-
tenant_dir = upload_dir / tenant.id
|
|
74
|
-
|
|
75
|
-
if not tenant_dir.exists():
|
|
76
|
-
return []
|
|
77
|
-
|
|
78
|
-
uploads: list[dict[str, Any]] = []
|
|
79
|
-
|
|
80
|
-
# Get all upload files for this tenant
|
|
81
|
-
try:
|
|
82
|
-
upload_files = sorted(
|
|
83
|
-
tenant_dir.glob("*.json"),
|
|
84
|
-
key=lambda p: p.stat().st_mtime,
|
|
85
|
-
reverse=True,
|
|
86
|
-
)[:limit]
|
|
87
|
-
|
|
88
|
-
for upload_file in upload_files:
|
|
89
|
-
stat = upload_file.stat()
|
|
90
|
-
uploads.append(
|
|
91
|
-
{
|
|
92
|
-
"upload_id": upload_file.stem,
|
|
93
|
-
"filename": upload_file.name,
|
|
94
|
-
"timestamp": str(int(stat.st_mtime)),
|
|
95
|
-
"size_bytes": stat.st_size,
|
|
96
|
-
}
|
|
97
|
-
)
|
|
98
|
-
except (OSError, PermissionError) as e:
|
|
99
|
-
logger.warning("Failed to list uploads for tenant %s: %s", tenant.id, e)
|
|
100
|
-
|
|
101
|
-
return uploads
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def get_tenant_stats(tenant: Tenant) -> dict[str, Any]:
|
|
105
|
-
"""Get statistics for a tenant.
|
|
106
|
-
|
|
107
|
-
Args:
|
|
108
|
-
tenant: The authenticated tenant
|
|
109
|
-
|
|
110
|
-
Returns:
|
|
111
|
-
Dictionary containing tenant statistics
|
|
112
|
-
"""
|
|
113
|
-
upload_dir = Path(os.environ.get("PORTAL_UPLOAD_DIR", "/var/lib/kekkai-portal/uploads"))
|
|
114
|
-
tenant_dir = upload_dir / tenant.id
|
|
115
|
-
|
|
116
|
-
if not tenant_dir.exists():
|
|
117
|
-
return {
|
|
118
|
-
"total_uploads": 0,
|
|
119
|
-
"total_size_bytes": 0,
|
|
120
|
-
"last_upload_time": None,
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
total_uploads = 0
|
|
124
|
-
total_size_bytes = 0
|
|
125
|
-
last_upload_time: int | None = None
|
|
126
|
-
|
|
127
|
-
try:
|
|
128
|
-
for upload_file in tenant_dir.glob("*.json"):
|
|
129
|
-
stat = upload_file.stat()
|
|
130
|
-
total_uploads += 1
|
|
131
|
-
total_size_bytes += stat.st_size
|
|
132
|
-
|
|
133
|
-
if last_upload_time is None or stat.st_mtime > last_upload_time:
|
|
134
|
-
last_upload_time = int(stat.st_mtime)
|
|
135
|
-
|
|
136
|
-
except (OSError, PermissionError) as e:
|
|
137
|
-
logger.warning("Failed to get stats for tenant %s: %s", tenant.id, e)
|
|
138
|
-
|
|
139
|
-
return {
|
|
140
|
-
"total_uploads": total_uploads,
|
|
141
|
-
"total_size_bytes": total_size_bytes,
|
|
142
|
-
"last_upload_time": str(last_upload_time) if last_upload_time else None,
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def serialize_api_response(data: dict[str, Any]) -> bytes:
|
|
147
|
-
"""Serialize API response to JSON bytes.
|
|
148
|
-
|
|
149
|
-
Args:
|
|
150
|
-
data: Response data dictionary
|
|
151
|
-
|
|
152
|
-
Returns:
|
|
153
|
-
JSON-encoded bytes
|
|
154
|
-
"""
|
|
155
|
-
return json.dumps(data, indent=2).encode("utf-8")
|
portal/auth.py
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
"""Authentication middleware for portal API.
|
|
2
|
-
|
|
3
|
-
Security controls:
|
|
4
|
-
- ASVS V16.3.2: Log failed authorization attempts
|
|
5
|
-
- Constant-time API key comparison to prevent timing attacks
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import logging
|
|
11
|
-
import re
|
|
12
|
-
from dataclasses import dataclass
|
|
13
|
-
from typing import TYPE_CHECKING
|
|
14
|
-
|
|
15
|
-
from kekkai_core import redact
|
|
16
|
-
|
|
17
|
-
from .tenants import Tenant, TenantStore
|
|
18
|
-
|
|
19
|
-
if TYPE_CHECKING:
|
|
20
|
-
from collections.abc import Mapping
|
|
21
|
-
|
|
22
|
-
logger = logging.getLogger(__name__)
|
|
23
|
-
|
|
24
|
-
BEARER_PATTERN = re.compile(r"^Bearer\s+(\S+)$", re.IGNORECASE)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
@dataclass(frozen=True)
|
|
28
|
-
class AuthResult:
|
|
29
|
-
"""Result of authentication attempt."""
|
|
30
|
-
|
|
31
|
-
authenticated: bool
|
|
32
|
-
tenant: Tenant | None = None
|
|
33
|
-
error: str | None = None
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def authenticate_request(
|
|
37
|
-
headers: Mapping[str, str],
|
|
38
|
-
tenant_store: TenantStore,
|
|
39
|
-
client_ip: str = "unknown",
|
|
40
|
-
) -> AuthResult:
|
|
41
|
-
"""Authenticate a request using Bearer token.
|
|
42
|
-
|
|
43
|
-
Args:
|
|
44
|
-
headers: Request headers (case-insensitive lookup)
|
|
45
|
-
tenant_store: Tenant storage for API key verification
|
|
46
|
-
client_ip: Client IP for logging failed attempts
|
|
47
|
-
|
|
48
|
-
Returns:
|
|
49
|
-
AuthResult with tenant if authenticated, error otherwise
|
|
50
|
-
"""
|
|
51
|
-
auth_header = _get_header(headers, "Authorization")
|
|
52
|
-
if not auth_header:
|
|
53
|
-
_log_auth_failure(client_ip, "missing_header")
|
|
54
|
-
return AuthResult(authenticated=False, error="Missing Authorization header")
|
|
55
|
-
|
|
56
|
-
match = BEARER_PATTERN.match(auth_header)
|
|
57
|
-
if not match:
|
|
58
|
-
_log_auth_failure(client_ip, "invalid_format")
|
|
59
|
-
return AuthResult(authenticated=False, error="Invalid Authorization format")
|
|
60
|
-
|
|
61
|
-
api_key = match.group(1)
|
|
62
|
-
if not api_key:
|
|
63
|
-
_log_auth_failure(client_ip, "empty_token")
|
|
64
|
-
return AuthResult(authenticated=False, error="Empty API token")
|
|
65
|
-
|
|
66
|
-
tenant = tenant_store.get_by_api_key(api_key)
|
|
67
|
-
if not tenant:
|
|
68
|
-
_log_auth_failure(client_ip, "invalid_token", api_key_prefix=api_key[:8])
|
|
69
|
-
return AuthResult(authenticated=False, error="Invalid API key")
|
|
70
|
-
|
|
71
|
-
if not tenant.enabled:
|
|
72
|
-
_log_auth_failure(client_ip, "tenant_disabled", tenant_id=tenant.id)
|
|
73
|
-
return AuthResult(authenticated=False, error="Tenant is disabled")
|
|
74
|
-
|
|
75
|
-
logger.info(
|
|
76
|
-
"auth.success client_ip=%s tenant_id=%s",
|
|
77
|
-
redact(client_ip),
|
|
78
|
-
tenant.id,
|
|
79
|
-
)
|
|
80
|
-
return AuthResult(authenticated=True, tenant=tenant)
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def _get_header(headers: Mapping[str, str], name: str) -> str | None:
|
|
84
|
-
"""Get header value with case-insensitive lookup."""
|
|
85
|
-
for key, value in headers.items():
|
|
86
|
-
if key.lower() == name.lower():
|
|
87
|
-
return value
|
|
88
|
-
return None
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _log_auth_failure(
|
|
92
|
-
client_ip: str,
|
|
93
|
-
reason: str,
|
|
94
|
-
tenant_id: str | None = None,
|
|
95
|
-
api_key_prefix: str | None = None,
|
|
96
|
-
) -> None:
|
|
97
|
-
"""Log authentication failure for security monitoring (ASVS V16.3.2)."""
|
|
98
|
-
parts = [f"auth.failure reason={reason}", f"client_ip={redact(client_ip)}"]
|
|
99
|
-
if tenant_id:
|
|
100
|
-
parts.append(f"tenant_id={tenant_id}")
|
|
101
|
-
if api_key_prefix:
|
|
102
|
-
parts.append(f"api_key_prefix={api_key_prefix}...")
|
|
103
|
-
logger.warning(" ".join(parts))
|