conduct-cli 0.8.8__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.
Files changed (50) hide show
  1. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/PKG-INFO +1 -1
  2. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/pyproject.toml +1 -1
  3. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/hooks/pretooluse.py +141 -11
  4. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli.egg-info/PKG-INFO +1 -1
  5. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/README.md +0 -0
  6. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/setup.cfg +0 -0
  7. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/setup.py +0 -0
  8. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/__init__.py +0 -0
  9. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/api.py +0 -0
  10. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/guard.py +0 -0
  11. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/guard_policy.md +0 -0
  12. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/guardmcp.py +0 -0
  13. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/hooks/__init__.py +0 -0
  14. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/hooks/base.py +0 -0
  15. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/hooks/posttooluse.py +0 -0
  16. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/hooks/precompact.py +0 -0
  17. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/hooks/session_parser.py +0 -0
  18. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/hooks/session_report_push.py +0 -0
  19. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/hooks/session_start.py +0 -0
  20. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/hooks/stop.py +0 -0
  21. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/log_util.py +0 -0
  22. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/main.py +0 -0
  23. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/mcp_server.py +0 -0
  24. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/memory.py +0 -0
  25. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/paxel.py +0 -0
  26. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli/tool_groups.py +0 -0
  27. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
  28. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  29. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  30. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli.egg-info/requires.txt +0 -0
  31. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/src/conduct_cli.egg-info/top_level.txt +0 -0
  32. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_advisory_and_hook.py +0 -0
  33. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_api_client.py +0 -0
  34. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_bash_operator_signature.py +0 -0
  35. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_blast_radius.py +0 -0
  36. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_cmd_run.py +0 -0
  37. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_command_word_matcher.py +0 -0
  38. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_drain_daemon_health.py +0 -0
  39. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_guard_policy.py +0 -0
  40. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_guard_savings.py +0 -0
  41. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_hook_dispatch.py +0 -0
  42. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_instruction_files.py +0 -0
  43. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_journal_drain.py +0 -0
  44. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_log_util.py +0 -0
  45. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_proxy_coverage.py +0 -0
  46. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_proxy_env.py +0 -0
  47. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_report_tool_coverage.py +0 -0
  48. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_signed_policy.py +0 -0
  49. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_switch.py +0 -0
  50. {conduct_cli-0.8.8 → conduct_cli-0.9.0}/tests/test_token_lifecycle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.8.8
3
+ Version: 0.9.0
4
4
  Summary: CLI for Conduct AI — secure, govern, install agents, manage projects, run tests
5
5
  Author-email: Conduct AI <hello@conductai.ai>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "conduct-cli"
7
- version = "0.8.8"
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
@@ -321,25 +329,72 @@ DEV_PATH_MARKERS = (
321
329
  "/apps/api/alembic/versions/",
322
330
  "/apps/api/tests/",
323
331
  "/apps/api/app/modules/agent_identity/",
324
- "/apps/api/app/routers/",
325
- "/apps/web/src/",
326
- "/docs/",
332
+ "/apps/api/app/modules/guard/cedar_adapter/",
333
+ # Removed /apps/web/src/ and /docs/ — too generic; those substrings appear
334
+ # in ordinary Next.js apps and every project with a docs folder, which
335
+ # would silently disable compliance rules in customer repos (#1049).
327
336
  )
328
337
 
338
+ # Sentinel file at repo root marks a Conduct source repo — required for
339
+ # DEV_PATH_MARKERS to activate. Without this file, doc-sensitive rules apply
340
+ # everywhere, including in paths that happen to match Conduct-specific
341
+ # subpaths. Prevents cross-project bypass of compliance rules.
342
+ DEV_SENTINEL_FILENAME = ".conduct-dev-repo"
343
+
344
+ _DEV_SENTINEL_CACHE: dict[str, bool] = {}
345
+
346
+
347
+ def _find_dev_sentinel(start_path: str) -> bool:
348
+ """Walk up from start_path looking for the DEV_SENTINEL_FILENAME marker.
349
+
350
+ Cached per starting directory. Returns False if start_path is empty or
351
+ the walk hits root without finding the sentinel.
352
+ """
353
+ if not start_path:
354
+ return False
355
+ try:
356
+ p = Path(start_path).resolve()
357
+ except Exception:
358
+ return False
359
+ d = str(p.parent if not p.is_dir() else p)
360
+ if d in _DEV_SENTINEL_CACHE:
361
+ return _DEV_SENTINEL_CACHE[d]
362
+ cur = Path(d)
363
+ seen: list[str] = []
364
+ while True:
365
+ seen.append(str(cur))
366
+ try:
367
+ if (cur / DEV_SENTINEL_FILENAME).is_file():
368
+ for s in seen:
369
+ _DEV_SENTINEL_CACHE[s] = True
370
+ return True
371
+ except Exception:
372
+ break
373
+ parent = cur.parent
374
+ if parent == cur:
375
+ break
376
+ cur = parent
377
+ for s in seen:
378
+ _DEV_SENTINEL_CACHE[s] = False
379
+ return False
380
+
329
381
 
330
382
  def _is_developer_source_path(tool_input: dict) -> bool:
331
- """True if the tool_input targets a path where we DEFINE/DOCUMENT rules.
383
+ """True if the tool_input targets a path in a Conduct source-repo AND
384
+ matches a Conduct-specific dev marker.
332
385
 
333
- Doc-sensitive rules skip these paths to prevent false positives on our own
334
- source that describes security concepts by name.
386
+ Both conditions required — the sentinel file gates the exclusion so
387
+ customer repos with similar subpaths do not silently bypass rules.
335
388
  """
336
389
  for field in ("file_path", "path"):
337
390
  p = tool_input.get(field, "") or ""
338
391
  if not p:
339
392
  continue
340
- for marker in DEV_PATH_MARKERS:
341
- if marker in p:
342
- return True
393
+ matched_marker = any(marker in p for marker in DEV_PATH_MARKERS)
394
+ if not matched_marker:
395
+ continue
396
+ if _find_dev_sentinel(p):
397
+ return True
343
398
  return False
344
399
 
345
400
 
@@ -354,6 +409,67 @@ def _rule_is_doc_sensitive(rid: str) -> bool:
354
409
  return False
355
410
 
356
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
+
357
473
  def check_policy(tool_name: str, tool_input: dict, tokens_before: int = 0):
358
474
  """Return (matched_rule, action, rule_id, message) or (None, 'allow', None, None)."""
359
475
  pol_path = active_policy_path()
@@ -377,8 +493,15 @@ def check_policy(tool_name: str, tool_input: dict, tokens_before: int = 0):
377
493
  rules = policy.get("rules", [])
378
494
  if tool_name == "Bash" and tool_input.get("command"):
379
495
  input_text = _bash_operator_signature(tool_input["command"])
496
+ raw_for_decode = tool_input["command"]
380
497
  else:
381
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)
382
505
  path_fields = [str(tool_input.get(f, "")) for f in ["file_path", "path", "command"]]
383
506
 
384
507
  is_dev_path = _is_developer_source_path(tool_input)
@@ -420,9 +543,16 @@ def check_policy(tool_name: str, tool_input: dict, tokens_before: int = 0):
420
543
  continue
421
544
  pattern = rule.get("match_pattern")
422
545
  if pattern:
546
+ scan_mode = rule.get("scan")
423
547
  try:
424
- if not re.search(pattern, input_text, re.IGNORECASE):
425
- continue
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
426
556
  except re.error:
427
557
  continue
428
558
  path_pattern = rule.get("match_path_pattern")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.8.8
3
+ Version: 0.9.0
4
4
  Summary: CLI for Conduct AI — secure, govern, install agents, manage projects, run tests
5
5
  Author-email: Conduct AI <hello@conductai.ai>
6
6
  License: MIT
File without changes
File without changes
File without changes