conduct-cli 0.4.56__tar.gz → 0.4.58__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 (20) hide show
  1. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/PKG-INFO +1 -1
  2. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/pyproject.toml +1 -1
  3. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli/guard.py +10 -2
  4. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli/main.py +189 -0
  5. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli.egg-info/PKG-INFO +1 -1
  6. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/README.md +0 -0
  7. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/setup.cfg +0 -0
  8. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/setup.py +0 -0
  9. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli/__init__.py +0 -0
  10. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli/api.py +0 -0
  11. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli/guardmcp.py +0 -0
  12. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli/mcp_server.py +0 -0
  13. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
  14. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  15. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  16. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli.egg-info/requires.txt +0 -0
  17. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli.egg-info/top_level.txt +0 -0
  18. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/tests/test_guard_policy.py +0 -0
  19. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/tests/test_guard_savings.py +0 -0
  20. {conduct_cli-0.4.56 → conduct_cli-0.4.58}/tests/test_switch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.56
3
+ Version: 0.4.58
4
4
  Summary: CLI for Conduct AI — 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.4.56"
7
+ version = "0.4.58"
8
8
  description = "CLI for Conduct AI — install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -241,7 +241,7 @@ def _post_usage(session_id, tool_name, tokens_input, tokens_output, duration_ms)
241
241
  )
242
242
 
243
243
 
244
- def _maybe_emit_security_finding(tool_response, session_id, tool_name):
244
+ def _maybe_emit_security_finding(tool_response, session_id, tool_name, tool_input=None):
245
245
  """Classify tool_response for security findings; POST to /security-findings if flag ON. Never raises."""
246
246
  try:
247
247
  cfg = json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
@@ -298,6 +298,12 @@ def _maybe_emit_security_finding(tool_response, session_id, tool_name):
298
298
  if not finding_type:
299
299
  return
300
300
 
301
+ ti = tool_input or {}
302
+ file_path = (
303
+ ti.get("file_path") or ti.get("path") or
304
+ (ti.get("command", "")[:120] if tool_name in ("bash", "terminal") else None)
305
+ ) or None
306
+
301
307
  payload = json.dumps({
302
308
  "tool": _detect_ai_tool(),
303
309
  "severity": severity,
@@ -305,6 +311,7 @@ def _maybe_emit_security_finding(tool_response, session_id, tool_name):
305
311
  "description": description,
306
312
  "source_run_id": session_id,
307
313
  "reporter_email": cfg.get("user_email") or "",
314
+ "file": file_path,
308
315
  })
309
316
  script = (
310
317
  "import urllib.request\\n"
@@ -489,7 +496,8 @@ def post_usage_main():
489
496
 
490
497
  # Security classifier runs regardless of transcript_path — scan every tool response
491
498
  tool_response = data.get("tool_response") or data.get("output") or ""
492
- _maybe_emit_security_finding(str(tool_response), session_id, tool_name)
499
+ tool_input = data.get("tool_input") or {}
500
+ _maybe_emit_security_finding(str(tool_response), session_id, tool_name, tool_input)
493
501
 
494
502
  sys.exit(0)
495
503
 
@@ -2214,6 +2214,182 @@ def cmd_run(args):
2214
2214
  _stream_run(server, workflow_id, run["id"], workspace_id, token, api_key)
2215
2215
 
2216
2216
 
2217
+ # ── conduct sync / test-guard / test-security ────────────────────────────────
2218
+
2219
+ def cmd_sync(args):
2220
+ """Sync Guard policies (and Security Loop policies if installed)."""
2221
+ import conduct_cli.guard as _g
2222
+ print(f"\n{BOLD}▶ conduct sync{RESET}\n")
2223
+ _g.cmd_guard_sync(args)
2224
+ print(f"\n{GREEN}Sync complete.{RESET}\n")
2225
+
2226
+
2227
+ _SECURITY_TEST_CASES = [
2228
+ ("AWS Access Key", "secret-leak", "critical", "AKIA1234567890ABCDEF found in output"),
2229
+ ("OpenAI API Key", "secret-leak", "high", "sk-abcdefghijklmnopqrstuvwx1234567890 in response"),
2230
+ ("GitHub PAT", "secret-leak", "high", "ghp_" + "A" * 36 + " token present"),
2231
+ ("Bearer Token", "secret-leak", "high", "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.test.sig"),
2232
+ ("Hardcoded Password", "secret-leak", "high", "password = 'hardcoded_secret_here'"),
2233
+ ("Hardcoded API Key", "secret-leak", "high", "api_key = 'abc123def456ghi789'"),
2234
+ ("Path Traversal", "path-traversal", "medium", "../../etc/passwd accessed"),
2235
+ ("File URI", "path-traversal", "medium", "file:///etc/passwd read"),
2236
+ ("eval() Injection", "injection", "high", "eval(user_input) called in output"),
2237
+ ("exec() Injection", "injection", "high", "exec(command) called in output"),
2238
+ ("SSL Disabled", "crypto", "high", "ssl.CERT_NONE used — verification disabled"),
2239
+ ("TLS Bypass", "crypto", "medium", "verify=False passed to requests"),
2240
+ ("SQL Injection", "injection", "high", "sql injection vulnerability in query"),
2241
+ ("XSS", "injection", "high", "cross-site scripting detected in output"),
2242
+ ("Auth Bypass", "auth-bypass", "high", "auth bypass possible via missing check"),
2243
+ ]
2244
+
2245
+
2246
+ def cmd_test_security(args):
2247
+ """Fire synthetic security findings for every classifier pattern."""
2248
+ from conduct_cli.guard import CONFIG_PATH
2249
+ try:
2250
+ import json as _json
2251
+ cfg = _json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
2252
+ except Exception:
2253
+ cfg = {}
2254
+
2255
+ workspace_id = cfg.get("workspace_id")
2256
+ api_key = cfg.get("api_key", "")
2257
+ user_email = cfg.get("user_email", "")
2258
+ api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
2259
+
2260
+ if not workspace_id:
2261
+ print(f"{RED}Not configured. Run: conduct guard setup{RESET}")
2262
+ sys.exit(1)
2263
+
2264
+ import urllib.request
2265
+ import json as _json
2266
+
2267
+ print(f"\n{BOLD}▶ conduct test-security — {len(_SECURITY_TEST_CASES)} patterns{RESET}\n")
2268
+
2269
+ passed = 0
2270
+ failed = 0
2271
+ for name, vtype, severity, description in _SECURITY_TEST_CASES:
2272
+ payload = _json.dumps({
2273
+ "tool": "claude-code",
2274
+ "severity": severity,
2275
+ "type": vtype,
2276
+ "description": f"[TEST] {description}",
2277
+ "reporter_email": user_email,
2278
+ "source_run_id": "conduct-test-security",
2279
+ }).encode()
2280
+ try:
2281
+ req = urllib.request.Request(
2282
+ f"{api_url}/security-findings?workspace_id={workspace_id}",
2283
+ data=payload,
2284
+ headers={"Content-Type": "application/json", "X-Api-Key": api_key},
2285
+ method="POST",
2286
+ )
2287
+ with urllib.request.urlopen(req, timeout=8) as resp:
2288
+ r = _json.loads(resp.read())
2289
+ fid = r.get("id", "")[:8]
2290
+ sev_color = RED if severity == "critical" else (YELLOW if severity == "high" else CYAN)
2291
+ print(f" {GREEN}✓{RESET} {name:<22} {sev_color}{severity:<8}{RESET} {GRAY}{fid}{RESET}")
2292
+ passed += 1
2293
+ except Exception as e:
2294
+ print(f" {RED}✕{RESET} {name:<22} {RED}FAILED — {e}{RESET}")
2295
+ failed += 1
2296
+
2297
+ print(f"\n {passed} posted · {failed} failed")
2298
+ print(f"\n {CYAN}→ View findings: {api_url.replace('api.', 'app.')}/secure/activity{RESET}\n")
2299
+
2300
+
2301
+ def cmd_test_guard(args):
2302
+ """Test each guard policy rule with a matching synthetic tool call."""
2303
+ import json as _json
2304
+ import re as _re
2305
+ from conduct_cli.guard import CONFIG_PATH, POLICY_PATH
2306
+
2307
+ try:
2308
+ cfg = _json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
2309
+ except Exception:
2310
+ cfg = {}
2311
+
2312
+ if not POLICY_PATH.exists():
2313
+ print(f"{RED}No policy file found. Run: conduct guard sync{RESET}")
2314
+ sys.exit(1)
2315
+
2316
+ try:
2317
+ policy = _json.loads(POLICY_PATH.read_text())
2318
+ except Exception as e:
2319
+ print(f"{RED}Could not load policy: {e}{RESET}")
2320
+ sys.exit(1)
2321
+
2322
+ rules = policy.get("rules", [])
2323
+ if not rules:
2324
+ print(f"{YELLOW}No rules in local policy. Run: conduct guard sync{RESET}")
2325
+ sys.exit(0)
2326
+
2327
+ workspace_id = cfg.get("workspace_id")
2328
+ api_key = cfg.get("api_key", "")
2329
+ api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
2330
+
2331
+ print(f"\n{BOLD}▶ conduct test-guard — {len(rules)} rule(s){RESET}\n")
2332
+
2333
+ import urllib.request
2334
+
2335
+ blocked = 0
2336
+ allowed = 0
2337
+ errors = 0
2338
+ for rule in rules:
2339
+ rule_id = rule.get("rule_id", "unknown")
2340
+ action = rule.get("action", "audit")
2341
+ message = rule.get("message") or rule_id
2342
+ tool = (rule.get("match_tool") or "bash").split(",")[0].strip()
2343
+ pattern = rule.get("match_pattern") or rule.get("match_path_pattern") or ""
2344
+
2345
+ # Build a synthetic input that satisfies the rule's pattern
2346
+ if pattern:
2347
+ try:
2348
+ # Use the pattern itself as a test input fragment where possible
2349
+ test_input = _re.sub(r"[\\^$.*+?()\[\]{}|]", "", pattern)[:80] or rule_id
2350
+ except Exception:
2351
+ test_input = rule_id
2352
+ else:
2353
+ test_input = rule_id
2354
+
2355
+ payload = _json.dumps({
2356
+ "ai_tool": "claude-code",
2357
+ "tool_call": tool,
2358
+ "input_summary": f"[TEST] {test_input}",
2359
+ "decision": action if action in ("blocked", "warn") else "blocked",
2360
+ "rule_id": rule_id,
2361
+ "rule_message": f"[TEST] {message}",
2362
+ }).encode()
2363
+
2364
+ action_color = RED if action == "blocked" else (YELLOW if action == "warn" else CYAN)
2365
+ action_label = action.upper()
2366
+
2367
+ if workspace_id:
2368
+ try:
2369
+ req = urllib.request.Request(
2370
+ f"{api_url}/guard/events/test?workspace_id={workspace_id}",
2371
+ data=payload,
2372
+ headers={"Content-Type": "application/json", "X-Api-Key": api_key},
2373
+ method="POST",
2374
+ )
2375
+ with urllib.request.urlopen(req, timeout=5):
2376
+ pass
2377
+ posted = f"{GRAY}posted{RESET}"
2378
+ except Exception:
2379
+ posted = f"{GRAY}local only{RESET}"
2380
+ else:
2381
+ posted = f"{GRAY}no workspace{RESET}"
2382
+
2383
+ print(f" {action_color}{action_label:<8}{RESET} {rule_id:<35} {GRAY}{message[:50]}{RESET} {posted}")
2384
+ if action == "blocked":
2385
+ blocked += 1
2386
+ else:
2387
+ allowed += 1
2388
+
2389
+ print(f"\n {blocked} would block · {allowed} audit/allow")
2390
+ print(f"\n {CYAN}→ View events: {api_url.replace('api.', 'app.')}/guard/activity{RESET}\n")
2391
+
2392
+
2217
2393
  # ── Entry point ───────────────────────────────────────────────────────────────
2218
2394
 
2219
2395
  def main():
@@ -2356,6 +2532,13 @@ def main():
2356
2532
  mcp_sub = mcp_p.add_subparsers(dest="mcp_command")
2357
2533
  mcp_sub.add_parser("install", help="Register conduct-mcp in Claude Code and Codex")
2358
2534
 
2535
+ # conduct sync
2536
+ sub.add_parser("sync", help="Sync Guard policies (and Security Loop policies if installed)")
2537
+
2538
+ # conduct test-guard / test-security
2539
+ sub.add_parser("test-guard", help="Fire a synthetic event per guard policy rule and show decisions")
2540
+ sub.add_parser("test-security", help="Post a synthetic finding per security classifier pattern")
2541
+
2359
2542
  args = parser.parse_args()
2360
2543
 
2361
2544
  if args.command == "login":
@@ -2423,6 +2606,12 @@ def main():
2423
2606
  cmd_mcp_install(args)
2424
2607
  else:
2425
2608
  mcp_p.print_help()
2609
+ elif args.command == "sync":
2610
+ cmd_sync(args)
2611
+ elif args.command == "test-guard":
2612
+ cmd_test_guard(args)
2613
+ elif args.command == "test-security":
2614
+ cmd_test_security(args)
2426
2615
  else:
2427
2616
  parser.print_help()
2428
2617
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.56
3
+ Version: 0.4.58
4
4
  Summary: CLI for Conduct AI — 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