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.
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/PKG-INFO +1 -1
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/pyproject.toml +1 -1
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/main.py +174 -3
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/README.md +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/setup.cfg +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/setup.py +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/guard.py +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/hook_precompact_template.py +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/hook_session_start_template.py +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/hook_template.py +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/tests/test_hook_syntax.py +0 -0
- {conduct_cli-0.4.62 → conduct_cli-0.4.64}/tests/test_switch.py +0 -0
|
@@ -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",
|
|
2546
|
-
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")
|
|
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
|
|
|
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
|