conduct-cli 0.4.62__tar.gz → 0.4.64__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 (24) hide show
  1. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/PKG-INFO +1 -1
  2. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/pyproject.toml +1 -1
  3. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/main.py +174 -3
  4. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/PKG-INFO +1 -1
  5. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/README.md +0 -0
  6. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/setup.cfg +0 -0
  7. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/setup.py +0 -0
  8. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/__init__.py +0 -0
  9. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/api.py +0 -0
  10. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/guard.py +0 -0
  11. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/guardmcp.py +0 -0
  12. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/hook_precompact_template.py +0 -0
  13. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/hook_session_start_template.py +0 -0
  14. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/hook_template.py +0 -0
  15. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/mcp_server.py +0 -0
  16. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
  17. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  18. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  19. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/requires.txt +0 -0
  20. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/top_level.txt +0 -0
  21. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/tests/test_guard_policy.py +0 -0
  22. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/tests/test_guard_savings.py +0 -0
  23. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/tests/test_hook_syntax.py +0 -0
  24. {conduct_cli-0.4.62 → conduct_cli-0.4.64}/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.62
3
+ Version: 0.4.64
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.62"
7
+ version = "0.4.64"
8
8
  description = "CLI for Conduct AI — install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -2267,6 +2267,21 @@ def cmd_test_security(args):
2267
2267
 
2268
2268
  print(f"\n{BOLD}▶ conduct test-security — {len(_SECURITY_TEST_CASES)} patterns{RESET}\n")
2269
2269
 
2270
+ # Clean up previous test run findings before inserting fresh ones
2271
+ try:
2272
+ req = urllib.request.Request(
2273
+ f"{api_url}/security-findings?workspace_id={workspace_id}&source_run_id=conduct-test-security",
2274
+ headers={"X-Api-Key": api_key},
2275
+ method="DELETE",
2276
+ )
2277
+ with urllib.request.urlopen(req, timeout=8) as resp:
2278
+ r = _json.loads(resp.read())
2279
+ n = r.get("deleted", 0)
2280
+ if n:
2281
+ print(f" {GRAY}↺ Cleaned {n} previous test finding{'s' if n != 1 else ''}{RESET}\n")
2282
+ except Exception:
2283
+ pass # cleanup is best-effort
2284
+
2270
2285
  passed = 0
2271
2286
  failed = 0
2272
2287
  for name, vtype, severity, description, test_file, test_line in _SECURITY_TEST_CASES:
@@ -2304,6 +2319,159 @@ def cmd_test_security(args):
2304
2319
  print(f"\n {CYAN}→ View findings: {api_url.replace('api.', 'app.')}/secure/activity{RESET}\n")
2305
2320
 
2306
2321
 
2322
+ def cmd_test_security_verify(args):
2323
+ """Post test findings and verify the full triage pipeline end-to-end."""
2324
+ from conduct_cli.guard import CONFIG_PATH
2325
+ import json as _json
2326
+ import time as _time
2327
+
2328
+ try:
2329
+ cfg = _json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
2330
+ except Exception:
2331
+ cfg = {}
2332
+
2333
+ workspace_id = cfg.get("workspace_id")
2334
+ api_key = cfg.get("api_key", "")
2335
+ user_email = cfg.get("user_email", "")
2336
+ api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
2337
+
2338
+ if not workspace_id:
2339
+ print(f"{RED}Not configured. Run: conduct guard setup{RESET}")
2340
+ sys.exit(1)
2341
+
2342
+ import urllib.request
2343
+
2344
+ TIMEOUT = 120 # seconds to wait for all findings to leave "open"
2345
+ POLL_SECS = 5
2346
+
2347
+ # ── Step 1: post test findings ────────────────────────────────────────
2348
+ print(f"\n{BOLD}▶ conduct test-security-verify{RESET}")
2349
+ print(f" {GRAY}Step 1/3 — posting {len(_SECURITY_TEST_CASES)} test findings…{RESET}\n")
2350
+
2351
+ # Clean previous run
2352
+ try:
2353
+ req = urllib.request.Request(
2354
+ f"{api_url}/security-findings?workspace_id={workspace_id}&source_run_id=conduct-test-security",
2355
+ headers={"X-Api-Key": api_key},
2356
+ method="DELETE",
2357
+ )
2358
+ with urllib.request.urlopen(req, timeout=8) as resp:
2359
+ r = _json.loads(resp.read())
2360
+ n = r.get("deleted", 0)
2361
+ if n:
2362
+ print(f" {GRAY}↺ Cleaned {n} previous test finding{'s' if n != 1 else ''}{RESET}\n")
2363
+ except Exception:
2364
+ pass
2365
+
2366
+ finding_ids: list[str] = []
2367
+ for name, vtype, severity, description, test_file, test_line in _SECURITY_TEST_CASES:
2368
+ body: dict = {
2369
+ "tool": "claude-code",
2370
+ "severity": severity,
2371
+ "type": vtype,
2372
+ "description": f"[TEST] {description}",
2373
+ "reporter_email": user_email,
2374
+ "source_run_id": "conduct-test-security",
2375
+ }
2376
+ if test_file:
2377
+ body["file"] = test_file
2378
+ if test_line is not None:
2379
+ body["line"] = test_line
2380
+ payload = _json.dumps(body).encode()
2381
+ try:
2382
+ req = urllib.request.Request(
2383
+ f"{api_url}/security-findings?workspace_id={workspace_id}",
2384
+ data=payload,
2385
+ headers={"Content-Type": "application/json", "X-Api-Key": api_key},
2386
+ method="POST",
2387
+ )
2388
+ with urllib.request.urlopen(req, timeout=8) as resp:
2389
+ r = _json.loads(resp.read())
2390
+ fid = r.get("id", "")
2391
+ finding_ids.append(fid)
2392
+ sev_color = RED if severity == "critical" else (YELLOW if severity == "high" else CYAN)
2393
+ print(f" {GREEN}✓{RESET} {name:<22} {sev_color}{severity:<8}{RESET} {GRAY}{fid[:8]}{RESET}")
2394
+ except Exception as e:
2395
+ print(f" {RED}✕{RESET} {name:<22} {RED}FAILED — {e}{RESET}")
2396
+
2397
+ if not finding_ids:
2398
+ print(f"\n{RED}✗ No findings posted — aborting.{RESET}\n")
2399
+ sys.exit(1)
2400
+
2401
+ # ── Step 2: poll until all findings move off "open" ───────────────────
2402
+ print(f"\n {GRAY}Step 2/3 — waiting for triage pipeline (timeout {TIMEOUT}s)…{RESET}\n")
2403
+ deadline = _time.time() + TIMEOUT
2404
+ final_statuses: dict[str, str] = {}
2405
+
2406
+ while _time.time() < deadline:
2407
+ try:
2408
+ req = urllib.request.Request(
2409
+ f"{api_url}/security-findings?workspace_id={workspace_id}&source_run_id=conduct-test-security&limit=100",
2410
+ headers={"X-Api-Key": api_key},
2411
+ )
2412
+ with urllib.request.urlopen(req, timeout=10) as resp:
2413
+ findings = _json.loads(resp.read())
2414
+ except Exception as e:
2415
+ print(f" {YELLOW}⚠ poll error: {e}{RESET}")
2416
+ _time.sleep(POLL_SECS)
2417
+ continue
2418
+
2419
+ for f in findings:
2420
+ if f["id"] in finding_ids:
2421
+ final_statuses[f["id"]] = f["status"]
2422
+
2423
+ still_open = [fid for fid in finding_ids if final_statuses.get(fid) == "open"]
2424
+ done_count = len(finding_ids) - len(still_open)
2425
+ elapsed = int(_time.time() - (deadline - TIMEOUT))
2426
+ print(f" {GRAY}[{elapsed:>3}s] {done_count}/{len(finding_ids)} processed…{RESET}", end="\r")
2427
+
2428
+ if not still_open:
2429
+ print() # newline after \r
2430
+ break
2431
+ _time.sleep(POLL_SECS)
2432
+ else:
2433
+ print(f"\n\n {RED}✗ Timeout — {len(still_open)} finding(s) still 'open' after {TIMEOUT}s{RESET}\n")
2434
+
2435
+ # ── Step 3: report per-finding results ────────────────────────────────
2436
+ print(f"\n {GRAY}Step 3/3 — results{RESET}\n")
2437
+
2438
+ all_pass = True
2439
+ name_by_id = {}
2440
+ for i, (name, *_) in enumerate(_SECURITY_TEST_CASES):
2441
+ if i < len(finding_ids):
2442
+ name_by_id[finding_ids[i]] = name
2443
+
2444
+ for fid in finding_ids:
2445
+ status = final_statuses.get(fid, "open")
2446
+ name = name_by_id.get(fid, fid[:8])
2447
+ if status == "open":
2448
+ icon = f"{RED}✗{RESET}"
2449
+ note = f"{RED}still open — triage did not run{RESET}"
2450
+ all_pass = False
2451
+ elif status == "dismissed":
2452
+ icon = f"{CYAN}○{RESET}"
2453
+ note = f"{CYAN}dismissed (false positive){RESET}"
2454
+ elif status == "triaging":
2455
+ icon = f"{YELLOW}◑{RESET}"
2456
+ note = f"{YELLOW}triaging (real finding, no autopilot fix){RESET}"
2457
+ elif status == "fixed":
2458
+ icon = f"{GREEN}✓{RESET}"
2459
+ note = f"{GREEN}fixed{RESET}"
2460
+ else:
2461
+ icon = f"{GRAY}?{RESET}"
2462
+ note = f"{GRAY}{status}{RESET}"
2463
+ print(f" {icon} {name:<22} → {note}")
2464
+
2465
+ print()
2466
+ if all_pass:
2467
+ print(f" {GREEN}{BOLD}✓ All findings processed — triage pipeline OK{RESET}\n")
2468
+ else:
2469
+ print(f" {RED}{BOLD}✗ Some findings were not processed — check Security Automation project runs{RESET}")
2470
+ app_url = api_url.replace("api.", "app.")
2471
+ print(f" {CYAN}→ {app_url}/projects{RESET}\n")
2472
+ sys.exit(1)
2473
+
2474
+
2307
2475
  def cmd_test_guard(args):
2308
2476
  """Test each guard policy rule with a matching synthetic tool call."""
2309
2477
  import json as _json
@@ -2541,9 +2709,10 @@ def main():
2541
2709
  # conduct sync
2542
2710
  sub.add_parser("sync", help="Sync Guard policies (and Security Loop policies if installed)")
2543
2711
 
2544
- # conduct test-guard / test-security
2545
- sub.add_parser("test-guard", help="Fire a synthetic event per guard policy rule and show decisions")
2546
- sub.add_parser("test-security", help="Post a synthetic finding per security classifier pattern")
2712
+ # conduct test-guard / test-security / test-security-verify
2713
+ sub.add_parser("test-guard", help="Fire a synthetic event per guard policy rule and show decisions")
2714
+ sub.add_parser("test-security", help="Post a synthetic finding per security classifier pattern")
2715
+ sub.add_parser("test-security-verify", help="Post test findings and verify full triage pipeline end-to-end")
2547
2716
 
2548
2717
  args = parser.parse_args()
2549
2718
 
@@ -2618,6 +2787,8 @@ def main():
2618
2787
  cmd_test_guard(args)
2619
2788
  elif args.command == "test-security":
2620
2789
  cmd_test_security(args)
2790
+ elif args.command == "test-security-verify":
2791
+ cmd_test_security_verify(args)
2621
2792
  else:
2622
2793
  parser.print_help()
2623
2794
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.62
3
+ Version: 0.4.64
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