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.
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/PKG-INFO +1 -1
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/pyproject.toml +1 -1
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/posttooluse.py +1 -135
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/main.py +2 -402
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli.egg-info/PKG-INFO +1 -1
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_cli_matrix.py +0 -1
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/LICENSE +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/README.md +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/setup.cfg +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/setup.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/__init__.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/api.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/guard.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/guard_policy.md +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/__init__.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/base.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/precompact.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/pretooluse.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/session_parser.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/session_report_push.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/session_start.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/hooks/stop.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/log_util.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/memory.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/paxel.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli/tool_groups.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli.egg-info/entry_points.txt +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli.egg-info/requires.txt +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/src/conduct_cli.egg-info/top_level.txt +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_advisory_and_hook.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_api_client.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_bash_operator_signature.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_bash_scan_target.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_blast_radius.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_cmd_run.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_command_word_matcher.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_drain_daemon_health.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_guard_approvals.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_guard_policy.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_guard_savings.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_hook_dispatch.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_hook_receipt.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_instruction_files.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_journal_drain.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_log_util.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_pretooluse_approval_e2e.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_pretooluse_check_policy.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_proxy_coverage.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_proxy_env.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_report_tool_coverage.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_signed_policy.py +0 -0
- {conduct_cli-0.12.0 → conduct_cli-0.13.0}/tests/test_switch.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
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
|
|
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 /
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|