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.
@@ -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.0
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
- ![Hero GIF](https://raw.githubusercontent.com/kademoslabs/assets/main/screenshots/kekkai-demo.gif)
31
+ ![Hero GIF](https://raw.githubusercontent.com/kademoslabs/assets/main/screenshots/kekkai-start.gif)
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
+ ![Hero GIF](https://raw.githubusercontent.com/kademoslabs/assets/main/screenshots/kekkai-threatflow.gif)
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 — Kekkai Portal
304
+ ## Enterprise Features
287
305
 
288
- For teams that need centralized management, **Kekkai Portal** provides:
306
+ For organizations that need advanced capabilities, **Kekkai Enterprise** provides:
289
307
 
290
308
  | Feature | Description |
291
309
  |---------|-------------|
292
- | **SAML 2.0 SSO** | Integrate with Okta, Azure AD, Google Workspace ([Setup Guide](docs/portal/saml-setup.md)) |
293
- | **Role-Based Access Control** | Fine-grained permissions per team/project ([RBAC Guide](docs/portal/rbac.md)) |
294
- | **Multi-Tenant Architecture** | Isolated environments per organization ([Architecture](docs/portal/multi-tenant.md)) |
295
- | **Aggregated Dashboards** | Centralized view of all CLI scan results |
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
- **Upgrade Path:**
299
- - CLI users can sync results to Portal: `kekkai upload` ([Sync Guide](docs/portal/cli-sync.md))
300
- - Portal provides dashboards for security managers
301
- - Self-hosted or Kademos-managed options ([Deployment Guide](docs/portal/deployment.md))
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 Portal access →](mailto:sales@kademos.org)
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=uCqs5KBqmOjNn9dCkj04H3Vq2lixQRsy2R4lCf_TJv8,60141
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=oI-vwpLITA7-U2_MxhaTp_PYfr5HqvcFy3VzKsWA6IY,6911
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=R-yyJm6tdD_uTA_8LoD6JHHO518vsQqZc4_jT7mGV-I,5500
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=5La5HUnO6ehoUoRbOfZ_QvRj0U4ud4W2o79oraBhpCg,798
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
- portal/__init__.py,sha256=vLjCqUgIqzHbT-oIMMWuWQ-lDA5jvuOOEa9qdBRLcIY,507
86
- portal/api.py,sha256=4_hQwkUnP8P3EjCdB5Tb7uRcuH3H7M6GxTvwTTmhLv4,4066
87
- portal/auth.py,sha256=4K_Ya9W_2sZl2MF0FNVr9QASjTOKAO3CMdgGUuYbb9s,3102
88
- portal/tenants.py,sha256=91SOqzjGefcHXodfN8LIHER8boeSB-Jb-WoHPTWI5GI,11394
89
- portal/uploads.py,sha256=WhosreaTKFYHNKXW9F4jOmB_OwUl1YGtT5DeaXnRMqk,7352
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,,
@@ -1,3 +1,2 @@
1
1
  [console_scripts]
2
2
  kekkai = kekkai.cli:main
3
- kekkai-portal = portal.web:main
@@ -1,3 +1,2 @@
1
1
  kekkai
2
2
  kekkai_core
3
- portal
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))