conduct-cli 0.4.63__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.
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/PKG-INFO +1 -1
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/pyproject.toml +1 -1
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/src/conduct_cli/main.py +159 -3
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/README.md +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/setup.cfg +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/setup.py +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/src/conduct_cli/guard.py +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/src/conduct_cli/hook_precompact_template.py +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/src/conduct_cli/hook_session_start_template.py +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/src/conduct_cli/hook_template.py +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/tests/test_hook_syntax.py +0 -0
- {conduct_cli-0.4.63 → conduct_cli-0.4.64}/tests/test_switch.py +0 -0
|
@@ -2319,6 +2319,159 @@ def cmd_test_security(args):
|
|
|
2319
2319
|
print(f"\n {CYAN}→ View findings: {api_url.replace('api.', 'app.')}/secure/activity{RESET}\n")
|
|
2320
2320
|
|
|
2321
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
|
+
|
|
2322
2475
|
def cmd_test_guard(args):
|
|
2323
2476
|
"""Test each guard policy rule with a matching synthetic tool call."""
|
|
2324
2477
|
import json as _json
|
|
@@ -2556,9 +2709,10 @@ def main():
|
|
|
2556
2709
|
# conduct sync
|
|
2557
2710
|
sub.add_parser("sync", help="Sync Guard policies (and Security Loop policies if installed)")
|
|
2558
2711
|
|
|
2559
|
-
# conduct test-guard / test-security
|
|
2560
|
-
sub.add_parser("test-guard",
|
|
2561
|
-
sub.add_parser("test-security",
|
|
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")
|
|
2562
2716
|
|
|
2563
2717
|
args = parser.parse_args()
|
|
2564
2718
|
|
|
@@ -2633,6 +2787,8 @@ def main():
|
|
|
2633
2787
|
cmd_test_guard(args)
|
|
2634
2788
|
elif args.command == "test-security":
|
|
2635
2789
|
cmd_test_security(args)
|
|
2790
|
+
elif args.command == "test-security-verify":
|
|
2791
|
+
cmd_test_security_verify(args)
|
|
2636
2792
|
else:
|
|
2637
2793
|
parser.print_help()
|
|
2638
2794
|
|
|
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
|