conduct-cli 0.4.55__tar.gz → 0.4.57__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.55 → conduct_cli-0.4.57}/PKG-INFO +1 -1
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/pyproject.toml +1 -1
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/src/conduct_cli/guard.py +1 -0
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/src/conduct_cli/main.py +189 -0
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/README.md +0 -0
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/setup.cfg +0 -0
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/setup.py +0 -0
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/src/conduct_cli/guardmcp.py +0 -0
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/src/conduct_cli/mcp_server.py +0 -0
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.4.55 → conduct_cli-0.4.57}/tests/test_switch.py +0 -0
|
@@ -304,6 +304,7 @@ def _maybe_emit_security_finding(tool_response, session_id, tool_name):
|
|
|
304
304
|
"type": finding_type,
|
|
305
305
|
"description": description,
|
|
306
306
|
"source_run_id": session_id,
|
|
307
|
+
"reporter_email": cfg.get("user_email") or "",
|
|
307
308
|
})
|
|
308
309
|
script = (
|
|
309
310
|
"import urllib.request\\n"
|
|
@@ -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
|
+
"""Run guard sync + security policy sync in one shot."""
|
|
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 + Security Loop in one shot")
|
|
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
|