kekkai-cli 2.2.0__tar.gz → 2.2.1__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.2.0 → kekkai_cli-2.2.1}/PKG-INFO +1 -1
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/pyproject.toml +1 -1
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/cli.py +4 -1
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/output.py +1 -1
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/triage/code_context.py +34 -10
- kekkai_cli-2.2.1/src/kekkai/triage/editor_support.py +166 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/triage/screens.py +16 -1
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_cli.egg-info/PKG-INFO +1 -1
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_cli.egg-info/SOURCES.txt +2 -0
- kekkai_cli-2.2.1/tests/test_editor_support.py +172 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_triage_code_context.py +60 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_triage_editor.py +80 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_triage_security.py +5 -3
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/README.md +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/setup.cfg +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/__init__.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/compliance/__init__.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/compliance/hipaa.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/compliance/mappings.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/compliance/owasp.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/compliance/owasp_agentic.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/compliance/pci_dss.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/compliance/soc2.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/config.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/dojo.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/dojo_import.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/fix/__init__.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/fix/audit.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/fix/differ.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/fix/engine.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/fix/prompts.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/github/__init__.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/github/commenter.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/github/models.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/github/sanitizer.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/installer/__init__.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/installer/errors.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/installer/extract.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/installer/manager.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/installer/manifest.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/installer/verify.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/manifest.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/paths.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/policy.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/report/__init__.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/report/compliance_matrix.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/report/generator.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/report/html.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/report/pdf.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/report/unified.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/runner.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/scanners/__init__.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/scanners/backends/__init__.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/scanners/backends/base.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/scanners/backends/docker.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/scanners/backends/native.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/scanners/base.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/scanners/container.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/scanners/falco.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/scanners/gitleaks.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/scanners/semgrep.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/scanners/trivy.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/scanners/url_policy.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/scanners/zap.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/threatflow/__init__.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/threatflow/artifacts.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/threatflow/chunking.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/threatflow/core.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/threatflow/mermaid.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/threatflow/model_adapter.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/threatflow/prompts.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/threatflow/redaction.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/threatflow/sanitizer.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/triage/__init__.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/triage/app.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/triage/audit.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/triage/fix_screen.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/triage/ignore.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/triage/loader.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/triage/models.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai/triage/widgets.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_cli.egg-info/dependency_links.txt +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_cli.egg-info/entry_points.txt +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_cli.egg-info/requires.txt +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_cli.egg-info/top_level.txt +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/__init__.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/ci/__init__.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/ci/benchmarks.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/ci/metadata.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/ci/validators.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/docker/__init__.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/docker/metadata.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/docker/sbom.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/docker/security.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/docker/signing.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/redaction.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/slsa/__init__.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/slsa/verify.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/windows/__init__.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/windows/chocolatey.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/windows/installer.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/windows/scoop.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/src/kekkai_core/windows/validators.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_cli_output.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_compliance.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_dojo_import.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_fix_engine.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_github_commenter_filter.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_github_commenter_format.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_github_commenter_limit.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_github_commenter_sanitize.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_installer_checksum.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_installer_extract.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_installer_manager.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_installer_manifest.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_installer_platform.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_kekkai_cli.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_kekkai_config.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_kekkai_dojo.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_kekkai_dojo_cli.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_kekkai_manifest.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_kekkai_paths.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_kekkai_runner.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_mermaid.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_policy.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_redaction.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_report.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_scanner_backends.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_scanner_base.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_scanner_container.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_scanner_digest_defaults.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_scanner_falco.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_scanner_gitleaks.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_scanner_native.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_scanner_semgrep.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_scanner_trivy.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_scanner_zap.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_slsa_provenance.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_threatflow_chunking.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_threatflow_model_adapter.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_threatflow_prompts.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_threatflow_redaction.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_threatflow_sanitizer.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_triage_audit.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_triage_ignore.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_triage_loader.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_triage_models.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_unified_report.py +0 -0
- {kekkai_cli-2.2.0 → kekkai_cli-2.2.1}/tests/test_url_policy.py +0 -0
|
@@ -1268,9 +1268,12 @@ def _command_triage(parsed: argparse.Namespace) -> int:
|
|
|
1268
1268
|
except (OSError, json.JSONDecodeError, KeyError):
|
|
1269
1269
|
pass
|
|
1270
1270
|
|
|
1271
|
-
# Fall back to current directory if still not set
|
|
1271
|
+
# Fall back to current directory if still not set (with warning)
|
|
1272
1272
|
if repo_path is None:
|
|
1273
1273
|
repo_path = Path.cwd()
|
|
1274
|
+
console.print("[warning]⚠ Repo path not detected. Using current directory.[/warning]")
|
|
1275
|
+
console.print("[dim]Tip: Use --repo to specify repository root explicitly.[/dim]")
|
|
1276
|
+
console.print(f"[dim]Current directory: {repo_path}[/dim]\n")
|
|
1274
1277
|
|
|
1275
1278
|
return run_triage(
|
|
1276
1279
|
findings=findings,
|
|
@@ -210,22 +210,38 @@ class CodeContextExtractor:
|
|
|
210
210
|
error=f"File too large for display ({size_mb:.1f}MB)",
|
|
211
211
|
)
|
|
212
212
|
|
|
213
|
-
# Read file content (with caching for performance)
|
|
213
|
+
# Read file content (with caching for performance and encoding fallback)
|
|
214
214
|
cache_key = str(full_path)
|
|
215
215
|
if cache_key in self._file_cache:
|
|
216
216
|
file_content = self._file_cache[cache_key]
|
|
217
217
|
else:
|
|
218
218
|
try:
|
|
219
|
+
# Try UTF-8 first (strict mode)
|
|
219
220
|
file_content = full_path.read_text(encoding="utf-8")
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
#
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
221
|
+
except UnicodeDecodeError:
|
|
222
|
+
# HOTFIX: Fallback to UTF-8 with replacement characters
|
|
223
|
+
# This allows viewing legacy files (Windows-1252, Latin-1) with � for invalid chars
|
|
224
|
+
# instead of completely hiding the file with "Cannot read file" error
|
|
225
|
+
logger.info(
|
|
226
|
+
"code_context_encoding_fallback",
|
|
227
|
+
extra={"file_path": Path(file_path).name, "reason": "non_utf8"},
|
|
228
|
+
)
|
|
229
|
+
try:
|
|
230
|
+
file_content = full_path.read_text(encoding="utf-8", errors="replace")
|
|
231
|
+
except (OSError, UnicodeDecodeError) as e:
|
|
232
|
+
# Even fallback failed (should be rare - permissions, etc.)
|
|
233
|
+
logger.warning(
|
|
234
|
+
"code_context_read_error",
|
|
235
|
+
extra={"file_path": Path(file_path).name, "error": str(e)},
|
|
236
|
+
)
|
|
237
|
+
return CodeContext(
|
|
238
|
+
code="",
|
|
239
|
+
language="",
|
|
240
|
+
vulnerable_line="",
|
|
241
|
+
error="Cannot read file (encoding error)",
|
|
242
|
+
)
|
|
243
|
+
except OSError as e:
|
|
244
|
+
# File read error (permissions, etc.)
|
|
229
245
|
logger.warning(
|
|
230
246
|
"code_context_read_error",
|
|
231
247
|
extra={"file_path": Path(file_path).name, "error": str(e)},
|
|
@@ -237,6 +253,14 @@ class CodeContextExtractor:
|
|
|
237
253
|
error="Cannot read file",
|
|
238
254
|
)
|
|
239
255
|
|
|
256
|
+
# Cache the content
|
|
257
|
+
self._file_cache[cache_key] = file_content
|
|
258
|
+
# Evict oldest entry if cache is full (simple FIFO)
|
|
259
|
+
if len(self._file_cache) > self._cache_max_size:
|
|
260
|
+
# Remove first (oldest) entry
|
|
261
|
+
oldest_key = next(iter(self._file_cache))
|
|
262
|
+
del self._file_cache[oldest_key]
|
|
263
|
+
|
|
240
264
|
# Extract code context using existing logic from fix engine
|
|
241
265
|
code_context, vulnerable_line = self._prompt_builder.extract_code_context(
|
|
242
266
|
file_content, line
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Editor-specific line jump syntax support.
|
|
2
|
+
|
|
3
|
+
Provides detection and command building for popular editors with
|
|
4
|
+
security validation per ASVS V5.1.3.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"EditorConfig",
|
|
15
|
+
"detect_editor_config",
|
|
16
|
+
"validate_editor_name",
|
|
17
|
+
"EDITOR_REGISTRY",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
# ASVS V5.1.3: Only allow safe characters in editor names
|
|
21
|
+
EDITOR_NAME_PATTERN = re.compile(r"^[a-zA-Z0-9/_.-]+$")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class EditorConfig:
|
|
26
|
+
"""Editor-specific command line configuration.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
name: Canonical editor name (e.g., "vim", "code", "subl").
|
|
30
|
+
syntax_type: Command syntax category for building commands.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
name: str
|
|
34
|
+
syntax_type: str # "vim", "vscode", "sublime", "notepadpp", "jetbrains"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Editor registry mapping base names to configurations
|
|
38
|
+
EDITOR_REGISTRY: dict[str, EditorConfig] = {
|
|
39
|
+
# Vim family (syntax: editor +LINE file)
|
|
40
|
+
"vim": EditorConfig("vim", "vim"),
|
|
41
|
+
"nvim": EditorConfig("nvim", "vim"),
|
|
42
|
+
"neovim": EditorConfig("neovim", "vim"),
|
|
43
|
+
"vi": EditorConfig("vi", "vim"),
|
|
44
|
+
# Emacs family (syntax: editor +LINE file)
|
|
45
|
+
"emacs": EditorConfig("emacs", "vim"),
|
|
46
|
+
"nano": EditorConfig("nano", "vim"),
|
|
47
|
+
# VS Code (syntax: code -g file:line)
|
|
48
|
+
"code": EditorConfig("code", "vscode"),
|
|
49
|
+
"code-insiders": EditorConfig("code-insiders", "vscode"),
|
|
50
|
+
"codium": EditorConfig("codium", "vscode"), # VSCodium (FOSS fork)
|
|
51
|
+
# Sublime Text (syntax: subl file:line)
|
|
52
|
+
"subl": EditorConfig("subl", "sublime"),
|
|
53
|
+
"sublime": EditorConfig("sublime", "sublime"),
|
|
54
|
+
"sublime_text": EditorConfig("sublime_text", "sublime"),
|
|
55
|
+
# Atom (syntax: atom file:line) - legacy but still used
|
|
56
|
+
"atom": EditorConfig("atom", "sublime"),
|
|
57
|
+
# Notepad++ (syntax: notepad++ -nLINE file)
|
|
58
|
+
"notepad++": EditorConfig("notepad++", "notepadpp"),
|
|
59
|
+
"notepad++.exe": EditorConfig("notepad++.exe", "notepadpp"),
|
|
60
|
+
# JetBrains IDEs (syntax: editor --line LINE file)
|
|
61
|
+
"idea": EditorConfig("idea", "jetbrains"),
|
|
62
|
+
"pycharm": EditorConfig("pycharm", "jetbrains"),
|
|
63
|
+
"webstorm": EditorConfig("webstorm", "jetbrains"),
|
|
64
|
+
"phpstorm": EditorConfig("phpstorm", "jetbrains"),
|
|
65
|
+
"goland": EditorConfig("goland", "jetbrains"),
|
|
66
|
+
"rider": EditorConfig("rider", "jetbrains"),
|
|
67
|
+
"clion": EditorConfig("clion", "jetbrains"),
|
|
68
|
+
"rubymine": EditorConfig("rubymine", "jetbrains"),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def detect_editor_config(editor_name: str) -> EditorConfig:
|
|
73
|
+
"""Detect editor configuration from name.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
editor_name: Editor executable name (e.g., "vim", "/usr/bin/code").
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
EditorConfig for the detected editor, or vim-style default if unknown.
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
>>> detect_editor_config("vim").syntax_type
|
|
83
|
+
'vim'
|
|
84
|
+
>>> detect_editor_config("/usr/local/bin/code").syntax_type
|
|
85
|
+
'vscode'
|
|
86
|
+
>>> detect_editor_config("unknown-editor").syntax_type
|
|
87
|
+
'vim'
|
|
88
|
+
"""
|
|
89
|
+
# Extract base name from path
|
|
90
|
+
base_name = Path(editor_name).stem.lower()
|
|
91
|
+
|
|
92
|
+
# Handle .exe extension on Windows
|
|
93
|
+
if base_name.endswith(".exe"):
|
|
94
|
+
base_name = base_name[:-4]
|
|
95
|
+
|
|
96
|
+
# Lookup in registry
|
|
97
|
+
config = EDITOR_REGISTRY.get(base_name)
|
|
98
|
+
if config:
|
|
99
|
+
return config
|
|
100
|
+
|
|
101
|
+
# Default to vim-style syntax for unknown editors
|
|
102
|
+
return EditorConfig("unknown", "vim")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def validate_editor_name(editor: str) -> bool:
|
|
106
|
+
"""Validate that editor name is safe to use.
|
|
107
|
+
|
|
108
|
+
Security: ASVS V5.1.3 - Validate data at trust boundaries.
|
|
109
|
+
Rejects editor names containing shell metacharacters to prevent
|
|
110
|
+
command injection attacks.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
editor: Editor name from environment variable.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
True if editor name is safe, False if it contains unsafe characters.
|
|
117
|
+
|
|
118
|
+
Examples:
|
|
119
|
+
>>> validate_editor_name("vim")
|
|
120
|
+
True
|
|
121
|
+
>>> validate_editor_name("/usr/bin/code")
|
|
122
|
+
True
|
|
123
|
+
>>> validate_editor_name("vim; curl evil.com")
|
|
124
|
+
False
|
|
125
|
+
>>> validate_editor_name("vim && rm -rf /")
|
|
126
|
+
False
|
|
127
|
+
"""
|
|
128
|
+
if not editor:
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
# Check against safe pattern (alphanumeric + /.-_ only)
|
|
132
|
+
return bool(EDITOR_NAME_PATTERN.match(editor))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def build_editor_command(
|
|
136
|
+
editor_path: str, file_path: Path, line: int, editor_config: EditorConfig
|
|
137
|
+
) -> list[str]:
|
|
138
|
+
"""Build editor command arguments based on editor type.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
editor_path: Full path to editor executable.
|
|
142
|
+
file_path: Path to file to open.
|
|
143
|
+
line: Line number to jump to.
|
|
144
|
+
editor_config: Editor configuration with syntax type.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
List of command arguments for subprocess.run().
|
|
148
|
+
|
|
149
|
+
Security:
|
|
150
|
+
ASVS V14.2.1 - Uses list args (not shell string) to prevent injection.
|
|
151
|
+
"""
|
|
152
|
+
if editor_config.syntax_type == "vscode":
|
|
153
|
+
# VS Code: code -g file:line
|
|
154
|
+
return [editor_path, "-g", f"{file_path}:{line}"]
|
|
155
|
+
elif editor_config.syntax_type == "sublime":
|
|
156
|
+
# Sublime Text / Atom: editor file:line
|
|
157
|
+
return [editor_path, f"{file_path}:{line}"]
|
|
158
|
+
elif editor_config.syntax_type == "notepadpp":
|
|
159
|
+
# Notepad++: notepad++ -nLINE file
|
|
160
|
+
return [editor_path, f"-n{line}", str(file_path)]
|
|
161
|
+
elif editor_config.syntax_type == "jetbrains":
|
|
162
|
+
# JetBrains IDEs: editor --line LINE file
|
|
163
|
+
return [editor_path, "--line", str(line), str(file_path)]
|
|
164
|
+
else:
|
|
165
|
+
# Default (Vim/Emacs/Nano): editor +LINE file
|
|
166
|
+
return [editor_path, f"+{line}", str(file_path)]
|
|
@@ -436,6 +436,8 @@ class FindingDetailScreen(Screen[None]):
|
|
|
436
436
|
import shutil
|
|
437
437
|
import subprocess
|
|
438
438
|
|
|
439
|
+
from .editor_support import build_editor_command, detect_editor_config, validate_editor_name
|
|
440
|
+
|
|
439
441
|
logger = logging.getLogger(__name__)
|
|
440
442
|
|
|
441
443
|
if not self.finding.file_path or not self.finding.line:
|
|
@@ -448,6 +450,15 @@ class FindingDetailScreen(Screen[None]):
|
|
|
448
450
|
# Get editor from environment (ASVS V5.1.3: validate before use)
|
|
449
451
|
editor = os.environ.get("EDITOR", "vim")
|
|
450
452
|
|
|
453
|
+
# ASVS V5.1.3: Validate EDITOR value before use (reject shell metacharacters)
|
|
454
|
+
if not validate_editor_name(editor):
|
|
455
|
+
self.notify(
|
|
456
|
+
f"Editor '{editor}' contains unsafe characters. Set a valid $EDITOR.",
|
|
457
|
+
severity="error",
|
|
458
|
+
)
|
|
459
|
+
logger.warning("unsafe_editor_value", extra={"editor": editor})
|
|
460
|
+
return
|
|
461
|
+
|
|
451
462
|
# Security validation: check editor exists and is executable
|
|
452
463
|
editor_path = shutil.which(editor)
|
|
453
464
|
if not editor_path:
|
|
@@ -463,18 +474,22 @@ class FindingDetailScreen(Screen[None]):
|
|
|
463
474
|
self.notify(f"File not found: {self.finding.file_path}", severity="error")
|
|
464
475
|
return
|
|
465
476
|
|
|
477
|
+
# Detect editor type and build appropriate command
|
|
478
|
+
editor_config = detect_editor_config(editor)
|
|
479
|
+
|
|
466
480
|
# Log editor invocation (ASVS V16.7.1)
|
|
467
481
|
logger.info(
|
|
468
482
|
"editor_opened",
|
|
469
483
|
extra={
|
|
470
484
|
"editor": editor,
|
|
485
|
+
"editor_type": editor_config.syntax_type,
|
|
471
486
|
"file": self.finding.file_path,
|
|
472
487
|
"line": self.finding.line,
|
|
473
488
|
},
|
|
474
489
|
)
|
|
475
490
|
|
|
476
491
|
# ASVS V14.2.1: Use list args (not shell=True) to prevent injection
|
|
477
|
-
cmd =
|
|
492
|
+
cmd = build_editor_command(editor_path, file_path, self.finding.line, editor_config)
|
|
478
493
|
|
|
479
494
|
try:
|
|
480
495
|
# Suspend TUI, run editor, then resume
|
|
@@ -64,6 +64,7 @@ src/kekkai/triage/__init__.py
|
|
|
64
64
|
src/kekkai/triage/app.py
|
|
65
65
|
src/kekkai/triage/audit.py
|
|
66
66
|
src/kekkai/triage/code_context.py
|
|
67
|
+
src/kekkai/triage/editor_support.py
|
|
67
68
|
src/kekkai/triage/fix_screen.py
|
|
68
69
|
src/kekkai/triage/ignore.py
|
|
69
70
|
src/kekkai/triage/loader.py
|
|
@@ -97,6 +98,7 @@ src/kekkai_core/windows/validators.py
|
|
|
97
98
|
tests/test_cli_output.py
|
|
98
99
|
tests/test_compliance.py
|
|
99
100
|
tests/test_dojo_import.py
|
|
101
|
+
tests/test_editor_support.py
|
|
100
102
|
tests/test_fix_engine.py
|
|
101
103
|
tests/test_github_commenter_filter.py
|
|
102
104
|
tests/test_github_commenter_format.py
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Unit tests for editor support module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from kekkai.triage.editor_support import (
|
|
8
|
+
EditorConfig,
|
|
9
|
+
build_editor_command,
|
|
10
|
+
detect_editor_config,
|
|
11
|
+
validate_editor_name,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestEditorDetection:
|
|
16
|
+
"""Tests for editor detection and configuration."""
|
|
17
|
+
|
|
18
|
+
def test_detect_vim_family(self) -> None:
|
|
19
|
+
"""Test detection of Vim family editors."""
|
|
20
|
+
for editor in ["vim", "nvim", "neovim", "vi"]:
|
|
21
|
+
config = detect_editor_config(editor)
|
|
22
|
+
assert config.syntax_type == "vim"
|
|
23
|
+
assert config.name in ["vim", "nvim", "neovim", "vi"]
|
|
24
|
+
|
|
25
|
+
def test_detect_emacs_family(self) -> None:
|
|
26
|
+
"""Test detection of Emacs family editors."""
|
|
27
|
+
for editor in ["emacs", "nano"]:
|
|
28
|
+
config = detect_editor_config(editor)
|
|
29
|
+
assert config.syntax_type == "vim" # Use vim-style syntax
|
|
30
|
+
|
|
31
|
+
def test_detect_vscode(self) -> None:
|
|
32
|
+
"""Test detection of VS Code variants."""
|
|
33
|
+
for editor in ["code", "code-insiders", "codium"]:
|
|
34
|
+
config = detect_editor_config(editor)
|
|
35
|
+
assert config.syntax_type == "vscode"
|
|
36
|
+
|
|
37
|
+
def test_detect_vscode_with_path(self) -> None:
|
|
38
|
+
"""Test detection works with full paths."""
|
|
39
|
+
config = detect_editor_config("/usr/local/bin/code")
|
|
40
|
+
assert config.syntax_type == "vscode"
|
|
41
|
+
assert config.name == "code"
|
|
42
|
+
|
|
43
|
+
def test_detect_sublime(self) -> None:
|
|
44
|
+
"""Test detection of Sublime Text variants."""
|
|
45
|
+
for editor in ["subl", "sublime", "sublime_text"]:
|
|
46
|
+
config = detect_editor_config(editor)
|
|
47
|
+
assert config.syntax_type == "sublime"
|
|
48
|
+
|
|
49
|
+
def test_detect_atom(self) -> None:
|
|
50
|
+
"""Test detection of Atom editor."""
|
|
51
|
+
config = detect_editor_config("atom")
|
|
52
|
+
assert config.syntax_type == "sublime" # Uses same syntax as Sublime
|
|
53
|
+
|
|
54
|
+
def test_detect_notepadpp(self) -> None:
|
|
55
|
+
"""Test detection of Notepad++."""
|
|
56
|
+
for editor in ["notepad++", "notepad++.exe"]:
|
|
57
|
+
config = detect_editor_config(editor)
|
|
58
|
+
assert config.syntax_type == "notepadpp"
|
|
59
|
+
|
|
60
|
+
def test_detect_jetbrains(self) -> None:
|
|
61
|
+
"""Test detection of JetBrains IDEs."""
|
|
62
|
+
for editor in ["idea", "pycharm", "webstorm", "phpstorm", "goland", "rider"]:
|
|
63
|
+
config = detect_editor_config(editor)
|
|
64
|
+
assert config.syntax_type == "jetbrains"
|
|
65
|
+
|
|
66
|
+
def test_unknown_editor_fallback(self) -> None:
|
|
67
|
+
"""Test that unknown editors fall back to vim syntax."""
|
|
68
|
+
config = detect_editor_config("unknown-editor")
|
|
69
|
+
assert config.syntax_type == "vim"
|
|
70
|
+
assert config.name == "unknown"
|
|
71
|
+
|
|
72
|
+
def test_case_insensitive_detection(self) -> None:
|
|
73
|
+
"""Test that editor detection is case-insensitive."""
|
|
74
|
+
config = detect_editor_config("CODE")
|
|
75
|
+
assert config.syntax_type == "vscode"
|
|
76
|
+
|
|
77
|
+
config = detect_editor_config("VIM")
|
|
78
|
+
assert config.syntax_type == "vim"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class TestEditorValidation:
|
|
82
|
+
"""Tests for editor name validation (ASVS V5.1.3)."""
|
|
83
|
+
|
|
84
|
+
def test_validate_safe_names(self) -> None:
|
|
85
|
+
"""Test that safe editor names are accepted."""
|
|
86
|
+
safe_names = [
|
|
87
|
+
"vim",
|
|
88
|
+
"code",
|
|
89
|
+
"nvim",
|
|
90
|
+
"/usr/bin/vim",
|
|
91
|
+
"/usr/local/bin/code",
|
|
92
|
+
"editor-name",
|
|
93
|
+
"editor.name",
|
|
94
|
+
"editor_name",
|
|
95
|
+
]
|
|
96
|
+
for name in safe_names:
|
|
97
|
+
assert validate_editor_name(name), f"Should accept: {name}"
|
|
98
|
+
|
|
99
|
+
def test_validate_reject_shell_metacharacters(self) -> None:
|
|
100
|
+
"""Test that editor names with shell metacharacters are rejected."""
|
|
101
|
+
unsafe_names = [
|
|
102
|
+
"vim; curl evil.com",
|
|
103
|
+
"vim && rm -rf /",
|
|
104
|
+
"vim | cat /etc/passwd",
|
|
105
|
+
"vim $(whoami)",
|
|
106
|
+
"vim `whoami`",
|
|
107
|
+
"vim & curl",
|
|
108
|
+
"vim > /tmp/evil",
|
|
109
|
+
"vim < /etc/passwd",
|
|
110
|
+
]
|
|
111
|
+
for name in unsafe_names:
|
|
112
|
+
assert not validate_editor_name(name), f"Should reject: {name}"
|
|
113
|
+
|
|
114
|
+
def test_validate_reject_empty(self) -> None:
|
|
115
|
+
"""Test that empty editor name is rejected."""
|
|
116
|
+
assert not validate_editor_name("")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TestCommandBuilding:
|
|
120
|
+
"""Tests for editor command building."""
|
|
121
|
+
|
|
122
|
+
def test_build_vim_command(self) -> None:
|
|
123
|
+
"""Test building command for Vim-style editors."""
|
|
124
|
+
config = EditorConfig("vim", "vim")
|
|
125
|
+
cmd = build_editor_command("/usr/bin/vim", Path("/repo/file.py"), 42, config)
|
|
126
|
+
|
|
127
|
+
assert cmd == ["/usr/bin/vim", "+42", "/repo/file.py"]
|
|
128
|
+
|
|
129
|
+
def test_build_vscode_command(self) -> None:
|
|
130
|
+
"""Test building command for VS Code."""
|
|
131
|
+
config = EditorConfig("code", "vscode")
|
|
132
|
+
cmd = build_editor_command("/usr/bin/code", Path("/repo/file.py"), 42, config)
|
|
133
|
+
|
|
134
|
+
assert cmd == ["/usr/bin/code", "-g", "/repo/file.py:42"]
|
|
135
|
+
|
|
136
|
+
def test_build_sublime_command(self) -> None:
|
|
137
|
+
"""Test building command for Sublime Text."""
|
|
138
|
+
config = EditorConfig("subl", "sublime")
|
|
139
|
+
cmd = build_editor_command("/usr/bin/subl", Path("/repo/file.py"), 42, config)
|
|
140
|
+
|
|
141
|
+
assert cmd == ["/usr/bin/subl", "/repo/file.py:42"]
|
|
142
|
+
|
|
143
|
+
def test_build_notepadpp_command(self) -> None:
|
|
144
|
+
"""Test building command for Notepad++."""
|
|
145
|
+
config = EditorConfig("notepad++", "notepadpp")
|
|
146
|
+
cmd = build_editor_command(
|
|
147
|
+
"C:\\Program Files\\Notepad++\\notepad++.exe", Path("C:\\repo\\file.py"), 42, config
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
assert cmd == ["C:\\Program Files\\Notepad++\\notepad++.exe", "-n42", "C:\\repo\\file.py"]
|
|
151
|
+
|
|
152
|
+
def test_build_jetbrains_command(self) -> None:
|
|
153
|
+
"""Test building command for JetBrains IDEs."""
|
|
154
|
+
config = EditorConfig("pycharm", "jetbrains")
|
|
155
|
+
cmd = build_editor_command("/usr/bin/pycharm", Path("/repo/file.py"), 42, config)
|
|
156
|
+
|
|
157
|
+
assert cmd == ["/usr/bin/pycharm", "--line", "42", "/repo/file.py"]
|
|
158
|
+
|
|
159
|
+
def test_command_uses_list_args(self) -> None:
|
|
160
|
+
"""Test that all commands use list args (not shell strings) per ASVS V14.2.1."""
|
|
161
|
+
editors = [
|
|
162
|
+
EditorConfig("vim", "vim"),
|
|
163
|
+
EditorConfig("code", "vscode"),
|
|
164
|
+
EditorConfig("subl", "sublime"),
|
|
165
|
+
EditorConfig("notepad++", "notepadpp"),
|
|
166
|
+
EditorConfig("idea", "jetbrains"),
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
for editor in editors:
|
|
170
|
+
cmd = build_editor_command("/usr/bin/editor", Path("/file.py"), 10, editor)
|
|
171
|
+
assert isinstance(cmd, list), f"Command should be list for {editor.name}"
|
|
172
|
+
assert all(isinstance(arg, str) for arg in cmd), "All args should be strings"
|
|
@@ -271,3 +271,63 @@ class TestCodeContextExtractor:
|
|
|
271
271
|
"""Test _validate_path rejects paths outside repo."""
|
|
272
272
|
outside_path = (tmp_path / "outside.py").resolve()
|
|
273
273
|
assert extractor._validate_path(outside_path) is False
|
|
274
|
+
|
|
275
|
+
def test_extract_windows1252_file(
|
|
276
|
+
self, temp_repo: Path, extractor: CodeContextExtractor
|
|
277
|
+
) -> None:
|
|
278
|
+
"""Test extraction of Windows-1252 encoded file with smart quotes."""
|
|
279
|
+
# Create file with Windows-1252 encoding (smart quotes)
|
|
280
|
+
# \x93 and \x94 are left and right double quotes in Windows-1252
|
|
281
|
+
windows_file = temp_repo / "legacy.txt"
|
|
282
|
+
windows_file.write_bytes(b"Line 1\nSmart quote: \x93test\x94\nLine 3\n")
|
|
283
|
+
|
|
284
|
+
context = extractor.extract("legacy.txt", 2)
|
|
285
|
+
|
|
286
|
+
# Should succeed with replacement characters
|
|
287
|
+
assert context is not None
|
|
288
|
+
assert context.error is None
|
|
289
|
+
# Replacement character � (U+FFFD) should appear for invalid UTF-8 bytes
|
|
290
|
+
assert "�" in context.code or "test" in context.code
|
|
291
|
+
|
|
292
|
+
def test_extract_latin1_file(self, temp_repo: Path, extractor: CodeContextExtractor) -> None:
|
|
293
|
+
"""Test extraction of Latin-1 encoded file."""
|
|
294
|
+
# Create file with Latin-1 encoding (accented characters)
|
|
295
|
+
latin1_file = temp_repo / "spanish.txt"
|
|
296
|
+
# é in Latin-1 is \xe9, ñ is \xf1
|
|
297
|
+
latin1_file.write_bytes(b"Line 1\ncaf\xe9 espa\xf1ol\nLine 3\n")
|
|
298
|
+
|
|
299
|
+
context = extractor.extract("spanish.txt", 2)
|
|
300
|
+
|
|
301
|
+
# Should succeed with replacement characters
|
|
302
|
+
assert context is not None
|
|
303
|
+
assert context.error is None
|
|
304
|
+
# Either shows replacement chars or the text (depending on encoding luck)
|
|
305
|
+
assert context.code
|
|
306
|
+
|
|
307
|
+
def test_extract_utf8_still_works(
|
|
308
|
+
self, temp_repo: Path, extractor: CodeContextExtractor
|
|
309
|
+
) -> None:
|
|
310
|
+
"""Test that UTF-8 files still work correctly after adding fallback."""
|
|
311
|
+
# Create proper UTF-8 file with emoji
|
|
312
|
+
utf8_file = temp_repo / "modern.py"
|
|
313
|
+
utf8_file.write_text("# Line 1\n# Test 🔥 emoji\n# Line 3\n", encoding="utf-8")
|
|
314
|
+
|
|
315
|
+
context = extractor.extract("modern.py", 2)
|
|
316
|
+
|
|
317
|
+
# Should succeed without fallback
|
|
318
|
+
assert context is not None
|
|
319
|
+
assert context.error is None
|
|
320
|
+
assert "🔥" in context.code or "emoji" in context.code
|
|
321
|
+
|
|
322
|
+
def test_extract_binary_still_skipped(
|
|
323
|
+
self, temp_repo: Path, extractor: CodeContextExtractor
|
|
324
|
+
) -> None:
|
|
325
|
+
"""Test that binary files are still skipped after adding encoding fallback."""
|
|
326
|
+
# Create binary file
|
|
327
|
+
binary_file = temp_repo / "binary.pyc"
|
|
328
|
+
binary_file.write_bytes(b"\x00\x00\x00\x00\xffbinary")
|
|
329
|
+
|
|
330
|
+
context = extractor.extract("binary.pyc", 1)
|
|
331
|
+
|
|
332
|
+
# Should be skipped (return None for binary)
|
|
333
|
+
assert context is None
|
|
@@ -87,6 +87,86 @@ class TestEditorIntegration:
|
|
|
87
87
|
assert "+42" in call_args
|
|
88
88
|
assert "src/app.py" in str(call_args[2])
|
|
89
89
|
|
|
90
|
+
def test_editor_vscode_syntax(self, sample_finding: FindingEntry, tmp_path: Path) -> None:
|
|
91
|
+
"""Test VS Code uses correct -g file:line syntax."""
|
|
92
|
+
# Create the file
|
|
93
|
+
(tmp_path / "src").mkdir()
|
|
94
|
+
(tmp_path / "src" / "app.py").write_text("print('test')\n")
|
|
95
|
+
|
|
96
|
+
with patch.dict(os.environ, {"EDITOR": "code"}):
|
|
97
|
+
with patch("shutil.which", return_value="/usr/bin/code"):
|
|
98
|
+
with patch("subprocess.run") as mock_run:
|
|
99
|
+
with patch("kekkai.triage.screens.FindingDetailScreen.app") as mock_app:
|
|
100
|
+
mock_app.suspend.return_value.__enter__ = MagicMock()
|
|
101
|
+
mock_app.suspend.return_value.__exit__ = MagicMock(return_value=False)
|
|
102
|
+
|
|
103
|
+
screen = FindingDetailScreen(
|
|
104
|
+
finding=sample_finding,
|
|
105
|
+
repo_path=tmp_path,
|
|
106
|
+
)
|
|
107
|
+
screen.action_open_in_editor()
|
|
108
|
+
|
|
109
|
+
# Verify VS Code syntax: code -g file:line
|
|
110
|
+
assert mock_run.called
|
|
111
|
+
call_args = mock_run.call_args[0][0]
|
|
112
|
+
assert call_args[0] == "/usr/bin/code"
|
|
113
|
+
assert call_args[1] == "-g"
|
|
114
|
+
assert ":42" in call_args[2]
|
|
115
|
+
assert "src/app.py" in call_args[2]
|
|
116
|
+
|
|
117
|
+
def test_editor_sublime_syntax(self, sample_finding: FindingEntry, tmp_path: Path) -> None:
|
|
118
|
+
"""Test Sublime Text uses correct file:line syntax."""
|
|
119
|
+
# Create the file
|
|
120
|
+
(tmp_path / "src").mkdir()
|
|
121
|
+
(tmp_path / "src" / "app.py").write_text("print('test')\n")
|
|
122
|
+
|
|
123
|
+
with patch.dict(os.environ, {"EDITOR": "subl"}):
|
|
124
|
+
with patch("shutil.which", return_value="/usr/bin/subl"):
|
|
125
|
+
with patch("subprocess.run") as mock_run:
|
|
126
|
+
with patch("kekkai.triage.screens.FindingDetailScreen.app") as mock_app:
|
|
127
|
+
mock_app.suspend.return_value.__enter__ = MagicMock()
|
|
128
|
+
mock_app.suspend.return_value.__exit__ = MagicMock(return_value=False)
|
|
129
|
+
|
|
130
|
+
screen = FindingDetailScreen(
|
|
131
|
+
finding=sample_finding,
|
|
132
|
+
repo_path=tmp_path,
|
|
133
|
+
)
|
|
134
|
+
screen.action_open_in_editor()
|
|
135
|
+
|
|
136
|
+
# Verify Sublime syntax: subl file:line
|
|
137
|
+
assert mock_run.called
|
|
138
|
+
call_args = mock_run.call_args[0][0]
|
|
139
|
+
assert call_args[0] == "/usr/bin/subl"
|
|
140
|
+
assert ":42" in call_args[1]
|
|
141
|
+
assert "src/app.py" in call_args[1]
|
|
142
|
+
|
|
143
|
+
def test_editor_unsafe_rejected(self, sample_finding: FindingEntry, tmp_path: Path) -> None:
|
|
144
|
+
"""Test that unsafe EDITOR values are rejected (ASVS V5.1.3)."""
|
|
145
|
+
# Create the file
|
|
146
|
+
(tmp_path / "src").mkdir()
|
|
147
|
+
(tmp_path / "src" / "app.py").write_text("print('test')\n")
|
|
148
|
+
|
|
149
|
+
unsafe_editors = [
|
|
150
|
+
"vim; curl evil.com",
|
|
151
|
+
"vim && rm -rf /",
|
|
152
|
+
"vim | cat /etc/passwd",
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
for unsafe_editor in unsafe_editors:
|
|
156
|
+
with patch.dict(os.environ, {"EDITOR": unsafe_editor}):
|
|
157
|
+
screen = FindingDetailScreen(
|
|
158
|
+
finding=sample_finding,
|
|
159
|
+
repo_path=tmp_path,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
with patch.object(screen, "notify") as mock_notify:
|
|
163
|
+
screen.action_open_in_editor()
|
|
164
|
+
|
|
165
|
+
# Should call notify with error about unsafe characters
|
|
166
|
+
mock_notify.assert_called_once()
|
|
167
|
+
args = mock_notify.call_args[0]
|
|
168
|
+
assert "unsafe" in args[0].lower() or "invalid" in args[0].lower()
|
|
169
|
+
|
|
90
170
|
def test_editor_handles_missing_file(
|
|
91
171
|
self, sample_finding: FindingEntry, tmp_path: Path
|
|
92
172
|
) -> None:
|