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 +4 -1
- kekkai/output.py +1 -1
- kekkai/triage/code_context.py +34 -10
- kekkai/triage/editor_support.py +166 -0
- kekkai/triage/screens.py +16 -1
- {kekkai_cli-2.2.0.dist-info → kekkai_cli-2.2.1.dist-info}/METADATA +1 -1
- {kekkai_cli-2.2.0.dist-info → kekkai_cli-2.2.1.dist-info}/RECORD +10 -9
- {kekkai_cli-2.2.0.dist-info → kekkai_cli-2.2.1.dist-info}/WHEEL +0 -0
- {kekkai_cli-2.2.0.dist-info → kekkai_cli-2.2.1.dist-info}/entry_points.txt +0 -0
- {kekkai_cli-2.2.0.dist-info → kekkai_cli-2.2.1.dist-info}/top_level.txt +0 -0
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
kekkai/triage/code_context.py
CHANGED
|
@@ -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)]
|
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 =
|
|
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,10 +1,10 @@
|
|
|
1
1
|
kekkai/__init__.py,sha256=_VrBvJRyqHiXs31S8HOhATk_O2iy-ac0_9X7rHH75j8,143
|
|
2
|
-
kekkai/cli.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
90
|
-
kekkai_cli-2.2.
|
|
91
|
-
kekkai_cli-2.2.
|
|
92
|
-
kekkai_cli-2.2.
|
|
93
|
-
kekkai_cli-2.2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|