kekkai-cli 2.0.1__tar.gz → 2.2.0__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 (147) hide show
  1. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/PKG-INFO +19 -30
  2. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/README.md +18 -29
  3. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/pyproject.toml +2 -2
  4. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/cli.py +46 -1
  5. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/output.py +1 -1
  6. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/__init__.py +6 -0
  7. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/app.py +12 -0
  8. kekkai_cli-2.2.0/src/kekkai/triage/code_context.py +345 -0
  9. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/fix_screen.py +34 -4
  10. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/screens.py +225 -0
  11. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_cli.egg-info/PKG-INFO +19 -30
  12. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_cli.egg-info/SOURCES.txt +4 -0
  13. kekkai_cli-2.2.0/tests/test_triage_code_context.py +273 -0
  14. kekkai_cli-2.2.0/tests/test_triage_editor.py +328 -0
  15. kekkai_cli-2.2.0/tests/test_triage_security.py +192 -0
  16. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/setup.cfg +0 -0
  17. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/__init__.py +0 -0
  18. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/compliance/__init__.py +0 -0
  19. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/compliance/hipaa.py +0 -0
  20. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/compliance/mappings.py +0 -0
  21. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/compliance/owasp.py +0 -0
  22. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/compliance/owasp_agentic.py +0 -0
  23. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/compliance/pci_dss.py +0 -0
  24. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/compliance/soc2.py +0 -0
  25. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/config.py +0 -0
  26. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/dojo.py +0 -0
  27. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/dojo_import.py +0 -0
  28. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/fix/__init__.py +0 -0
  29. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/fix/audit.py +0 -0
  30. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/fix/differ.py +0 -0
  31. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/fix/engine.py +0 -0
  32. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/fix/prompts.py +0 -0
  33. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/github/__init__.py +0 -0
  34. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/github/commenter.py +0 -0
  35. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/github/models.py +0 -0
  36. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/github/sanitizer.py +0 -0
  37. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/installer/__init__.py +0 -0
  38. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/installer/errors.py +0 -0
  39. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/installer/extract.py +0 -0
  40. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/installer/manager.py +0 -0
  41. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/installer/manifest.py +0 -0
  42. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/installer/verify.py +0 -0
  43. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/manifest.py +0 -0
  44. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/paths.py +0 -0
  45. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/policy.py +0 -0
  46. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/report/__init__.py +0 -0
  47. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/report/compliance_matrix.py +0 -0
  48. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/report/generator.py +0 -0
  49. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/report/html.py +0 -0
  50. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/report/pdf.py +0 -0
  51. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/report/unified.py +0 -0
  52. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/runner.py +0 -0
  53. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/__init__.py +0 -0
  54. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/backends/__init__.py +0 -0
  55. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/backends/base.py +0 -0
  56. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/backends/docker.py +0 -0
  57. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/backends/native.py +0 -0
  58. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/base.py +0 -0
  59. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/container.py +0 -0
  60. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/falco.py +0 -0
  61. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/gitleaks.py +0 -0
  62. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/semgrep.py +0 -0
  63. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/trivy.py +0 -0
  64. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/url_policy.py +0 -0
  65. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/zap.py +0 -0
  66. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/__init__.py +0 -0
  67. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/artifacts.py +0 -0
  68. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/chunking.py +0 -0
  69. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/core.py +0 -0
  70. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/mermaid.py +0 -0
  71. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/model_adapter.py +0 -0
  72. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/prompts.py +0 -0
  73. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/redaction.py +0 -0
  74. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/sanitizer.py +0 -0
  75. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/audit.py +0 -0
  76. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/ignore.py +0 -0
  77. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/loader.py +0 -0
  78. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/models.py +0 -0
  79. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/widgets.py +0 -0
  80. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_cli.egg-info/dependency_links.txt +0 -0
  81. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_cli.egg-info/entry_points.txt +0 -0
  82. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_cli.egg-info/requires.txt +0 -0
  83. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_cli.egg-info/top_level.txt +0 -0
  84. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/__init__.py +0 -0
  85. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/ci/__init__.py +0 -0
  86. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/ci/benchmarks.py +0 -0
  87. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/ci/metadata.py +0 -0
  88. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/ci/validators.py +0 -0
  89. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/docker/__init__.py +0 -0
  90. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/docker/metadata.py +0 -0
  91. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/docker/sbom.py +0 -0
  92. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/docker/security.py +0 -0
  93. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/docker/signing.py +0 -0
  94. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/redaction.py +0 -0
  95. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/slsa/__init__.py +0 -0
  96. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/slsa/verify.py +0 -0
  97. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/windows/__init__.py +0 -0
  98. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/windows/chocolatey.py +0 -0
  99. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/windows/installer.py +0 -0
  100. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/windows/scoop.py +0 -0
  101. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/windows/validators.py +0 -0
  102. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_cli_output.py +0 -0
  103. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_compliance.py +0 -0
  104. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_dojo_import.py +0 -0
  105. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_fix_engine.py +0 -0
  106. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_github_commenter_filter.py +0 -0
  107. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_github_commenter_format.py +0 -0
  108. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_github_commenter_limit.py +0 -0
  109. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_github_commenter_sanitize.py +0 -0
  110. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_installer_checksum.py +0 -0
  111. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_installer_extract.py +0 -0
  112. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_installer_manager.py +0 -0
  113. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_installer_manifest.py +0 -0
  114. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_installer_platform.py +0 -0
  115. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_kekkai_cli.py +0 -0
  116. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_kekkai_config.py +0 -0
  117. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_kekkai_dojo.py +0 -0
  118. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_kekkai_dojo_cli.py +0 -0
  119. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_kekkai_manifest.py +0 -0
  120. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_kekkai_paths.py +0 -0
  121. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_kekkai_runner.py +0 -0
  122. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_mermaid.py +0 -0
  123. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_policy.py +0 -0
  124. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_redaction.py +0 -0
  125. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_report.py +0 -0
  126. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_backends.py +0 -0
  127. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_base.py +0 -0
  128. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_container.py +0 -0
  129. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_digest_defaults.py +0 -0
  130. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_falco.py +0 -0
  131. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_gitleaks.py +0 -0
  132. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_native.py +0 -0
  133. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_semgrep.py +0 -0
  134. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_trivy.py +0 -0
  135. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_zap.py +0 -0
  136. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_slsa_provenance.py +0 -0
  137. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_threatflow_chunking.py +0 -0
  138. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_threatflow_model_adapter.py +0 -0
  139. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_threatflow_prompts.py +0 -0
  140. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_threatflow_redaction.py +0 -0
  141. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_threatflow_sanitizer.py +0 -0
  142. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_triage_audit.py +0 -0
  143. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_triage_ignore.py +0 -0
  144. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_triage_loader.py +0 -0
  145. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_triage_models.py +0 -0
  146. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_unified_report.py +0 -0
  147. {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_url_policy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kekkai-cli
3
- Version: 2.0.1
3
+ Version: 2.2.0
4
4
  Summary: Terminal UI for Trivy/Semgrep/Gitleaks. Local-first security triage.
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -14,19 +14,20 @@ Requires-Dist: jinja2>=3.1.6
14
14
  <img src="https://raw.githubusercontent.com/kademoslabs/assets/main/logos/kekkai-slim.png" alt="Kekkai CLI Logo" width="250"/>
15
15
  </p>
16
16
 
17
- <p align="center"><strong>Stop parsing JSON. Security triage in your terminal.</strong></p>
17
+ <p align="center"><strong>Interactive security triage in the terminal.</strong></p>
18
18
 
19
19
  <p align="center">
20
20
  <img src="https://img.shields.io/github/actions/workflow/status/kademoslabs/kekkai/docker-publish.yml?logo=github"/>
21
21
  <img src="https://img.shields.io/circleci/build/github/kademoslabs/kekkai?logo=circleci"/>
22
- <img src="https://img.shields.io/pypi/v/kekkai-cli?pypiBaseUrl=https%3A%2F%2Fpypi.org&logo=pypi"/>
22
+ <img alt="PyPI - Version" src="https://img.shields.io/pypi/v/kekkai-cli">
23
+
23
24
  </p>
24
25
 
25
26
  ---
26
27
 
27
28
  # Kekkai
28
29
 
29
- **Interactive security triage in the terminal.**
30
+ **Stop parsing JSON.**
30
31
 
31
32
  Kekkai is a small open-source CLI that wraps existing security scanners (Trivy, Semgrep, Gitleaks) and focuses on the part that tends to be slow and frustrating: reviewing and triaging results.
32
33
 
@@ -73,6 +74,17 @@ kekkai triage
73
74
  # Interactive TUI to review findings with keyboard navigation
74
75
  ```
75
76
 
77
+ ### ⚡️ Auto-Install (Pre-commit)
78
+
79
+ Add this to your `.pre-commit-config.yaml` to scan on every commit:
80
+
81
+ ```yaml
82
+ - repo: [https://github.com/kademoslabs/kekkai](https://github.com/kademoslabs/kekkai)
83
+ rev: v2.0.1
84
+ hooks:
85
+ - id: kekkai-scan
86
+ ```
87
+
76
88
  No signup, no cloud service required.
77
89
 
78
90
  ---
@@ -114,34 +126,11 @@ kekkai triage
114
126
 
115
127
  ---
116
128
 
117
- ### CI/CD Policy Gate
118
-
119
- Break builds on severity thresholds.
120
-
121
- Kekkai can be used as a CI gate based on severity thresholds.
129
+ ### 🚦 CI/CD in 1 Second
122
130
 
131
+ Don't write YAML. Run this in your repo:
123
132
  ```bash
124
- # Fail on any critical or high findings
125
- kekkai scan --ci --fail-on high
126
-
127
- # Fail only on critical
128
- kekkai scan --ci --fail-on critical
129
- ```
130
-
131
- **Exit Codes:**
132
- | Code | Meaning |
133
- |------|---------|
134
- | 0 | No findings above threshold |
135
- | 1 | Findings exceed threshold |
136
- | 2 | Scanner error |
137
-
138
- **GitHub Actions Example:**
139
-
140
- ```yaml
141
- - name: Security Scan
142
- run: |
143
- pipx install kekkai-cli
144
- kekkai scan --ci --fail-on high
133
+ kekkai init --ci
145
134
  ```
146
135
 
147
136
  [Full CI Documentation →](docs/ci/ci-mode.md)
@@ -2,19 +2,20 @@
2
2
  <img src="https://raw.githubusercontent.com/kademoslabs/assets/main/logos/kekkai-slim.png" alt="Kekkai CLI Logo" width="250"/>
3
3
  </p>
4
4
 
5
- <p align="center"><strong>Stop parsing JSON. Security triage in your terminal.</strong></p>
5
+ <p align="center"><strong>Interactive security triage in the terminal.</strong></p>
6
6
 
7
7
  <p align="center">
8
8
  <img src="https://img.shields.io/github/actions/workflow/status/kademoslabs/kekkai/docker-publish.yml?logo=github"/>
9
9
  <img src="https://img.shields.io/circleci/build/github/kademoslabs/kekkai?logo=circleci"/>
10
- <img src="https://img.shields.io/pypi/v/kekkai-cli?pypiBaseUrl=https%3A%2F%2Fpypi.org&logo=pypi"/>
10
+ <img alt="PyPI - Version" src="https://img.shields.io/pypi/v/kekkai-cli">
11
+
11
12
  </p>
12
13
 
13
14
  ---
14
15
 
15
16
  # Kekkai
16
17
 
17
- **Interactive security triage in the terminal.**
18
+ **Stop parsing JSON.**
18
19
 
19
20
  Kekkai is a small open-source CLI that wraps existing security scanners (Trivy, Semgrep, Gitleaks) and focuses on the part that tends to be slow and frustrating: reviewing and triaging results.
20
21
 
@@ -61,6 +62,17 @@ kekkai triage
61
62
  # Interactive TUI to review findings with keyboard navigation
62
63
  ```
63
64
 
65
+ ### ⚡️ Auto-Install (Pre-commit)
66
+
67
+ Add this to your `.pre-commit-config.yaml` to scan on every commit:
68
+
69
+ ```yaml
70
+ - repo: [https://github.com/kademoslabs/kekkai](https://github.com/kademoslabs/kekkai)
71
+ rev: v2.0.1
72
+ hooks:
73
+ - id: kekkai-scan
74
+ ```
75
+
64
76
  No signup, no cloud service required.
65
77
 
66
78
  ---
@@ -102,34 +114,11 @@ kekkai triage
102
114
 
103
115
  ---
104
116
 
105
- ### CI/CD Policy Gate
106
-
107
- Break builds on severity thresholds.
108
-
109
- Kekkai can be used as a CI gate based on severity thresholds.
117
+ ### 🚦 CI/CD in 1 Second
110
118
 
119
+ Don't write YAML. Run this in your repo:
111
120
  ```bash
112
- # Fail on any critical or high findings
113
- kekkai scan --ci --fail-on high
114
-
115
- # Fail only on critical
116
- kekkai scan --ci --fail-on critical
117
- ```
118
-
119
- **Exit Codes:**
120
- | Code | Meaning |
121
- |------|---------|
122
- | 0 | No findings above threshold |
123
- | 1 | Findings exceed threshold |
124
- | 2 | Scanner error |
125
-
126
- **GitHub Actions Example:**
127
-
128
- ```yaml
129
- - name: Security Scan
130
- run: |
131
- pipx install kekkai-cli
132
- kekkai scan --ci --fail-on high
121
+ kekkai init --ci
133
122
  ```
134
123
 
135
124
  [Full CI Documentation →](docs/ci/ci-mode.md)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kekkai-cli"
3
- version = "2.0.1"
3
+ version = "2.2.0"
4
4
  description = "Terminal UI for Trivy/Semgrep/Gitleaks. Local-first security triage."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -21,7 +21,7 @@ line-length = 100
21
21
  extend-exclude = ["dist", "build", ".venv"]
22
22
  lint.select = ["E", "F", "I", "B", "UP", "S", "SIM"]
23
23
  lint.ignore = ["S101"] # allow asserts in tests
24
- lint.per-file-ignores = { "tests/**" = ["S"], "src/kekkai_core/docker/**" = ["S603"], "src/kekkai_core/slsa/**" = ["S603", "S607"], "src/kekkai/github/**" = ["S105"] }
24
+ lint.per-file-ignores = { "tests/**" = ["S", "SIM117"], "src/kekkai_core/docker/**" = ["S603"], "src/kekkai_core/slsa/**" = ["S603", "S607"], "src/kekkai/github/**" = ["S105"] }
25
25
 
26
26
  [tool.mypy]
27
27
  python_version = "3.12"
@@ -221,6 +221,17 @@ def main(argv: Sequence[str] | None = None) -> int:
221
221
  type=str,
222
222
  help="Path for .kekkaiignore output (default: .kekkaiignore)",
223
223
  )
224
+ triage_parser.add_argument(
225
+ "--repo",
226
+ type=str,
227
+ help="Repository root path (default: auto-detect from run.json)",
228
+ )
229
+ triage_parser.add_argument(
230
+ "--context-lines",
231
+ type=int,
232
+ default=10,
233
+ help="Context lines before/after line (default: 10, range: 5-100)",
234
+ )
224
235
 
225
236
  # Fix subcommand - AI-powered remediation
226
237
  fix_parser = subparsers.add_parser("fix", help="generate AI-powered code fixes for findings")
@@ -1186,6 +1197,11 @@ def _command_triage(parsed: argparse.Namespace) -> int:
1186
1197
 
1187
1198
  input_path_str = cast(str | None, getattr(parsed, "input", None))
1188
1199
  output_path_str = cast(str | None, getattr(parsed, "output", None))
1200
+ repo_path_str = cast(str | None, getattr(parsed, "repo", None))
1201
+ context_lines = cast(int, getattr(parsed, "context_lines", 10))
1202
+
1203
+ # Validate context_lines range
1204
+ context_lines = max(5, min(100, context_lines))
1189
1205
 
1190
1206
  # Default to latest run if no input specified
1191
1207
  if not input_path_str:
@@ -1232,7 +1248,36 @@ def _command_triage(parsed: argparse.Namespace) -> int:
1232
1248
 
1233
1249
  console.print(f"[info]Loaded {len(findings)} finding(s)[/info]\n")
1234
1250
 
1235
- return run_triage(findings=findings, output_path=output_path)
1251
+ # Auto-detect repo_path from run.json if not explicitly provided
1252
+ repo_path: Path | None = None
1253
+ if repo_path_str:
1254
+ repo_path = Path(repo_path_str).expanduser().resolve()
1255
+ elif input_path.is_dir():
1256
+ # Try to read repo_path from run.json
1257
+ manifest_path = input_path / "run.json"
1258
+ if manifest_path.exists():
1259
+ try:
1260
+ import json
1261
+
1262
+ with manifest_path.open() as f:
1263
+ manifest_data = json.load(f)
1264
+ stored_repo = manifest_data.get("repo_path")
1265
+ if stored_repo:
1266
+ repo_path = Path(stored_repo).expanduser().resolve()
1267
+ console.print(f"[dim]Using repo path from run metadata: {repo_path}[/dim]\n")
1268
+ except (OSError, json.JSONDecodeError, KeyError):
1269
+ pass
1270
+
1271
+ # Fall back to current directory if still not set
1272
+ if repo_path is None:
1273
+ repo_path = Path.cwd()
1274
+
1275
+ return run_triage(
1276
+ findings=findings,
1277
+ output_path=output_path,
1278
+ repo_path=repo_path,
1279
+ context_lines=context_lines,
1280
+ )
1236
1281
 
1237
1282
 
1238
1283
  def _command_fix(parsed: argparse.Namespace) -> int:
@@ -57,7 +57,7 @@ BANNER_ASCII = r"""
57
57
  /_/\_\\___/_/\_/_/\_\\_,_/_/
58
58
  """
59
59
 
60
- VERSION = "2.0.1"
60
+ VERSION = "2.2.0"
61
61
 
62
62
 
63
63
  def print_dashboard() -> None:
@@ -29,6 +29,8 @@ def run_triage(
29
29
  input_path: Path | None = None,
30
30
  output_path: Path | None = None,
31
31
  findings: Sequence[FindingEntry] | None = None,
32
+ repo_path: Path | None = None,
33
+ context_lines: int = 10,
32
34
  ) -> int:
33
35
  """Run the triage TUI (lazy import).
34
36
 
@@ -36,6 +38,8 @@ def run_triage(
36
38
  input_path: Path to findings JSON file.
37
39
  output_path: Path for .kekkaiignore output.
38
40
  findings: Pre-loaded findings (alternative to input_path).
41
+ repo_path: Repository root path for code context display.
42
+ context_lines: Number of lines to show before/after vulnerable line.
39
43
 
40
44
  Returns:
41
45
  Exit code (0 for success).
@@ -50,6 +54,8 @@ def run_triage(
50
54
  input_path=input_path,
51
55
  output_path=output_path,
52
56
  findings=findings,
57
+ repo_path=repo_path,
58
+ context_lines=context_lines,
53
59
  )
54
60
  except ImportError as e:
55
61
  raise RuntimeError(
@@ -51,6 +51,8 @@ class TriageApp(App[None]):
51
51
  input_path: Path | None = None,
52
52
  output_path: Path | None = None,
53
53
  audit_path: Path | None = None,
54
+ repo_path: Path | None = None,
55
+ context_lines: int = 10,
54
56
  ) -> None:
55
57
  """Initialize triage application.
56
58
 
@@ -59,6 +61,8 @@ class TriageApp(App[None]):
59
61
  input_path: Path to findings JSON file.
60
62
  output_path: Path for .kekkaiignore output.
61
63
  audit_path: Path for audit log.
64
+ repo_path: Repository root path for code context display.
65
+ context_lines: Number of lines to show before/after vulnerable line.
62
66
  """
63
67
  super().__init__()
64
68
  self._input_path = input_path
@@ -66,6 +70,8 @@ class TriageApp(App[None]):
66
70
  self.ignore_file = IgnoreFile(output_path)
67
71
  self.audit_log = TriageAuditLog(audit_path)
68
72
  self._decisions: dict[str, TriageDecision] = {}
73
+ self.repo_path = repo_path or Path.cwd()
74
+ self.context_lines = context_lines
69
75
 
70
76
  @property
71
77
  def findings(self) -> list[FindingEntry]:
@@ -148,6 +154,8 @@ def run_triage(
148
154
  input_path: Path | None = None,
149
155
  output_path: Path | None = None,
150
156
  findings: Sequence[FindingEntry] | None = None,
157
+ repo_path: Path | None = None,
158
+ context_lines: int = 10,
151
159
  ) -> int:
152
160
  """Run the triage TUI.
153
161
 
@@ -155,6 +163,8 @@ def run_triage(
155
163
  input_path: Path to findings JSON file.
156
164
  output_path: Path for .kekkaiignore output.
157
165
  findings: Pre-loaded findings (alternative to input_path).
166
+ repo_path: Repository root path for code context display.
167
+ context_lines: Number of lines to show before/after vulnerable line.
158
168
 
159
169
  Returns:
160
170
  Exit code (0 for success).
@@ -163,6 +173,8 @@ def run_triage(
163
173
  findings=findings,
164
174
  input_path=input_path,
165
175
  output_path=output_path,
176
+ repo_path=repo_path,
177
+ context_lines=context_lines,
166
178
  )
167
179
  app.run()
168
180
  return 0
@@ -0,0 +1,345 @@
1
+ """Code context extraction for triage TUI.
2
+
3
+ Provides secure code context extraction with security controls:
4
+ - Path traversal protection (ASVS V5.3.3)
5
+ - File size limits (ASVS V10.3.3)
6
+ - Sensitive file detection (ASVS V8.3.4)
7
+ - Error sanitization (ASVS V7.4.1)
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from dataclasses import dataclass
14
+ from pathlib import Path
15
+
16
+ from ..fix.prompts import FixPromptBuilder
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ __all__ = [
21
+ "CodeContext",
22
+ "CodeContextExtractor",
23
+ ]
24
+
25
+ # Security limits per ASVS V10.3.3
26
+ MAX_FILE_SIZE_MB = 10
27
+
28
+ # Sensitive file extensions to block (ASVS V8.3.4)
29
+ SENSITIVE_EXTENSIONS = {
30
+ ".env",
31
+ ".pem",
32
+ ".key",
33
+ ".crt",
34
+ ".p12",
35
+ ".pfx",
36
+ ".jks",
37
+ ".keystore",
38
+ ".pub",
39
+ "id_rsa",
40
+ "id_dsa",
41
+ "id_ecdsa",
42
+ "id_ed25519",
43
+ }
44
+
45
+ # Binary file extensions to skip
46
+ BINARY_EXTENSIONS = {
47
+ ".pyc",
48
+ ".pyo",
49
+ ".so",
50
+ ".dll",
51
+ ".dylib",
52
+ ".exe",
53
+ ".bin",
54
+ ".class",
55
+ ".jar",
56
+ ".war",
57
+ ".ear",
58
+ ".png",
59
+ ".jpg",
60
+ ".jpeg",
61
+ ".gif",
62
+ ".bmp",
63
+ ".ico",
64
+ ".pdf",
65
+ ".zip",
66
+ ".tar",
67
+ ".gz",
68
+ ".bz2",
69
+ ".7z",
70
+ ".rar",
71
+ ".whl",
72
+ ".egg",
73
+ }
74
+
75
+
76
+ @dataclass
77
+ class CodeContext:
78
+ """Code context with syntax highlighting metadata.
79
+
80
+ Attributes:
81
+ code: Formatted code with line numbers and >>> marker.
82
+ language: Detected programming language for syntax highlighting.
83
+ vulnerable_line: The specific vulnerable line text.
84
+ error: Error message if extraction failed (None on success).
85
+ """
86
+
87
+ code: str
88
+ language: str
89
+ vulnerable_line: str
90
+ error: str | None = None
91
+
92
+
93
+ class CodeContextExtractor:
94
+ """Extracts code context from files with security controls.
95
+
96
+ Security features:
97
+ - Path validation to prevent traversal attacks (ASVS V5.3.3)
98
+ - File size limits to prevent DoS (ASVS V10.3.3)
99
+ - Sensitive file detection (ASVS V8.3.4)
100
+ - Sanitized error messages (ASVS V7.4.1)
101
+
102
+ Performance features:
103
+ - LRU cache for file contents (max 20 files, ~200KB per file = ~4MB total)
104
+ - Reduces re-reads when navigating between findings in same files
105
+ """
106
+
107
+ def __init__(self, repo_path: Path, max_file_size_mb: int = MAX_FILE_SIZE_MB) -> None:
108
+ """Initialize code context extractor.
109
+
110
+ Args:
111
+ repo_path: Repository root path (for path validation).
112
+ max_file_size_mb: Maximum file size in MB (DoS protection).
113
+ """
114
+ self.repo_path = repo_path.resolve()
115
+ self.max_file_size_mb = max_file_size_mb
116
+ self._prompt_builder = FixPromptBuilder(context_lines=10)
117
+ # Simple LRU cache: {file_path: file_content}
118
+ # Limited to 20 files to prevent memory bloat
119
+ self._file_cache: dict[str, str] = {}
120
+ self._cache_max_size = 20
121
+
122
+ def extract(self, file_path: str, line: int | None) -> CodeContext | None:
123
+ """Extract code context from a file.
124
+
125
+ Args:
126
+ file_path: Relative or absolute path to the file.
127
+ line: Line number (1-indexed) of the vulnerability.
128
+
129
+ Returns:
130
+ CodeContext object with code and metadata, or None if unavailable.
131
+
132
+ Security:
133
+ - Path validation (ASVS V5.3.3)
134
+ - Size limits (ASVS V10.3.3)
135
+ - Sensitive file blocking (ASVS V8.3.4)
136
+ - Error sanitization (ASVS V7.4.1)
137
+ """
138
+ if not file_path or not line:
139
+ return None
140
+
141
+ # Resolve path
142
+ try:
143
+ full_path = (self.repo_path / file_path).resolve()
144
+ except (ValueError, OSError):
145
+ # ASVS V7.4.1: Sanitized error (no full path)
146
+ logger.warning(
147
+ "code_context_path_invalid",
148
+ extra={"file_path": Path(file_path).name, "reason": "invalid_path"},
149
+ )
150
+ return CodeContext(
151
+ code="",
152
+ language="",
153
+ vulnerable_line="",
154
+ error="Invalid file path",
155
+ )
156
+
157
+ # ASVS V5.3.3: Path traversal check
158
+ if not self._validate_path(full_path):
159
+ logger.warning(
160
+ "code_context_path_traversal",
161
+ extra={"file_path": Path(file_path).name, "reason": "path_traversal"},
162
+ )
163
+ return None
164
+
165
+ # Check if file exists
166
+ if not full_path.exists():
167
+ logger.info(
168
+ "code_context_file_not_found",
169
+ extra={"file_path": Path(file_path).name},
170
+ )
171
+ return CodeContext(
172
+ code="",
173
+ language="",
174
+ vulnerable_line="",
175
+ error="File not found",
176
+ )
177
+
178
+ # ASVS V8.3.4: Sensitive file check
179
+ if self._is_sensitive_file(full_path):
180
+ logger.info(
181
+ "code_context_sensitive_file",
182
+ extra={"file_path": Path(file_path).name},
183
+ )
184
+ return CodeContext(
185
+ code="",
186
+ language="",
187
+ vulnerable_line="",
188
+ error="Code hidden (sensitive file type)",
189
+ )
190
+
191
+ # Binary file check
192
+ if not self._is_text_file(full_path):
193
+ logger.info(
194
+ "code_context_binary_file",
195
+ extra={"file_path": Path(file_path).name},
196
+ )
197
+ return None
198
+
199
+ # ASVS V10.3.3: File size check
200
+ size_mb = full_path.stat().st_size / (1024 * 1024)
201
+ if size_mb > self.max_file_size_mb:
202
+ logger.warning(
203
+ "code_context_file_too_large",
204
+ extra={"file_path": Path(file_path).name, "size_mb": f"{size_mb:.1f}"},
205
+ )
206
+ return CodeContext(
207
+ code="",
208
+ language="",
209
+ vulnerable_line="",
210
+ error=f"File too large for display ({size_mb:.1f}MB)",
211
+ )
212
+
213
+ # Read file content (with caching for performance)
214
+ cache_key = str(full_path)
215
+ if cache_key in self._file_cache:
216
+ file_content = self._file_cache[cache_key]
217
+ else:
218
+ try:
219
+ file_content = full_path.read_text(encoding="utf-8")
220
+ # Cache the content
221
+ self._file_cache[cache_key] = file_content
222
+ # Evict oldest entry if cache is full (simple FIFO)
223
+ if len(self._file_cache) > self._cache_max_size:
224
+ # Remove first (oldest) entry
225
+ oldest_key = next(iter(self._file_cache))
226
+ del self._file_cache[oldest_key]
227
+ except (OSError, UnicodeDecodeError) as e:
228
+ # ASVS V7.4.1: Sanitized error
229
+ logger.warning(
230
+ "code_context_read_error",
231
+ extra={"file_path": Path(file_path).name, "error": str(e)},
232
+ )
233
+ return CodeContext(
234
+ code="",
235
+ language="",
236
+ vulnerable_line="",
237
+ error="Cannot read file",
238
+ )
239
+
240
+ # Extract code context using existing logic from fix engine
241
+ code_context, vulnerable_line = self._prompt_builder.extract_code_context(
242
+ file_content, line
243
+ )
244
+
245
+ # Detect language for syntax highlighting
246
+ language = self._detect_language(full_path)
247
+
248
+ return CodeContext(
249
+ code=code_context,
250
+ language=language,
251
+ vulnerable_line=vulnerable_line,
252
+ error=None,
253
+ )
254
+
255
+ def _validate_path(self, path: Path) -> bool:
256
+ """Validate that path is within repo_path (prevent traversal).
257
+
258
+ Args:
259
+ path: Resolved path to validate.
260
+
261
+ Returns:
262
+ True if path is safe, False otherwise.
263
+
264
+ Security:
265
+ ASVS V5.3.3: Path validation to prevent directory traversal.
266
+ """
267
+ try:
268
+ # Check if path is within repo_path
269
+ path.relative_to(self.repo_path)
270
+ return True
271
+ except ValueError:
272
+ return False
273
+
274
+ def _is_text_file(self, path: Path) -> bool:
275
+ """Check if file is a text file (not binary).
276
+
277
+ Args:
278
+ path: Path to check.
279
+
280
+ Returns:
281
+ True if likely a text file, False if binary.
282
+ """
283
+ suffix = path.suffix.lower()
284
+ name = path.name.lower()
285
+
286
+ # Check against binary extensions
287
+ if suffix in BINARY_EXTENSIONS:
288
+ return False
289
+
290
+ # Special cases without extensions
291
+ if name in ("dockerfile", "makefile", "vagrantfile", "jenkinsfile"):
292
+ return True
293
+
294
+ return True
295
+
296
+ def _is_sensitive_file(self, path: Path) -> bool:
297
+ """Check if file contains sensitive data (secrets, keys).
298
+
299
+ Args:
300
+ path: Path to check.
301
+
302
+ Returns:
303
+ True if file is sensitive and should not be displayed.
304
+
305
+ Security:
306
+ ASVS V8.3.4: Prevent sensitive data in outputs.
307
+ """
308
+ suffix = path.suffix.lower()
309
+ name = path.name.lower()
310
+
311
+ # Check extension
312
+ if suffix in SENSITIVE_EXTENSIONS:
313
+ return True
314
+
315
+ # Check if the entire filename (including leading dot) matches sensitive patterns
316
+ # For files like .env, .pem, etc., the suffix is empty but name includes the dot
317
+ if name in {".env", ".pem", ".key", ".crt"}:
318
+ return True
319
+
320
+ # Check filename patterns
321
+ return any(
322
+ pattern in name
323
+ for pattern in [
324
+ "secret",
325
+ "credential",
326
+ "password",
327
+ "token",
328
+ "apikey",
329
+ "private_key",
330
+ "id_rsa",
331
+ "id_dsa",
332
+ "id_ecdsa",
333
+ ]
334
+ )
335
+
336
+ def _detect_language(self, path: Path) -> str:
337
+ """Detect programming language from file extension.
338
+
339
+ Args:
340
+ path: Path to the file.
341
+
342
+ Returns:
343
+ Language identifier for syntax highlighting.
344
+ """
345
+ return self._prompt_builder._detect_language(str(path))