tweek 0.4.0__py3-none-any.whl → 0.4.2__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.
- tweek/__init__.py +1 -1
- tweek/cli_core.py +23 -6
- tweek/cli_install.py +361 -91
- tweek/cli_uninstall.py +119 -36
- tweek/config/families.yaml +13 -0
- tweek/config/models.py +31 -3
- tweek/config/patterns.yaml +126 -2
- tweek/diagnostics.py +124 -1
- tweek/hooks/break_glass.py +70 -47
- tweek/hooks/overrides.py +19 -1
- tweek/hooks/post_tool_use.py +6 -2
- tweek/hooks/pre_tool_use.py +19 -2
- tweek/hooks/wrapper_post_tool_use.py +121 -0
- tweek/hooks/wrapper_pre_tool_use.py +121 -0
- tweek/integrations/openclaw.py +70 -60
- tweek/integrations/openclaw_detection.py +140 -0
- tweek/integrations/openclaw_server.py +359 -86
- tweek/logging/security_log.py +22 -0
- tweek/memory/safety.py +7 -3
- tweek/memory/store.py +31 -10
- tweek/plugins/base.py +9 -1
- tweek/plugins/detectors/openclaw.py +31 -92
- tweek/plugins/screening/heuristic_scorer.py +12 -1
- tweek/plugins/screening/local_model_reviewer.py +9 -0
- tweek/security/language.py +2 -1
- tweek/security/llm_reviewer.py +53 -24
- tweek/security/local_model.py +21 -0
- tweek/security/model_registry.py +2 -2
- tweek/security/rate_limiter.py +99 -1
- tweek/skills/guard.py +30 -7
- {tweek-0.4.0.dist-info → tweek-0.4.2.dist-info}/METADATA +1 -1
- {tweek-0.4.0.dist-info → tweek-0.4.2.dist-info}/RECORD +37 -34
- {tweek-0.4.0.dist-info → tweek-0.4.2.dist-info}/WHEEL +0 -0
- {tweek-0.4.0.dist-info → tweek-0.4.2.dist-info}/entry_points.txt +0 -0
- {tweek-0.4.0.dist-info → tweek-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {tweek-0.4.0.dist-info → tweek-0.4.2.dist-info}/licenses/NOTICE +0 -0
- {tweek-0.4.0.dist-info → tweek-0.4.2.dist-info}/top_level.txt +0 -0
tweek/cli_uninstall.py
CHANGED
|
@@ -7,17 +7,12 @@ Full removal of Tweek from the system:
|
|
|
7
7
|
tweek uninstall --all Remove ALL Tweek data system-wide
|
|
8
8
|
tweek uninstall --all --confirm Remove everything without prompts
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
To manually clean up orphaned hooks, remove the ``PreToolUse`` and
|
|
18
|
-
``PostToolUse`` entries that reference ``tweek`` from:
|
|
19
|
-
~/.claude/settings.json (global hooks)
|
|
20
|
-
<project>/.claude/settings.json (project-level hooks)
|
|
10
|
+
Resilience against ``pip uninstall tweek`` running first:
|
|
11
|
+
- Hooks in settings.json point to self-healing wrappers at ~/.tweek/hooks/
|
|
12
|
+
(outside the pip package). If tweek is gone, the wrappers silently remove
|
|
13
|
+
themselves from settings.json and allow the tool call.
|
|
14
|
+
- A standalone cleanup script at ~/.tweek/uninstall.sh can remove all
|
|
15
|
+
Tweek state without requiring the Python package.
|
|
21
16
|
"""
|
|
22
17
|
from __future__ import annotations
|
|
23
18
|
|
|
@@ -289,6 +284,8 @@ def _remove_tweek_data_dir(tweek_dir: Path) -> list:
|
|
|
289
284
|
("config.yaml", "configuration"),
|
|
290
285
|
("overrides.yaml", "security overrides"),
|
|
291
286
|
("security.db", "security log database"),
|
|
287
|
+
("uninstall.sh", "standalone uninstall script"),
|
|
288
|
+
("installed_scopes.json", "installation scope tracking"),
|
|
292
289
|
]
|
|
293
290
|
for filename, label in items:
|
|
294
291
|
filepath = tweek_dir / filename
|
|
@@ -297,6 +294,7 @@ def _remove_tweek_data_dir(tweek_dir: Path) -> list:
|
|
|
297
294
|
removed.append(label)
|
|
298
295
|
|
|
299
296
|
dirs = [
|
|
297
|
+
("hooks", "self-healing hook wrappers"),
|
|
300
298
|
("patterns", "pattern repository"),
|
|
301
299
|
("chamber", "skill isolation chamber"),
|
|
302
300
|
("jail", "skill jail"),
|
|
@@ -329,6 +327,40 @@ def _remove_tweek_data_dir(tweek_dir: Path) -> list:
|
|
|
329
327
|
return removed
|
|
330
328
|
|
|
331
329
|
|
|
330
|
+
def _remove_tweek_yaml_files(tweek_dir: Path) -> list:
|
|
331
|
+
"""Remove .tweek.yaml control files. Returns list of paths removed."""
|
|
332
|
+
removed = []
|
|
333
|
+
|
|
334
|
+
# Global .tweek.yaml
|
|
335
|
+
global_yaml = Path("~/.tweek.yaml").expanduser()
|
|
336
|
+
if global_yaml.exists():
|
|
337
|
+
global_yaml.unlink()
|
|
338
|
+
removed.append(str(global_yaml))
|
|
339
|
+
|
|
340
|
+
# Project-level .tweek.yaml files from recorded scopes
|
|
341
|
+
scopes_file = tweek_dir / "installed_scopes.json"
|
|
342
|
+
if scopes_file.exists():
|
|
343
|
+
try:
|
|
344
|
+
scopes = json.loads(scopes_file.read_text()) or []
|
|
345
|
+
for scope_str in scopes:
|
|
346
|
+
# .tweek.yaml is in the parent of the .claude/ directory
|
|
347
|
+
scope_parent = Path(scope_str).parent
|
|
348
|
+
project_yaml = scope_parent / ".tweek.yaml"
|
|
349
|
+
if project_yaml.exists():
|
|
350
|
+
project_yaml.unlink()
|
|
351
|
+
removed.append(str(project_yaml))
|
|
352
|
+
except (json.JSONDecodeError, IOError):
|
|
353
|
+
pass
|
|
354
|
+
|
|
355
|
+
# Current directory .tweek.yaml
|
|
356
|
+
cwd_yaml = Path.cwd() / ".tweek.yaml"
|
|
357
|
+
if cwd_yaml.exists() and str(cwd_yaml) not in removed:
|
|
358
|
+
cwd_yaml.unlink()
|
|
359
|
+
removed.append(str(cwd_yaml))
|
|
360
|
+
|
|
361
|
+
return removed
|
|
362
|
+
|
|
363
|
+
|
|
332
364
|
def _remove_mcp_integrations() -> list:
|
|
333
365
|
"""Remove MCP integrations for all known clients. Returns list of clients removed."""
|
|
334
366
|
removed = []
|
|
@@ -460,14 +492,52 @@ def _uninstall_scope(target: Path, tweek_dir: Path, confirm: bool, scope_label:
|
|
|
460
492
|
console.print("[white]Tweek data directory (~/.tweek/) was preserved.[/white]")
|
|
461
493
|
|
|
462
494
|
|
|
495
|
+
def _get_all_project_scopes(project_target: Path) -> list:
|
|
496
|
+
"""Get all project-level .claude/ directories that may have tweek hooks.
|
|
497
|
+
|
|
498
|
+
Merges: (1) the current project, (2) any recorded install scopes from
|
|
499
|
+
~/.tweek/installed_scopes.json. Deduplicates by resolved path.
|
|
500
|
+
"""
|
|
501
|
+
seen = set()
|
|
502
|
+
result = []
|
|
503
|
+
|
|
504
|
+
# Always include current project
|
|
505
|
+
resolved_cwd = str(project_target.resolve())
|
|
506
|
+
seen.add(resolved_cwd)
|
|
507
|
+
result.append(project_target)
|
|
508
|
+
|
|
509
|
+
# Include all recorded scopes from install-time tracking
|
|
510
|
+
try:
|
|
511
|
+
from tweek.cli_install import _get_installed_scopes
|
|
512
|
+
for scope_str in _get_installed_scopes():
|
|
513
|
+
scope = Path(scope_str)
|
|
514
|
+
resolved = str(scope.resolve())
|
|
515
|
+
if resolved not in seen and scope.exists():
|
|
516
|
+
seen.add(resolved)
|
|
517
|
+
result.append(scope)
|
|
518
|
+
except (ImportError, Exception):
|
|
519
|
+
pass
|
|
520
|
+
|
|
521
|
+
return result
|
|
522
|
+
|
|
523
|
+
|
|
463
524
|
def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir: Path, confirm: bool):
|
|
464
525
|
"""Full system removal of all Tweek data."""
|
|
526
|
+
# Discover all project scopes (current + recorded from install)
|
|
527
|
+
all_project_scopes = _get_all_project_scopes(project_target)
|
|
528
|
+
|
|
465
529
|
console.print("[bold yellow]FULL REMOVAL[/bold yellow] \u2014 This will remove ALL Tweek data:\n")
|
|
466
|
-
|
|
530
|
+
if len(all_project_scopes) > 1:
|
|
531
|
+
console.print(f" [white]\u2022[/white] Hooks from {len(all_project_scopes)} project(s):")
|
|
532
|
+
for scope in all_project_scopes:
|
|
533
|
+
console.print(f" [white]{scope}/settings.json[/white]")
|
|
534
|
+
else:
|
|
535
|
+
console.print(" [white]\u2022[/white] Hooks from current project (.claude/settings.json)")
|
|
467
536
|
console.print(" [white]\u2022[/white] Hooks from global installation (~/.claude/settings.json)")
|
|
468
537
|
console.print(" [white]\u2022[/white] Tweek skill directories (project + global)")
|
|
469
538
|
console.print(" [white]\u2022[/white] All backup files")
|
|
470
|
-
console.print(" [white]\u2022[/white]
|
|
539
|
+
console.print(" [white]\u2022[/white] .tweek.yaml control files")
|
|
540
|
+
console.print(" [white]\u2022[/white] Tweek data directory (~/.tweek/) including hook wrappers")
|
|
471
541
|
|
|
472
542
|
# Show what exists in ~/.tweek/
|
|
473
543
|
if tweek_dir.exists():
|
|
@@ -489,23 +559,25 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
|
|
|
489
559
|
|
|
490
560
|
console.print()
|
|
491
561
|
|
|
492
|
-
# ── Project
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
562
|
+
# ── Project scopes (current + recorded from install) ──
|
|
563
|
+
for scope in all_project_scopes:
|
|
564
|
+
scope_label = str(scope)
|
|
565
|
+
console.print(f"[bold]Project scope ({scope_label}):[/bold]")
|
|
566
|
+
removed_hooks = _remove_hooks_from_settings(scope / "settings.json")
|
|
567
|
+
for hook_type in removed_hooks:
|
|
568
|
+
console.print(f" [green]\u2713[/green] Removed {hook_type} hook from {scope_label}/settings.json")
|
|
569
|
+
if not removed_hooks:
|
|
570
|
+
console.print(f" [white]-[/white] Skipped: no hooks found")
|
|
571
|
+
|
|
572
|
+
if _remove_skill_directory(scope):
|
|
573
|
+
console.print(f" [green]\u2713[/green] Removed Tweek skill directory")
|
|
574
|
+
else:
|
|
575
|
+
console.print(f" [white]-[/white] Skipped: no skill directory")
|
|
504
576
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
577
|
+
if _remove_backup_file(scope):
|
|
578
|
+
console.print(f" [green]\u2713[/green] Removed backup file")
|
|
579
|
+
else:
|
|
580
|
+
console.print(f" [white]-[/white] Skipped: no backup file")
|
|
509
581
|
|
|
510
582
|
console.print()
|
|
511
583
|
|
|
@@ -529,13 +601,14 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
|
|
|
529
601
|
|
|
530
602
|
console.print()
|
|
531
603
|
|
|
532
|
-
# ──
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
604
|
+
# ── .tweek.yaml control files ──
|
|
605
|
+
# Must run before data directory removal (needs installed_scopes.json)
|
|
606
|
+
console.print("[bold]Control files (.tweek.yaml):[/bold]")
|
|
607
|
+
yaml_removed = _remove_tweek_yaml_files(tweek_dir)
|
|
608
|
+
for yaml_path in yaml_removed:
|
|
609
|
+
console.print(f" [green]\u2713[/green] Removed {yaml_path}")
|
|
610
|
+
if not yaml_removed:
|
|
611
|
+
console.print(f" [white]-[/white] Skipped: no .tweek.yaml files found")
|
|
539
612
|
|
|
540
613
|
console.print()
|
|
541
614
|
|
|
@@ -547,5 +620,15 @@ def _uninstall_everything(global_target: Path, project_target: Path, tweek_dir:
|
|
|
547
620
|
if not mcp_removed:
|
|
548
621
|
console.print(f" [white]-[/white] Skipped: no MCP integrations found")
|
|
549
622
|
|
|
623
|
+
console.print()
|
|
624
|
+
|
|
625
|
+
# ── Tweek data directory (last — other steps need installed_scopes.json) ──
|
|
626
|
+
console.print("[bold]Tweek data (~/.tweek/):[/bold]")
|
|
627
|
+
data_removed = _remove_tweek_data_dir(tweek_dir)
|
|
628
|
+
for item in data_removed:
|
|
629
|
+
console.print(f" [green]\u2713[/green] Removed {item}")
|
|
630
|
+
if not data_removed:
|
|
631
|
+
console.print(f" [white]-[/white] Skipped: no data directory found")
|
|
632
|
+
|
|
550
633
|
console.print()
|
|
551
634
|
console.print("[green]All Tweek data has been removed.[/green]")
|
tweek/config/families.yaml
CHANGED
|
@@ -128,6 +128,8 @@ families:
|
|
|
128
128
|
- 218 # reverse_shell_perl_ruby
|
|
129
129
|
- 219 # reverse_shell_mkfifo
|
|
130
130
|
- 220 # reverse_shell_encoded
|
|
131
|
+
- 272 # shortened_url_to_shell
|
|
132
|
+
- 275 # raw_ip_url_to_shell
|
|
131
133
|
heuristic_signals:
|
|
132
134
|
exfil_verbs:
|
|
133
135
|
- "curl"
|
|
@@ -308,6 +310,15 @@ families:
|
|
|
308
310
|
- 113 # gzip_obfuscation
|
|
309
311
|
- 125 # importlib_evasion
|
|
310
312
|
- 126 # variable_indirection
|
|
313
|
+
- 263 # punycode_domain
|
|
314
|
+
- 264 # non_ascii_hostname
|
|
315
|
+
- 265 # cyrillic_in_url
|
|
316
|
+
- 266 # greek_in_url
|
|
317
|
+
- 267 # bidi_isolate_chars
|
|
318
|
+
- 268 # url_userinfo_trick
|
|
319
|
+
- 269 # lookalike_tld
|
|
320
|
+
- 270 # fullwidth_dot_in_url
|
|
321
|
+
- 274 # double_encoding_in_url
|
|
311
322
|
heuristic_signals:
|
|
312
323
|
evasion_indicators:
|
|
313
324
|
- "eval("
|
|
@@ -391,6 +402,7 @@ families:
|
|
|
391
402
|
- 96 # login_item_persistence
|
|
392
403
|
- 118 # curl_write_sensitive
|
|
393
404
|
- 227 # privesc_cron_inject
|
|
405
|
+
- 273 # dotfile_overwrite_via_redirect
|
|
394
406
|
heuristic_signals:
|
|
395
407
|
persistence_paths:
|
|
396
408
|
- "LaunchAgents"
|
|
@@ -582,6 +594,7 @@ families:
|
|
|
582
594
|
- 167 # pnpm_symlink_traversal
|
|
583
595
|
- 248 # supply_chain_typosquat_ai
|
|
584
596
|
- 249 # supply_chain_postinstall_exec
|
|
597
|
+
- 271 # insecure_tls_flags
|
|
585
598
|
heuristic_signals:
|
|
586
599
|
supply_chain_indicators:
|
|
587
600
|
- "--pre"
|
tweek/config/models.py
CHANGED
|
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import re
|
|
14
14
|
from enum import Enum
|
|
15
|
-
from typing import Any, Dict, List, Optional
|
|
15
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
16
16
|
|
|
17
17
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
18
18
|
|
|
@@ -53,6 +53,21 @@ class PatternConfidence(str, Enum):
|
|
|
53
53
|
CONTEXTUAL = "contextual"
|
|
54
54
|
|
|
55
55
|
|
|
56
|
+
class LLMFailMode(str, Enum):
|
|
57
|
+
"""Behavior when LLM review is unavailable (provider errors, timeouts).
|
|
58
|
+
|
|
59
|
+
OPEN: Degrade gracefully, return SAFE (current default behavior).
|
|
60
|
+
Pattern matching remains the primary defense.
|
|
61
|
+
CLOSED: Hard block -- return DANGEROUS with should_prompt=True.
|
|
62
|
+
Use when LLM review is considered a mandatory gate.
|
|
63
|
+
ESCALATE: Return SUSPICIOUS with should_prompt=True (triggers ASK).
|
|
64
|
+
Recommended middle ground for security-sensitive environments.
|
|
65
|
+
"""
|
|
66
|
+
OPEN = "open"
|
|
67
|
+
CLOSED = "closed"
|
|
68
|
+
ESCALATE = "escalate"
|
|
69
|
+
|
|
70
|
+
|
|
56
71
|
# ============================================================================
|
|
57
72
|
# Configuration Section Models
|
|
58
73
|
# ============================================================================
|
|
@@ -88,6 +103,10 @@ class LLMReviewConfig(BaseModel):
|
|
|
88
103
|
base_url: Optional[str] = None
|
|
89
104
|
api_key_env: Optional[str] = None
|
|
90
105
|
timeout_seconds: float = Field(default=15.0, gt=0)
|
|
106
|
+
fail_mode: LLMFailMode = Field(
|
|
107
|
+
default=LLMFailMode.OPEN,
|
|
108
|
+
description="Behavior when LLM review is unavailable: open, closed, or escalate",
|
|
109
|
+
)
|
|
91
110
|
local: LLMReviewLocalConfig = Field(default_factory=LLMReviewLocalConfig)
|
|
92
111
|
fallback: LLMReviewFallbackConfig = Field(default_factory=LLMReviewFallbackConfig)
|
|
93
112
|
|
|
@@ -186,9 +205,18 @@ class OpenClawConfig(BaseModel):
|
|
|
186
205
|
gateway_port: int = Field(default=18789, gt=0, le=65535)
|
|
187
206
|
scanner_port: int = Field(default=9878, gt=0, le=65535)
|
|
188
207
|
plugin_installed: bool = False
|
|
189
|
-
preset:
|
|
208
|
+
preset: Literal["paranoid", "cautious", "balanced", "trusted"] = "cautious"
|
|
190
209
|
|
|
191
|
-
model_config = {"extra": "
|
|
210
|
+
model_config = {"extra": "forbid"}
|
|
211
|
+
|
|
212
|
+
@model_validator(mode="after")
|
|
213
|
+
def check_port_collision(self) -> "OpenClawConfig":
|
|
214
|
+
if self.gateway_port == self.scanner_port:
|
|
215
|
+
raise ValueError(
|
|
216
|
+
f"gateway_port and scanner_port must differ "
|
|
217
|
+
f"(both set to {self.gateway_port})"
|
|
218
|
+
)
|
|
219
|
+
return self
|
|
192
220
|
|
|
193
221
|
|
|
194
222
|
# ============================================================================
|
tweek/config/patterns.yaml
CHANGED
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
# contextual - Depends on surrounding context; broad behavioral pattern
|
|
25
25
|
# PRO tier adds: LLM review, session analysis, rate limiting
|
|
26
26
|
|
|
27
|
-
version:
|
|
28
|
-
pattern_count:
|
|
27
|
+
version: 6
|
|
28
|
+
pattern_count: 275
|
|
29
29
|
|
|
30
30
|
patterns:
|
|
31
31
|
# ============================================================================
|
|
@@ -2275,3 +2275,127 @@ patterns:
|
|
|
2275
2275
|
severity: medium
|
|
2276
2276
|
confidence: contextual
|
|
2277
2277
|
family: prompt_injection
|
|
2278
|
+
|
|
2279
|
+
# ============================================================================
|
|
2280
|
+
# TERMINAL & URL SECURITY PATTERNS (263-275)
|
|
2281
|
+
# 13 patterns covering IDN/punycode, homoglyph scripts, bidi text direction,
|
|
2282
|
+
# URL spoofing, insecure transport, dotfile persistence, and encoding attacks.
|
|
2283
|
+
# Inspired by Tirith terminal security research.
|
|
2284
|
+
# Added: 2026-02-03
|
|
2285
|
+
# ============================================================================
|
|
2286
|
+
|
|
2287
|
+
# --- IDN / Punycode Attacks (263-264) ---
|
|
2288
|
+
|
|
2289
|
+
- id: 263
|
|
2290
|
+
name: punycode_domain
|
|
2291
|
+
description: "Internationalized domain name (punycode) in URL"
|
|
2292
|
+
regex: 'https?://[^\s/]*xn--[^\s/]*'
|
|
2293
|
+
severity: high
|
|
2294
|
+
confidence: heuristic
|
|
2295
|
+
family: evasion_techniques
|
|
2296
|
+
|
|
2297
|
+
- id: 264
|
|
2298
|
+
name: non_ascii_hostname
|
|
2299
|
+
description: "Non-ASCII characters in URL hostname (potential homoglyph)"
|
|
2300
|
+
regex: 'https?://[^\s/]*[^\x00-\x7f][^\s/]*'
|
|
2301
|
+
severity: high
|
|
2302
|
+
confidence: heuristic
|
|
2303
|
+
family: evasion_techniques
|
|
2304
|
+
|
|
2305
|
+
# --- Homoglyph Script Detection (265-266) ---
|
|
2306
|
+
|
|
2307
|
+
- id: 265
|
|
2308
|
+
name: cyrillic_in_url
|
|
2309
|
+
description: "Cyrillic characters in URL (homoglyph spoofing)"
|
|
2310
|
+
regex: 'https?://[^\s]*[\u0400-\u04ff]'
|
|
2311
|
+
severity: high
|
|
2312
|
+
confidence: heuristic
|
|
2313
|
+
family: evasion_techniques
|
|
2314
|
+
|
|
2315
|
+
- id: 266
|
|
2316
|
+
name: greek_in_url
|
|
2317
|
+
description: "Greek characters in URL (homoglyph spoofing)"
|
|
2318
|
+
regex: 'https?://[^\s]*[\u0370-\u03ff]'
|
|
2319
|
+
severity: medium
|
|
2320
|
+
confidence: heuristic
|
|
2321
|
+
family: evasion_techniques
|
|
2322
|
+
|
|
2323
|
+
# --- Bidi Text Direction Attacks (267) ---
|
|
2324
|
+
|
|
2325
|
+
- id: 267
|
|
2326
|
+
name: bidi_isolate_chars
|
|
2327
|
+
description: "Bidi isolate characters that manipulate text display direction"
|
|
2328
|
+
regex: '[\u2066-\u2069]'
|
|
2329
|
+
severity: high
|
|
2330
|
+
confidence: heuristic
|
|
2331
|
+
family: evasion_techniques
|
|
2332
|
+
|
|
2333
|
+
# --- URL Spoofing (268-270) ---
|
|
2334
|
+
|
|
2335
|
+
- id: 268
|
|
2336
|
+
name: url_userinfo_trick
|
|
2337
|
+
description: "URL with userinfo@ to disguise real hostname"
|
|
2338
|
+
regex: 'https?://[^\s/@]+\.[^\s/@]+@[^\s/]+'
|
|
2339
|
+
severity: high
|
|
2340
|
+
confidence: heuristic
|
|
2341
|
+
family: evasion_techniques
|
|
2342
|
+
|
|
2343
|
+
- id: 269
|
|
2344
|
+
name: lookalike_tld
|
|
2345
|
+
description: "URL with .zip or .mov TLD that mimics file extension"
|
|
2346
|
+
regex: 'https?://[^\s/]+\.(zip|mov)(/|\s|$)'
|
|
2347
|
+
severity: medium
|
|
2348
|
+
confidence: contextual
|
|
2349
|
+
family: evasion_techniques
|
|
2350
|
+
|
|
2351
|
+
- id: 270
|
|
2352
|
+
name: fullwidth_dot_in_url
|
|
2353
|
+
description: "Fullwidth or ideographic dots in URL to bypass domain parsing"
|
|
2354
|
+
regex: 'https?://[^\s]*[\uff0e\u3002\uff61]'
|
|
2355
|
+
severity: high
|
|
2356
|
+
confidence: heuristic
|
|
2357
|
+
family: evasion_techniques
|
|
2358
|
+
|
|
2359
|
+
# --- Insecure Transport (271-272) ---
|
|
2360
|
+
|
|
2361
|
+
- id: 271
|
|
2362
|
+
name: insecure_tls_flags
|
|
2363
|
+
description: "TLS certificate verification disabled (MITM risk)"
|
|
2364
|
+
regex: '(curl\s+.*(-k\b|--insecure)|wget\s+.*--no-check-certificate)'
|
|
2365
|
+
severity: high
|
|
2366
|
+
confidence: heuristic
|
|
2367
|
+
family: supply_chain
|
|
2368
|
+
|
|
2369
|
+
- id: 272
|
|
2370
|
+
name: shortened_url_to_shell
|
|
2371
|
+
description: "Shortened URL piped to interpreter (hidden destination)"
|
|
2372
|
+
regex: '(curl|wget)\s+.*https?://(bit\.ly|t\.co|tinyurl\.com|is\.gd|rb\.gy|shorturl\.at)[^\s]*.*\|\s*(bash|sh|zsh|python|perl|ruby)'
|
|
2373
|
+
severity: critical
|
|
2374
|
+
confidence: deterministic
|
|
2375
|
+
family: data_exfiltration
|
|
2376
|
+
|
|
2377
|
+
# --- Dotfile Persistence & Encoding (273-275) ---
|
|
2378
|
+
|
|
2379
|
+
- id: 273
|
|
2380
|
+
name: dotfile_overwrite_via_redirect
|
|
2381
|
+
description: "Download command redirecting output to shell config dotfile"
|
|
2382
|
+
regex: '(curl|wget)\s+[^|]*?(>|>>)\s*~?/?\.(bashrc|zshrc|profile|bash_profile|gitconfig)'
|
|
2383
|
+
severity: critical
|
|
2384
|
+
confidence: deterministic
|
|
2385
|
+
family: persistence
|
|
2386
|
+
|
|
2387
|
+
- id: 274
|
|
2388
|
+
name: double_encoding_in_url
|
|
2389
|
+
description: "Double percent-encoding to bypass URL security filters"
|
|
2390
|
+
regex: '%25[0-9a-fA-F]{2}'
|
|
2391
|
+
severity: medium
|
|
2392
|
+
confidence: heuristic
|
|
2393
|
+
family: evasion_techniques
|
|
2394
|
+
|
|
2395
|
+
- id: 275
|
|
2396
|
+
name: raw_ip_url_to_shell
|
|
2397
|
+
description: "Raw IP address URL piped to interpreter"
|
|
2398
|
+
regex: '(curl|wget)\s+.*https?://\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}[^\s]*.*\|\s*(bash|sh|zsh|python)'
|
|
2399
|
+
severity: high
|
|
2400
|
+
confidence: heuristic
|
|
2401
|
+
family: data_exfiltration
|
tweek/diagnostics.py
CHANGED
|
@@ -33,12 +33,13 @@ class HealthCheck:
|
|
|
33
33
|
fix_hint: str = "" # Recovery: "Run: tweek protect claude-code --global"
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
def run_health_checks(verbose: bool = False) -> List[HealthCheck]:
|
|
36
|
+
def run_health_checks(verbose: bool = False, interactive: bool = False) -> List[HealthCheck]:
|
|
37
37
|
"""
|
|
38
38
|
Run all health checks and return results.
|
|
39
39
|
|
|
40
40
|
Args:
|
|
41
41
|
verbose: If True, include extra detail in messages.
|
|
42
|
+
interactive: If True, offer to fix issues interactively.
|
|
42
43
|
|
|
43
44
|
Returns:
|
|
44
45
|
List of HealthCheck results in check order.
|
|
@@ -72,6 +73,10 @@ def run_health_checks(verbose: bool = False) -> List[HealthCheck]:
|
|
|
72
73
|
fix_hint="This may indicate a corrupted installation. Try: pip install --force-reinstall tweek",
|
|
73
74
|
))
|
|
74
75
|
|
|
76
|
+
# Interactive fix mode — offer to resolve fixable issues
|
|
77
|
+
if interactive:
|
|
78
|
+
_offer_interactive_fixes(results)
|
|
79
|
+
|
|
75
80
|
# Log health check results
|
|
76
81
|
try:
|
|
77
82
|
from tweek.logging.security_log import get_logger, SecurityEvent, EventType
|
|
@@ -788,3 +793,121 @@ def _check_llm_review(verbose: bool = False) -> HealthCheck:
|
|
|
788
793
|
status=CheckStatus.WARNING,
|
|
789
794
|
message=f"Cannot check LLM review: {e}",
|
|
790
795
|
)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
# ==================== Interactive Fix Mode ====================
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def _offer_interactive_fixes(results: List[HealthCheck]) -> None:
|
|
802
|
+
"""Offer to fix issues found during health checks.
|
|
803
|
+
|
|
804
|
+
Only runs when ``tweek doctor --fix`` is used. Prompts for each
|
|
805
|
+
fixable issue and attempts automatic remediation.
|
|
806
|
+
"""
|
|
807
|
+
import click
|
|
808
|
+
from rich.console import Console
|
|
809
|
+
|
|
810
|
+
console = Console()
|
|
811
|
+
fixable = [r for r in results if r.status in (CheckStatus.WARNING, CheckStatus.ERROR)]
|
|
812
|
+
|
|
813
|
+
if not fixable:
|
|
814
|
+
return
|
|
815
|
+
|
|
816
|
+
console.print("\n[bold]Interactive Fix Mode[/bold]")
|
|
817
|
+
console.print(f"Found {len(fixable)} issue(s) that may be fixable:\n")
|
|
818
|
+
|
|
819
|
+
for check in fixable:
|
|
820
|
+
fixed = False
|
|
821
|
+
|
|
822
|
+
if check.name == "local_model" and "not downloaded" in check.message:
|
|
823
|
+
if click.confirm(f" [{check.name}] Download local model now?", default=True):
|
|
824
|
+
fixed = _fix_download_model()
|
|
825
|
+
|
|
826
|
+
elif check.name == "local_model" and "Dependencies" in check.message:
|
|
827
|
+
if click.confirm(f" [{check.name}] Install local model dependencies?", default=True):
|
|
828
|
+
fixed = _fix_install_model_deps()
|
|
829
|
+
|
|
830
|
+
elif check.name == "local_model" and "SHA-256" in check.message:
|
|
831
|
+
if click.confirm(f" [{check.name}] Re-download model (integrity check failed)?", default=True):
|
|
832
|
+
fixed = _fix_redownload_model()
|
|
833
|
+
|
|
834
|
+
elif check.name == "llm_review" and "No LLM provider" in check.message:
|
|
835
|
+
if click.confirm(f" [{check.name}] Configure LLM provider now?", default=True):
|
|
836
|
+
fixed = _fix_configure_llm()
|
|
837
|
+
|
|
838
|
+
elif check.name == "hooks_installed" and "No hooks" in check.message:
|
|
839
|
+
if click.confirm(f" [{check.name}] Install hooks now?", default=True):
|
|
840
|
+
fixed = _fix_install_hooks()
|
|
841
|
+
|
|
842
|
+
else:
|
|
843
|
+
if check.fix_hint:
|
|
844
|
+
console.print(f" [{check.name}] {check.message}")
|
|
845
|
+
console.print(f" [dim]Hint: {check.fix_hint}[/dim]")
|
|
846
|
+
|
|
847
|
+
if fixed:
|
|
848
|
+
console.print(f" [green]\u2713[/green] Fixed: {check.name}")
|
|
849
|
+
console.print()
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def _fix_download_model() -> bool:
|
|
853
|
+
"""Download the local classifier model."""
|
|
854
|
+
try:
|
|
855
|
+
from tweek.cli_install import _download_local_model
|
|
856
|
+
return _download_local_model(quick=False)
|
|
857
|
+
except Exception as e:
|
|
858
|
+
from rich.console import Console
|
|
859
|
+
Console().print(f" [red]\u2717[/red] Failed: {e}")
|
|
860
|
+
return False
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
def _fix_install_model_deps() -> bool:
|
|
864
|
+
"""Install onnxruntime, tokenizers, numpy."""
|
|
865
|
+
try:
|
|
866
|
+
from tweek.cli_install import _ensure_local_model_deps
|
|
867
|
+
return _ensure_local_model_deps()
|
|
868
|
+
except Exception as e:
|
|
869
|
+
from rich.console import Console
|
|
870
|
+
Console().print(f" [red]\u2717[/red] Failed: {e}")
|
|
871
|
+
return False
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def _fix_redownload_model() -> bool:
|
|
875
|
+
"""Force re-download the local model."""
|
|
876
|
+
try:
|
|
877
|
+
from tweek.security.model_registry import download_model, get_default_model_name
|
|
878
|
+
download_model(get_default_model_name(), force=True)
|
|
879
|
+
return True
|
|
880
|
+
except Exception as e:
|
|
881
|
+
from rich.console import Console
|
|
882
|
+
Console().print(f" [red]\u2717[/red] Failed: {e}")
|
|
883
|
+
return False
|
|
884
|
+
|
|
885
|
+
|
|
886
|
+
def _fix_configure_llm() -> bool:
|
|
887
|
+
"""Run the LLM provider configuration wizard."""
|
|
888
|
+
try:
|
|
889
|
+
from tweek.cli_install import _configure_llm_provider
|
|
890
|
+
tweek_dir = Path("~/.tweek").expanduser()
|
|
891
|
+
_configure_llm_provider(tweek_dir, interactive=True, quick=False)
|
|
892
|
+
return True
|
|
893
|
+
except Exception as e:
|
|
894
|
+
from rich.console import Console
|
|
895
|
+
Console().print(f" [red]\u2717[/red] Failed: {e}")
|
|
896
|
+
return False
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _fix_install_hooks() -> bool:
|
|
900
|
+
"""Run hook installation."""
|
|
901
|
+
try:
|
|
902
|
+
from tweek.cli_install import _install_claude_code_hooks
|
|
903
|
+
_install_claude_code_hooks(
|
|
904
|
+
install_global=True, dev_test=False, backup=True,
|
|
905
|
+
skip_env_scan=True, interactive=False, preset="cautious",
|
|
906
|
+
ai_defaults=False, with_sandbox=False, force_proxy=False,
|
|
907
|
+
skip_proxy_check=True, quick=True,
|
|
908
|
+
)
|
|
909
|
+
return True
|
|
910
|
+
except Exception as e:
|
|
911
|
+
from rich.console import Console
|
|
912
|
+
Console().print(f" [red]\u2717[/red] Failed: {e}")
|
|
913
|
+
return False
|