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.
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/PKG-INFO +1 -1
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/pyproject.toml +1 -1
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli/guard.py +10 -2
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli/main.py +189 -0
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/README.md +0 -0
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/setup.cfg +0 -0
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/setup.py +0 -0
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.4.56 → conduct_cli-0.4.58}/tests/test_switch.py +0 -0
|
@@ -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
|
-
|
|
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
|
|
|
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
|