conduct-cli 0.8.9__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/PKG-INFO +1 -1
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/pyproject.toml +1 -1
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/hooks/pretooluse.py +85 -2
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/README.md +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/setup.cfg +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/setup.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/guard.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/guard_policy.md +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/hooks/__init__.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/hooks/base.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/hooks/posttooluse.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/hooks/precompact.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/hooks/session_parser.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/hooks/session_report_push.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/hooks/session_start.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/hooks/stop.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/log_util.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/main.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/memory.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli/tool_groups.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_advisory_and_hook.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_api_client.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_bash_operator_signature.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_blast_radius.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_cmd_run.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_command_word_matcher.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_drain_daemon_health.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_hook_dispatch.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_instruction_files.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_journal_drain.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_log_util.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_proxy_coverage.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_proxy_env.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_report_tool_coverage.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_signed_policy.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_switch.py +0 -0
- {conduct_cli-0.8.9 → conduct_cli-0.9.0}/tests/test_token_lifecycle.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "conduct-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.9.0"
|
|
8
8
|
description = "CLI for Conduct AI — secure, govern, install agents, manage projects, run tests"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -309,6 +309,14 @@ DOC_SENSITIVE_RULE_PREFIXES = (
|
|
|
309
309
|
"nist-govern-doc",
|
|
310
310
|
"eu-ai-pii-",
|
|
311
311
|
"no-" + "env" + "-read", # split literal to avoid triggering the pattern itself
|
|
312
|
+
# Rules that document what an attack looks like will fire on files that
|
|
313
|
+
# define, seed, or test those attacks. On dev paths (with sentinel) these
|
|
314
|
+
# skip so we can maintain the packs themselves. Not doc-sensitive in the
|
|
315
|
+
# customer sense — sensitive to *authoring* the detection rule.
|
|
316
|
+
"no_prompt_" + "inject",
|
|
317
|
+
"proxy-no-prompt-" + "inject",
|
|
318
|
+
"prompt-" + "inject",
|
|
319
|
+
"nist-govern-" + "policy",
|
|
312
320
|
)
|
|
313
321
|
|
|
314
322
|
# IRS regulatory pack rules also skip on dev paths since our own code names
|
|
@@ -401,6 +409,67 @@ def _rule_is_doc_sensitive(rid: str) -> bool:
|
|
|
401
409
|
return False
|
|
402
410
|
|
|
403
411
|
|
|
412
|
+
_B64_CHUNK = re.compile(r"[A-Za-z0-9+/]{20,}={0,2}")
|
|
413
|
+
_HEX_CHUNK = re.compile(r"\b[0-9a-fA-F]{40,}\b")
|
|
414
|
+
_URL_ESC = re.compile(r"%[0-9a-fA-F]{2}")
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def _decoded_variants(text: str) -> list[str]:
|
|
418
|
+
"""Return likely-decoded versions of text for obfuscation-bypass matching.
|
|
419
|
+
|
|
420
|
+
Applies base64, hex, URL-decode, and ROT13. Caps output to keep the scan
|
|
421
|
+
cost bounded regardless of input size. Silent on decode failure — bad
|
|
422
|
+
input just yields no extra variants.
|
|
423
|
+
"""
|
|
424
|
+
variants: list[str] = []
|
|
425
|
+
if not text:
|
|
426
|
+
return variants
|
|
427
|
+
import base64 as _b64
|
|
428
|
+
import codecs as _codecs
|
|
429
|
+
import urllib.parse as _urlparse
|
|
430
|
+
|
|
431
|
+
for m in _B64_CHUNK.findall(text)[:5]:
|
|
432
|
+
try:
|
|
433
|
+
padded = m + "=" * ((4 - len(m) % 4) % 4)
|
|
434
|
+
decoded = _b64.b64decode(padded, validate=False)
|
|
435
|
+
s = decoded.decode("utf-8", errors="ignore")
|
|
436
|
+
if s.strip():
|
|
437
|
+
variants.append(s)
|
|
438
|
+
except Exception:
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
for m in _HEX_CHUNK.findall(text)[:3]:
|
|
442
|
+
try:
|
|
443
|
+
s = bytes.fromhex(m).decode("utf-8", errors="ignore")
|
|
444
|
+
if s.strip():
|
|
445
|
+
variants.append(s)
|
|
446
|
+
except Exception:
|
|
447
|
+
pass
|
|
448
|
+
|
|
449
|
+
if _URL_ESC.search(text):
|
|
450
|
+
try:
|
|
451
|
+
s = _urlparse.unquote(text)
|
|
452
|
+
if s and s != text:
|
|
453
|
+
variants.append(s)
|
|
454
|
+
except Exception:
|
|
455
|
+
pass
|
|
456
|
+
|
|
457
|
+
try:
|
|
458
|
+
variants.append(_codecs.decode(text, "rot_13"))
|
|
459
|
+
except Exception:
|
|
460
|
+
pass
|
|
461
|
+
|
|
462
|
+
try:
|
|
463
|
+
import unicodedata as _u
|
|
464
|
+
norm = _u.normalize("NFKC", text)
|
|
465
|
+
if norm != text:
|
|
466
|
+
variants.append(norm)
|
|
467
|
+
except Exception:
|
|
468
|
+
pass
|
|
469
|
+
|
|
470
|
+
return variants[:20]
|
|
471
|
+
|
|
472
|
+
|
|
404
473
|
def check_policy(tool_name: str, tool_input: dict, tokens_before: int = 0):
|
|
405
474
|
"""Return (matched_rule, action, rule_id, message) or (None, 'allow', None, None)."""
|
|
406
475
|
pol_path = active_policy_path()
|
|
@@ -424,8 +493,15 @@ def check_policy(tool_name: str, tool_input: dict, tokens_before: int = 0):
|
|
|
424
493
|
rules = policy.get("rules", [])
|
|
425
494
|
if tool_name == "Bash" and tool_input.get("command"):
|
|
426
495
|
input_text = _bash_operator_signature(tool_input["command"])
|
|
496
|
+
raw_for_decode = tool_input["command"]
|
|
427
497
|
else:
|
|
428
498
|
input_text = json.dumps(tool_input)
|
|
499
|
+
raw_for_decode = input_text
|
|
500
|
+
# Obfuscation pre-decode: any variant of the input decoded from base64/
|
|
501
|
+
# hex/URL-encoding/ROT13/NFKC. Rules whose "scan" is "decoded" only match
|
|
502
|
+
# against these; all other rules match raw AND decoded so encoded payloads
|
|
503
|
+
# cannot slip past pattern-based detection.
|
|
504
|
+
decoded_variants = _decoded_variants(raw_for_decode)
|
|
429
505
|
path_fields = [str(tool_input.get(f, "")) for f in ["file_path", "path", "command"]]
|
|
430
506
|
|
|
431
507
|
is_dev_path = _is_developer_source_path(tool_input)
|
|
@@ -467,9 +543,16 @@ def check_policy(tool_name: str, tool_input: dict, tokens_before: int = 0):
|
|
|
467
543
|
continue
|
|
468
544
|
pattern = rule.get("match_pattern")
|
|
469
545
|
if pattern:
|
|
546
|
+
scan_mode = rule.get("scan")
|
|
470
547
|
try:
|
|
471
|
-
if
|
|
472
|
-
|
|
548
|
+
if scan_mode == "decoded":
|
|
549
|
+
if not any(re.search(pattern, v, re.IGNORECASE) for v in decoded_variants):
|
|
550
|
+
continue
|
|
551
|
+
else:
|
|
552
|
+
if not re.search(pattern, input_text, re.IGNORECASE) and not any(
|
|
553
|
+
re.search(pattern, v, re.IGNORECASE) for v in decoded_variants
|
|
554
|
+
):
|
|
555
|
+
continue
|
|
473
556
|
except re.error:
|
|
474
557
|
continue
|
|
475
558
|
path_pattern = rule.get("match_path_pattern")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|