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.
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/PKG-INFO +19 -30
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/README.md +18 -29
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/pyproject.toml +2 -2
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/cli.py +46 -1
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/output.py +1 -1
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/__init__.py +6 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/app.py +12 -0
- kekkai_cli-2.2.0/src/kekkai/triage/code_context.py +345 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/fix_screen.py +34 -4
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/screens.py +225 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_cli.egg-info/PKG-INFO +19 -30
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_cli.egg-info/SOURCES.txt +4 -0
- kekkai_cli-2.2.0/tests/test_triage_code_context.py +273 -0
- kekkai_cli-2.2.0/tests/test_triage_editor.py +328 -0
- kekkai_cli-2.2.0/tests/test_triage_security.py +192 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/setup.cfg +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/__init__.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/compliance/__init__.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/compliance/hipaa.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/compliance/mappings.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/compliance/owasp.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/compliance/owasp_agentic.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/compliance/pci_dss.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/compliance/soc2.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/config.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/dojo.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/dojo_import.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/fix/__init__.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/fix/audit.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/fix/differ.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/fix/engine.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/fix/prompts.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/github/__init__.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/github/commenter.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/github/models.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/github/sanitizer.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/installer/__init__.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/installer/errors.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/installer/extract.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/installer/manager.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/installer/manifest.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/installer/verify.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/manifest.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/paths.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/policy.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/report/__init__.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/report/compliance_matrix.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/report/generator.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/report/html.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/report/pdf.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/report/unified.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/runner.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/__init__.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/backends/__init__.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/backends/base.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/backends/docker.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/backends/native.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/base.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/container.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/falco.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/gitleaks.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/semgrep.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/trivy.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/url_policy.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/scanners/zap.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/__init__.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/artifacts.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/chunking.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/core.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/mermaid.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/model_adapter.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/prompts.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/redaction.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/threatflow/sanitizer.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/audit.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/ignore.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/loader.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/models.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai/triage/widgets.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_cli.egg-info/dependency_links.txt +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_cli.egg-info/entry_points.txt +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_cli.egg-info/requires.txt +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_cli.egg-info/top_level.txt +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/__init__.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/ci/__init__.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/ci/benchmarks.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/ci/metadata.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/ci/validators.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/docker/__init__.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/docker/metadata.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/docker/sbom.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/docker/security.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/docker/signing.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/redaction.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/slsa/__init__.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/slsa/verify.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/windows/__init__.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/windows/chocolatey.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/windows/installer.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/windows/scoop.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/src/kekkai_core/windows/validators.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_cli_output.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_compliance.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_dojo_import.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_fix_engine.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_github_commenter_filter.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_github_commenter_format.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_github_commenter_limit.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_github_commenter_sanitize.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_installer_checksum.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_installer_extract.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_installer_manager.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_installer_manifest.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_installer_platform.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_kekkai_cli.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_kekkai_config.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_kekkai_dojo.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_kekkai_dojo_cli.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_kekkai_manifest.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_kekkai_paths.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_kekkai_runner.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_mermaid.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_policy.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_redaction.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_report.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_backends.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_base.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_container.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_digest_defaults.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_falco.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_gitleaks.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_native.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_semgrep.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_trivy.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_scanner_zap.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_slsa_provenance.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_threatflow_chunking.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_threatflow_model_adapter.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_threatflow_prompts.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_threatflow_redaction.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_threatflow_sanitizer.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_triage_audit.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_triage_ignore.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_triage_loader.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_triage_models.py +0 -0
- {kekkai_cli-2.0.1 → kekkai_cli-2.2.0}/tests/test_unified_report.py +0 -0
- {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
|
|
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>
|
|
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
|
|
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
|
-
**
|
|
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
|
|
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
|
-
|
|
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>
|
|
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
|
|
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
|
-
**
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
@@ -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))
|