conduct-cli 0.12.0__tar.gz → 0.13.0__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 (55) hide show
  1. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/PKG-INFO +1 -1
  2. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/pyproject.toml +1 -1
  3. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/posttooluse.py +1 -135
  4. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/main.py +2 -402
  5. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli.egg-info/PKG-INFO +1 -1
  6. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_cli_matrix.py +0 -1
  7. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/LICENSE +0 -0
  8. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/README.md +0 -0
  9. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/setup.cfg +0 -0
  10. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/setup.py +0 -0
  11. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/__init__.py +0 -0
  12. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/api.py +0 -0
  13. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/guard.py +0 -0
  14. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/guard_policy.md +0 -0
  15. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/__init__.py +0 -0
  16. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/base.py +0 -0
  17. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/precompact.py +0 -0
  18. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/pretooluse.py +0 -0
  19. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/session_parser.py +0 -0
  20. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/session_report_push.py +0 -0
  21. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/session_start.py +0 -0
  22. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/stop.py +0 -0
  23. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/log_util.py +0 -0
  24. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/memory.py +0 -0
  25. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/paxel.py +0 -0
  26. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/tool_groups.py +0 -0
  27. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
  28. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  29. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  30. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli.egg-info/requires.txt +0 -0
  31. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli.egg-info/top_level.txt +0 -0
  32. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_advisory_and_hook.py +0 -0
  33. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_api_client.py +0 -0
  34. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_bash_operator_signature.py +0 -0
  35. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_bash_scan_target.py +0 -0
  36. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_blast_radius.py +0 -0
  37. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_cmd_run.py +0 -0
  38. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_command_word_matcher.py +0 -0
  39. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_drain_daemon_health.py +0 -0
  40. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_guard_approvals.py +0 -0
  41. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_guard_policy.py +0 -0
  42. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_guard_savings.py +0 -0
  43. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_hook_dispatch.py +0 -0
  44. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_hook_receipt.py +0 -0
  45. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_instruction_files.py +0 -0
  46. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_journal_drain.py +0 -0
  47. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_log_util.py +0 -0
  48. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_pretooluse_approval_e2e.py +0 -0
  49. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_pretooluse_check_policy.py +0 -0
  50. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_proxy_coverage.py +0 -0
  51. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_proxy_env.py +0 -0
  52. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_report_tool_coverage.py +0 -0
  53. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_signed_policy.py +0 -0
  54. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_switch.py +0 -0
  55. {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_token_lifecycle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  Summary: Runtime governance for AI agents. CLI for Conduct AI — secure, govern, install agents, manage projects, run tests
5
5
  Author-email: Conduct AI <hello@conductai.ai>
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "conduct-cli"
7
- version = "0.12.0"
7
+ version = "0.13.0"
8
8
  description = "Runtime governance for AI agents. CLI for Conduct AI — secure, govern, install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
@@ -1,4 +1,4 @@
1
- """ConductGuard PostToolUse hook — token tracking + security classifier."""
1
+ """ConductGuard PostToolUse hook — token tracking."""
2
2
  from __future__ import annotations
3
3
 
4
4
  import json
@@ -20,139 +20,6 @@ from conduct_cli.hooks.pretooluse import (
20
20
  check_policy,
21
21
  )
22
22
 
23
- # ── Security classifier ───────────────────────────────────────────────────────
24
-
25
- SECRET_PATTERNS = [
26
- (r"sk-[A-Za-z0-9]{20,}", "secret-leak", "high", "Potential OpenAI/Anthropic API key"),
27
- (r"ghp_[A-Za-z0-9]{36}", "secret-leak", "high", "GitHub Personal Access Token"),
28
- (r"AKIA[0-9A-Z]{16}", "secret-leak", "critical", "AWS Access Key ID"),
29
- (r"Bearer\s+[A-Za-z0-9+/=]{20,}", "secret-leak", "high", "Bearer token in output"),
30
- (r"""password\s*=\s*['"][^'"]{4,}""", "secret-leak", "high", "Hardcoded password"),
31
- (r"""api[_-]?key\s*=\s*['"][^'"]{4,}""", "secret-leak", "high", "Hardcoded API key"),
32
- (r"\.\./\.\./\.\./", "path-traversal", "medium", "Path traversal sequence"),
33
- (r"file://", "path-traversal", "medium", "File URI scheme in output"),
34
- (r"eval\s*\(", "injection", "high", "eval() in output"),
35
- (r"exec\s*\(", "injection", "high", "exec() in output"),
36
- (r"__import__\s*\(", "injection", "high", "__import__() in output"),
37
- (r"ssl\.CERT_NONE", "crypto", "high", "SSL verification disabled"),
38
- (r"verify\s*=\s*False", "crypto", "medium", "TLS verification bypassed"),
39
- ]
40
- OWASP_KEYWORDS = [
41
- ("sql injection", "injection", "high", "SQL injection mentioned in AI output"),
42
- ("cross-site scripting", "injection", "high", "XSS mentioned in AI output"),
43
- (" xss ", "injection", "high", "XSS mentioned in AI output"),
44
- ("idor", "injection", "medium", "IDOR mentioned in AI output"),
45
- ("ssrf", "injection", "high", "SSRF mentioned in AI output"),
46
- ("command injection", "injection", "high", "Command injection mentioned in AI output"),
47
- ("auth bypass", "auth-bypass", "high", "Auth bypass mentioned in AI output"),
48
- ]
49
-
50
-
51
- def _classify_text(text: str):
52
- import re as _re
53
- for pattern, ftype, sev, desc in SECRET_PATTERNS:
54
- if _re.search(pattern, text, _re.IGNORECASE):
55
- return ftype, sev, desc, pattern
56
- lower = text.lower()
57
- for kw, ftype, sev, desc in OWASP_KEYWORDS:
58
- if kw in lower:
59
- return ftype, sev, desc, kw
60
- return None, None, None, None
61
-
62
-
63
- def _line_number_from_text(text: str, matched_pattern: str):
64
- import re as _re
65
- if not matched_pattern:
66
- return None
67
- try:
68
- for raw_line in text.splitlines():
69
- m = _re.match(r"^\s*(\d+)\t(.*)$", raw_line)
70
- if m:
71
- lineno, content = int(m.group(1)), m.group(2)
72
- try:
73
- if _re.search(matched_pattern, content, _re.IGNORECASE):
74
- return lineno
75
- except Exception:
76
- if matched_pattern.lower() in content.lower():
77
- return lineno
78
- m = _re.search(matched_pattern, text, _re.IGNORECASE)
79
- if m:
80
- return text[:m.start()].count(chr(10)) + 1
81
- except Exception:
82
- pass
83
- return None
84
-
85
-
86
- def _maybe_emit_security_finding(tool_response, session_id, tool_name, tool_input=None) -> None:
87
- """Classify tool output + input for security findings; POST if flag ON. Never raises."""
88
- try:
89
- cfg = load_config()
90
- except Exception:
91
- return
92
- if not cfg.get("security_emit_enabled", False):
93
- return
94
- workspace_id = cfg.get("workspace_id")
95
- agent_token = cfg.get("agent_token", "")
96
- api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
97
- if not workspace_id:
98
- return
99
-
100
- ti = tool_input or {}
101
- candidates = [("response", str(tool_response))]
102
- if tool_name in ("edit", "multiedit"):
103
- candidates.append(("input", ti.get("new_string", "")))
104
- elif tool_name == "write":
105
- candidates.append(("input", ti.get("content", "")))
106
- elif tool_name in ("bash", "terminal"):
107
- candidates.append(("input", ti.get("command", "")))
108
-
109
- finding_type = severity = description = matched_pattern = scan_text = None
110
- for _src, text in candidates:
111
- ft, sv, desc, pat = _classify_text(text)
112
- if ft:
113
- finding_type, severity, description, matched_pattern, scan_text = ft, sv, desc, pat, text
114
- break
115
-
116
- if not finding_type:
117
- return
118
-
119
- file_path = (
120
- ti.get("file_path") or ti.get("path") or
121
- (ti.get("command", "")[:120] if tool_name in ("bash", "terminal") else None)
122
- ) or None
123
-
124
- line_no = _line_number_from_text(scan_text, matched_pattern) if scan_text else None
125
-
126
- payload = json.dumps({
127
- "tool": detect_ai_tool(),
128
- "severity": severity,
129
- "type": finding_type,
130
- "description": description,
131
- "source_run_id": session_id,
132
- "reporter_email": cfg.get("user_email") or "",
133
- "file": file_path,
134
- "line": line_no,
135
- })
136
- script = (
137
- "import urllib.request\n"
138
- "try:\n"
139
- f" req = urllib.request.Request(\"{api_url}/security-findings?workspace_id={workspace_id}\","
140
- f" data={repr(payload.encode())}, headers={{\"Content-Type\": \"application/json\","
141
- f" \"Authorization\": \"Bearer {agent_token}\"}}, method=\"POST\")\n"
142
- " urllib.request.urlopen(req, timeout=5)\n"
143
- "except: pass\n"
144
- )
145
- try:
146
- import subprocess as _sp
147
- _sp.Popen(
148
- [sys.executable, "-c", script],
149
- stdout=_sp.DEVNULL,
150
- stderr=_sp.DEVNULL,
151
- start_new_session=True,
152
- )
153
- except Exception:
154
- pass
155
-
156
23
 
157
24
  # ── Token reading ─────────────────────────────────────────────────────────────
158
25
 
@@ -442,7 +309,6 @@ def main() -> None:
442
309
  if action == "warn" and session_id and rule_id:
443
310
  _record_session_warn(session_id, rule_id)
444
311
  post_event(tool_name, {}, decision, rule_id, message, session_id, drain_via=_this_file, blast_radius=blast_radius)
445
- _maybe_emit_security_finding(str(tool_response), session_id, tool_name, tool_input)
446
312
 
447
313
  sys.exit(0)
448
314
 
@@ -1373,7 +1373,6 @@ _ALL_SLUGS = [
1373
1373
  "release_notes",
1374
1374
  "issue_triage",
1375
1375
  "copilot_reviewer",
1376
- "security_scanner",
1377
1376
  "security_patch_updater",
1378
1377
  "smoke_test",
1379
1378
  "flaky_test_detective",
@@ -1396,7 +1395,6 @@ _FRIENDLY_NAMES = {
1396
1395
  "release_notes": "Release Notes",
1397
1396
  "issue_triage": "Issue Triage",
1398
1397
  "copilot_reviewer": "Copilot / AI PR Reviewer",
1399
- "security_scanner": "Security Scanner",
1400
1398
  "security_patch_updater": "Security Patch Updater",
1401
1399
  "smoke_test": "Smoke Test",
1402
1400
  "flaky_test_detective": "Flaky Test Detective",
@@ -2244,67 +2242,6 @@ def cmd_sessions(args):
2244
2242
  print(_render_table(rows))
2245
2243
 
2246
2244
 
2247
- # ── Security finding classifier ───────────────────────────────────────────────
2248
-
2249
- import re as _re
2250
-
2251
- _SEVERITY_KEYWORDS = {
2252
- "critical": ["rce", "remote code execution", "sql injection", "sqli", "command injection"],
2253
- "high": ["xss", "cross-site", "path traversal", "directory traversal", "auth bypass",
2254
- "authentication bypass", "idor", "privilege escalation", "ssrf"],
2255
- "medium": ["csrf", "open redirect", "clickjacking", "insecure deserialization", "xxe"],
2256
- "low": ["information disclosure", "verbose error", "stack trace exposed", "version disclosure"],
2257
- "info": ["todo security", "fixme security", "hardcoded", "weak cipher"],
2258
- }
2259
-
2260
- _SECRET_PATTERNS = [
2261
- r'(?i)(api[_-]?key|secret|password|token|private[_-]?key)\s*[:=]\s*["\']?[\w\-]{8,}',
2262
- r'(?i)sk-[a-zA-Z0-9]{20,}', # OpenAI key pattern
2263
- r'(?i)ghp_[a-zA-Z0-9]{36}', # GitHub PAT
2264
- ]
2265
-
2266
- _VULN_TYPES = {
2267
- "injection": ["sql injection", "sqli", "command injection", "code injection", "ldap injection"],
2268
- "path-traversal": ["path traversal", "directory traversal", "../"],
2269
- "secret-leak": ["hardcoded", "secret", "api key", "password", "token", "private key"],
2270
- "auth-bypass": ["auth bypass", "authentication bypass", "idor", "privilege escalation"],
2271
- "crypto": ["weak cipher", "md5", "sha1", "tls 1.0", "ssl 2.0", "cert_none"],
2272
- }
2273
-
2274
-
2275
- def classify_finding(text: str) -> "dict | None":
2276
- """
2277
- Fast-path classifier. Returns structured finding dict or None if not a security issue.
2278
- Checks secret patterns first (regex), then keyword severity matching.
2279
- """
2280
- text_lower = text.lower()
2281
-
2282
- # Secret detection (regex fast path)
2283
- for pattern in _SECRET_PATTERNS:
2284
- if _re.search(pattern, text):
2285
- return {
2286
- "severity": "high",
2287
- "type": "secret-leak",
2288
- "description": text[:500],
2289
- }
2290
-
2291
- # Severity + type matching
2292
- for severity, keywords in _SEVERITY_KEYWORDS.items():
2293
- for kw in keywords:
2294
- if kw in text_lower:
2295
- vuln_type = "other"
2296
- for vtype, vkws in _VULN_TYPES.items():
2297
- if any(vk in text_lower for vk in vkws):
2298
- vuln_type = vtype
2299
- break
2300
- return {
2301
- "severity": severity,
2302
- "type": vuln_type,
2303
- "description": text[:500],
2304
- }
2305
- return None
2306
-
2307
-
2308
2245
  def cmd_session_report(args):
2309
2246
  """Run paxel analysis and open an HTML report in the local browser."""
2310
2247
  import json as _json
@@ -2467,46 +2404,6 @@ def cmd_session_report(args):
2467
2404
  print(f" (Dashboard push failed: {_push_err})")
2468
2405
 
2469
2406
 
2470
- def cmd_emit_finding(args):
2471
- """POST a security finding to /security-findings."""
2472
- server, workspace_id, token = _require_auth(args)
2473
- hdrs = api.headers(workspace_id, token, "application/json")
2474
-
2475
- from_stdin = getattr(args, "from_stdin", False)
2476
-
2477
- if from_stdin:
2478
- raw = sys.stdin.read()
2479
- result = classify_finding(raw)
2480
- if result is None:
2481
- print("No security finding detected")
2482
- sys.exit(0)
2483
- severity = result["severity"]
2484
- vuln_type = result["type"]
2485
- description = result["description"]
2486
- else:
2487
- severity = args.severity
2488
- vuln_type = args.type
2489
- description = args.description
2490
-
2491
- payload: dict = {
2492
- "severity": severity,
2493
- "type": vuln_type,
2494
- "description": description,
2495
- }
2496
- if getattr(args, "file", None):
2497
- payload["file"] = args.file
2498
- if getattr(args, "line", None) is not None:
2499
- payload["line"] = args.line
2500
- if getattr(args, "repo", None):
2501
- payload["repo"] = args.repo
2502
-
2503
- result = api.req("POST", f"{server}/security-findings", hdrs, payload)
2504
- finding_id = result.get("id", result.get("finding_id", ""))
2505
- print(f"[FINDING] {severity} · {vuln_type} · {finding_id}")
2506
- if finding_id:
2507
- print(finding_id)
2508
-
2509
-
2510
2407
  def _gh_api_get(url: str, token: str):
2511
2408
  import urllib.request, urllib.error
2512
2409
  req = urllib.request.Request(url, headers={
@@ -2715,7 +2612,7 @@ def cmd_run(args):
2715
2612
  _stream_run(server, workflow_id, run_id, workspace_id, token)
2716
2613
 
2717
2614
 
2718
- # ── conduct sync / test-guard / test-security ────────────────────────────────
2615
+ # ── conduct sync / test-guard ────────────────────────────────
2719
2616
 
2720
2617
  def _refresh_agent_token() -> bool:
2721
2618
  """Silently rotate agent_token using refresh_token. Returns True if refreshed."""
@@ -2764,271 +2661,6 @@ def cmd_sync(args):
2764
2661
  print(f"\n{GREEN}Sync complete.{RESET}\n")
2765
2662
 
2766
2663
 
2767
- _SECURITY_TEST_CASES = [
2768
- # (name, type, severity, description, file, line)
2769
- ("AWS Access Key", "secret-leak", "critical", "AKIA1234567890ABCDEF found in output", "test_vuln.py", 7),
2770
- ("OpenAI API Key", "secret-leak", "high", "sk-abcdefghijklmnopqrstuvwx1234567890 in response", "test_vuln.py", 8),
2771
- ("GitHub PAT", "secret-leak", "high", "ghp_" + "A" * 36 + " token present", "test_vuln.py", 8),
2772
- ("Bearer Token", "secret-leak", "high", "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.test.sig", "test_vuln.py", None),
2773
- ("Hardcoded Password", "secret-leak", "high", "password = 'hardcoded_secret_here'", "test_vuln.py", 11),
2774
- ("Hardcoded API Key", "secret-leak", "high", "api_key = 'abc123def456ghi789'", "test_vuln.py", 12),
2775
- ("Path Traversal", "path-traversal", "medium", "../../etc/passwd accessed", "test_vuln.py", 32),
2776
- ("File URI", "path-traversal", "medium", "file:///etc/passwd read", "test_vuln.py", None),
2777
- ("eval() Injection", "injection", "high", "eval(user_input) called in output", "test_vuln.py", 16),
2778
- ("exec() Injection", "injection", "high", "exec(command) called in output", "test_vuln.py", 20),
2779
- ("SSL Disabled", "crypto", "high", "ssl.CERT_NONE used — verification disabled", "test_vuln.py", 28),
2780
- ("TLS Bypass", "crypto", "medium", "verify=False passed to requests", "test_vuln.py", 23),
2781
- ("SQL Injection", "injection", "high", "sql injection vulnerability in query", "test_vuln.py", None),
2782
- ("XSS", "injection", "high", "cross-site scripting detected in output", "test_vuln.py", None),
2783
- ("Auth Bypass", "auth-bypass", "high", "auth bypass possible via missing check", "test_vuln.py", None),
2784
- ]
2785
-
2786
-
2787
- def cmd_test_security(args):
2788
- """Fire synthetic security findings for every classifier pattern."""
2789
- from conduct_cli.guard import CONFIG_PATH
2790
- try:
2791
- import json as _json
2792
- cfg = _json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
2793
- except Exception:
2794
- cfg = {}
2795
-
2796
- workspace_id = cfg.get("workspace_id")
2797
- api_key = cfg.get("agent_token", "")
2798
- user_email = cfg.get("user_email", "")
2799
- api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
2800
-
2801
- if not workspace_id:
2802
- print(f"{RED}Not configured. Run: conduct guard setup{RESET}")
2803
- sys.exit(1)
2804
-
2805
- import urllib.request
2806
- import json as _json
2807
-
2808
- print(f"\n{BOLD}▶ conduct test-security — {len(_SECURITY_TEST_CASES)} patterns{RESET}\n")
2809
-
2810
- # Clean up previous test run findings before inserting fresh ones
2811
- try:
2812
- req = urllib.request.Request(
2813
- f"{api_url}/security-findings?workspace_id={workspace_id}&source_run_id=conduct-test-security",
2814
- headers={"Authorization": f"Bearer {api_key}"},
2815
- method="DELETE",
2816
- )
2817
- with urllib.request.urlopen(req, timeout=8) as resp:
2818
- r = _json.loads(resp.read())
2819
- n = r.get("deleted", 0)
2820
- if n:
2821
- print(f" {GRAY}↺ Cleaned {n} previous test finding{'s' if n != 1 else ''}{RESET}\n")
2822
- except Exception:
2823
- pass # cleanup is best-effort
2824
-
2825
- passed = 0
2826
- failed = 0
2827
- for name, vtype, severity, description, test_file, test_line in _SECURITY_TEST_CASES:
2828
- body: dict = {
2829
- "tool": "claude-code",
2830
- "severity": severity,
2831
- "type": vtype,
2832
- "description": f"[TEST] {description}",
2833
- "reporter_email": user_email,
2834
- "source_run_id": "conduct-test-security",
2835
- }
2836
- if test_file:
2837
- body["file"] = test_file
2838
- if test_line is not None:
2839
- body["line"] = test_line
2840
- payload = _json.dumps(body).encode()
2841
- try:
2842
- req = urllib.request.Request(
2843
- f"{api_url}/security-findings?workspace_id={workspace_id}",
2844
- data=payload,
2845
- headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
2846
- method="POST",
2847
- )
2848
- with urllib.request.urlopen(req, timeout=8) as resp:
2849
- r = _json.loads(resp.read())
2850
- fid = r.get("id", "")[:8]
2851
- sev_color = RED if severity == "critical" else (YELLOW if severity == "high" else CYAN)
2852
- print(f" {GREEN}✓{RESET} {name:<22} {sev_color}{severity:<8}{RESET} {GRAY}{fid}{RESET}")
2853
- passed += 1
2854
- except Exception as e:
2855
- print(f" {RED}✕{RESET} {name:<22} {RED}FAILED — {e}{RESET}")
2856
- failed += 1
2857
-
2858
- print(f"\n {passed} posted · {failed} failed")
2859
- print(f"\n {CYAN}→ View findings: {api_url.replace('api.', 'app.')}/secure/activity{RESET}\n")
2860
-
2861
-
2862
- def cmd_test_security_verify(args):
2863
- """Post test findings and verify the full triage pipeline end-to-end."""
2864
- from conduct_cli.guard import CONFIG_PATH
2865
- import json as _json
2866
- import time as _time
2867
-
2868
- try:
2869
- cfg = _json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
2870
- except Exception:
2871
- cfg = {}
2872
-
2873
- workspace_id = cfg.get("workspace_id")
2874
- api_key = cfg.get("agent_token", "")
2875
- user_email = cfg.get("user_email", "")
2876
- api_url = cfg.get("api_url", "https://api.conductai.ai").rstrip("/")
2877
-
2878
- if not workspace_id:
2879
- print(f"{RED}Not configured. Run: conduct guard setup{RESET}")
2880
- sys.exit(1)
2881
-
2882
- import urllib.request
2883
-
2884
- TIMEOUT = 300 # 15 runs × ~28s / 4 workers ≈ 105s; 300s gives headroom for queue variance
2885
- POLL_SECS = 5
2886
-
2887
- # ── Step 0: fresh Security Automation project ─────────────────────────
2888
- print(f"\n{BOLD}▶ conduct test-security-verify{RESET}")
2889
- print(f" {GRAY}Step 0/3 — refreshing Security Automation project…{RESET}")
2890
- try:
2891
- req = urllib.request.Request(
2892
- f"{api_url}/secure/refresh-automation?workspace_id={workspace_id}",
2893
- data=b"{}",
2894
- headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
2895
- method="POST",
2896
- )
2897
- with urllib.request.urlopen(req, timeout=15) as resp:
2898
- _json.loads(resp.read())
2899
- print(f" {GREEN}✓{RESET} Fresh project created with latest YAML\n")
2900
- except Exception as e:
2901
- print(f" {YELLOW}⚠ refresh failed ({e}) — using existing project{RESET}\n")
2902
-
2903
- # ── Step 1: post test findings ────────────────────────────────────────
2904
- print(f" {GRAY}Step 1/3 — posting {len(_SECURITY_TEST_CASES)} test findings…{RESET}\n")
2905
-
2906
- # Clean previous run
2907
- try:
2908
- req = urllib.request.Request(
2909
- f"{api_url}/security-findings?workspace_id={workspace_id}&source_run_id=conduct-test-security",
2910
- headers={"Authorization": f"Bearer {api_key}"},
2911
- method="DELETE",
2912
- )
2913
- with urllib.request.urlopen(req, timeout=8) as resp:
2914
- r = _json.loads(resp.read())
2915
- n = r.get("deleted", 0)
2916
- if n:
2917
- print(f" {GRAY}↺ Cleaned {n} previous test finding{'s' if n != 1 else ''}{RESET}\n")
2918
- except Exception:
2919
- pass
2920
-
2921
- finding_ids: list[str] = []
2922
- for name, vtype, severity, description, test_file, test_line in _SECURITY_TEST_CASES:
2923
- body: dict = {
2924
- "tool": "claude-code",
2925
- "severity": severity,
2926
- "type": vtype,
2927
- "description": f"[TEST] {description}",
2928
- "reporter_email": user_email,
2929
- "source_run_id": "conduct-test-security",
2930
- }
2931
- if test_file:
2932
- body["file"] = test_file
2933
- if test_line is not None:
2934
- body["line"] = test_line
2935
- payload = _json.dumps(body).encode()
2936
- try:
2937
- req = urllib.request.Request(
2938
- f"{api_url}/security-findings?workspace_id={workspace_id}",
2939
- data=payload,
2940
- headers={"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"},
2941
- method="POST",
2942
- )
2943
- with urllib.request.urlopen(req, timeout=8) as resp:
2944
- r = _json.loads(resp.read())
2945
- fid = r.get("id", "")
2946
- finding_ids.append(fid)
2947
- sev_color = RED if severity == "critical" else (YELLOW if severity == "high" else CYAN)
2948
- print(f" {GREEN}✓{RESET} {name:<22} {sev_color}{severity:<8}{RESET} {GRAY}{fid[:8]}{RESET}")
2949
- except Exception as e:
2950
- print(f" {RED}✕{RESET} {name:<22} {RED}FAILED — {e}{RESET}")
2951
-
2952
- if not finding_ids:
2953
- print(f"\n{RED}✗ No findings posted — aborting.{RESET}\n")
2954
- sys.exit(1)
2955
-
2956
- # ── Step 2: poll until all findings move off "open" ───────────────────
2957
- print(f"\n {GRAY}Step 2/3 — waiting for triage pipeline (timeout {TIMEOUT}s)…{RESET}\n")
2958
-
2959
- deadline = _time.time() + TIMEOUT
2960
- final_statuses: dict[str, str] = {}
2961
-
2962
- while _time.time() < deadline:
2963
- try:
2964
- req = urllib.request.Request(
2965
- f"{api_url}/security-findings?workspace_id={workspace_id}&source_run_id=conduct-test-security&limit=100",
2966
- headers={"Authorization": f"Bearer {api_key}"},
2967
- )
2968
- with urllib.request.urlopen(req, timeout=10) as resp:
2969
- findings = _json.loads(resp.read())
2970
- except Exception as e:
2971
- print(f" {YELLOW}⚠ poll error: {e}{RESET}")
2972
- _time.sleep(POLL_SECS)
2973
- continue
2974
-
2975
- for f in findings:
2976
- if f["id"] in finding_ids:
2977
- final_statuses[f["id"]] = f["status"]
2978
-
2979
- still_open = [fid for fid in finding_ids if final_statuses.get(fid) == "open"]
2980
- done_count = len(finding_ids) - len(still_open)
2981
- elapsed = int(_time.time() - (deadline - TIMEOUT))
2982
- print(f" {GRAY}[{elapsed:>3}s] {done_count}/{len(finding_ids)} processed…{RESET}", end="\r")
2983
-
2984
- if not still_open:
2985
- print() # newline after \r
2986
- break
2987
- _time.sleep(POLL_SECS)
2988
- else:
2989
- print(f"\n\n {RED}✗ Timeout — {len(still_open)} finding(s) still 'open' after {TIMEOUT}s{RESET}\n")
2990
-
2991
- # ── Step 3: report per-finding results ────────────────────────────────
2992
- print(f"\n {GRAY}Step 3/3 — results{RESET}\n")
2993
-
2994
-
2995
- all_pass = True
2996
- name_by_id = {}
2997
- for i, (name, *_) in enumerate(_SECURITY_TEST_CASES):
2998
- if i < len(finding_ids):
2999
- name_by_id[finding_ids[i]] = name
3000
-
3001
- for fid in finding_ids:
3002
- status = final_statuses.get(fid, "open")
3003
- name = name_by_id.get(fid, fid[:8])
3004
- if status == "open":
3005
- icon = f"{RED}✗{RESET}"
3006
- note = f"{RED}still open — triage did not run{RESET}"
3007
- all_pass = False
3008
- elif status == "dismissed":
3009
- icon = f"{CYAN}○{RESET}"
3010
- note = f"{CYAN}dismissed (false positive){RESET}"
3011
- elif status == "triaging":
3012
- icon = f"{YELLOW}◑{RESET}"
3013
- note = f"{YELLOW}triaging (real finding, no autopilot fix){RESET}"
3014
- elif status == "fixed":
3015
- icon = f"{GREEN}✓{RESET}"
3016
- note = f"{GREEN}fixed{RESET}"
3017
- else:
3018
- icon = f"{GRAY}?{RESET}"
3019
- note = f"{GRAY}{status}{RESET}"
3020
- print(f" {icon} {name:<22} → {note}")
3021
-
3022
- print()
3023
- if all_pass:
3024
- print(f" {GREEN}{BOLD}✓ All findings processed — triage pipeline OK{RESET}\n")
3025
- else:
3026
- print(f" {RED}{BOLD}✗ Some findings were not processed — check Security Automation project runs{RESET}")
3027
- app_url = api_url.replace("api.", "app.")
3028
- print(f" {CYAN}→ {app_url}/projects{RESET}\n")
3029
- sys.exit(1)
3030
-
3031
-
3032
2664
  def cmd_test_guard(args):
3033
2665
  """Test each guard policy rule with a matching synthetic tool call."""
3034
2666
  import json as _json
@@ -3418,22 +3050,6 @@ def main():
3418
3050
  ia_p.add_argument("--input", action="append", metavar="key=value",
3419
3051
  help="Input value applied to all playbooks (repeatable)")
3420
3052
 
3421
- # conduct emit finding
3422
- emit_p = sub.add_parser("emit", help="Emit events to Conduct (e.g. security findings)")
3423
- emit_sub = emit_p.add_subparsers(dest="emit_command")
3424
-
3425
- finding_p = emit_sub.add_parser("finding", help="Emit a security finding to POST /security-findings")
3426
- finding_p.add_argument("--severity", choices=["critical", "high", "medium", "low", "info"],
3427
- help="Finding severity")
3428
- finding_p.add_argument("--type", dest="type", metavar="TYPE",
3429
- help="Vulnerability type (e.g. secret-leak, injection, path-traversal)")
3430
- finding_p.add_argument("--file", metavar="PATH", help="Source file path where finding was detected")
3431
- finding_p.add_argument("--line", type=int, metavar="N", help="Line number of finding")
3432
- finding_p.add_argument("--description", metavar="TEXT", help="Human-readable description of the finding")
3433
- finding_p.add_argument("--repo", metavar="owner/repo", help="Repository where finding was detected")
3434
- finding_p.add_argument("--from-stdin", dest="from_stdin", action="store_true",
3435
- help="Read raw tool output from stdin and auto-classify the finding")
3436
-
3437
3053
  # conduct run (existing)
3438
3054
  run_p = sub.add_parser("run", help="Run an installed agent by name")
3439
3055
  run_p.add_argument("agent", help="Agent name (e.g. 'security_autopilot_fix')")
@@ -3491,7 +3107,7 @@ def main():
3491
3107
  # conduct sync
3492
3108
  sub.add_parser("sync", help="Sync Guard policies (and Security Loop policies if installed)")
3493
3109
 
3494
- # conduct test-guard / test-security / test-security-verify
3110
+ # conduct test-guard / verify
3495
3111
  verify_p = sub.add_parser("verify", help="OWASP Agentic Top 10 coverage + governance grade")
3496
3112
  verify_p.add_argument("--evidence", metavar="FILE", default=None, help="Write evidence artifact to FILE.")
3497
3113
  verify_p.add_argument("--badge", action="store_true", help="Print markdown badge and exit.")
@@ -3501,8 +3117,6 @@ def main():
3501
3117
  verify_p.add_argument("--since", default="24h", help="Time window for blocked event check (e.g. 7d, 24h)")
3502
3118
  verify_p.add_argument("--run", action="store_true", help="Fire live adversarial test battery and show per-test verdicts.")
3503
3119
  sub.add_parser("test-guard", help="Fire a synthetic event per guard policy rule and show decisions")
3504
- sub.add_parser("test-security", help="Post a synthetic finding per security classifier pattern")
3505
- sub.add_parser("test-security-verify", help="Post test findings and verify full triage pipeline end-to-end")
3506
3120
  sr_p = sub.add_parser("session-report", help="Analyse local AI coding sessions with paxel and send report to admin")
3507
3121
  sr_p.add_argument("--developer", default=None, help="Developer name (defaults to OS username)")
3508
3122
 
@@ -3584,16 +3198,6 @@ def main():
3584
3198
  cmd_sessions(args)
3585
3199
  elif args.command == "guard":
3586
3200
  _guard.dispatch_guard(args, guard_p)
3587
- elif args.command == "emit":
3588
- emit_command = getattr(args, "emit_command", None)
3589
- if emit_command == "finding":
3590
- from_stdin = getattr(args, "from_stdin", False)
3591
- if not from_stdin and not (args.severity and args.type and args.description):
3592
- finding_p.print_help()
3593
- sys.exit(1)
3594
- cmd_emit_finding(args)
3595
- else:
3596
- emit_p.print_help()
3597
3201
  elif args.command == "mcp":
3598
3202
  if getattr(args, "mcp_command", None) == "install":
3599
3203
  cmd_mcp_install(args)
@@ -3609,10 +3213,6 @@ def main():
3609
3213
  _guard.cmd_verify(args)
3610
3214
  elif args.command == "test-guard":
3611
3215
  cmd_test_guard(args)
3612
- elif args.command == "test-security":
3613
- cmd_test_security(args)
3614
- elif args.command == "test-security-verify":
3615
- cmd_test_security_verify(args)
3616
3216
  elif args.command == "session-report":
3617
3217
  cmd_session_report(args)
3618
3218
  elif args.command == "memory":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
  Summary: Runtime governance for AI agents. CLI for Conduct AI — secure, govern, install agents, manage projects, run tests
5
5
  Author-email: Conduct AI <hello@conductai.ai>
6
6
  License: Apache-2.0
@@ -34,7 +34,6 @@ OFFLINE_ONLY: set[tuple[str, ...]] = {
34
34
  ("mcp", "install"), # writes local Claude/Codex config
35
35
  ("session-report",), # scans local files
36
36
  ("test-guard",), # local synthetic events
37
- ("test-security",), # local synthetic findings
38
37
  ("sessions",), # reads local Claude Code session directory
39
38
  ("whoami",), # status query — prints config state, no API call when unauth'd
40
39
  ("guard", "audit"), # reads local ~/.conduct/audit.log
File without changes
File without changes
File without changes
File without changes