conduct-cli 0.4.58__tar.gz → 0.4.60__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 (20) hide show
  1. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/PKG-INFO +1 -1
  2. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/pyproject.toml +1 -1
  3. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/src/conduct_cli/guard.py +102 -44
  4. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/src/conduct_cli.egg-info/PKG-INFO +1 -1
  5. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/README.md +0 -0
  6. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/setup.cfg +0 -0
  7. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/setup.py +0 -0
  8. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/src/conduct_cli/__init__.py +0 -0
  9. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/src/conduct_cli/api.py +0 -0
  10. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/src/conduct_cli/guardmcp.py +0 -0
  11. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/src/conduct_cli/main.py +0 -0
  12. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/src/conduct_cli/mcp_server.py +0 -0
  13. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/src/conduct_cli.egg-info/SOURCES.txt +0 -0
  14. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/src/conduct_cli.egg-info/dependency_links.txt +0 -0
  15. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/src/conduct_cli.egg-info/entry_points.txt +0 -0
  16. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/src/conduct_cli.egg-info/requires.txt +0 -0
  17. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/src/conduct_cli.egg-info/top_level.txt +0 -0
  18. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/tests/test_guard_policy.py +0 -0
  19. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/tests/test_guard_savings.py +0 -0
  20. {conduct_cli-0.4.58 → conduct_cli-0.4.60}/tests/test_switch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.58
3
+ Version: 0.4.60
4
4
  Summary: CLI for Conduct AI — install agents, manage projects, run tests
5
5
  Author-email: Conduct AI <hello@conductai.ai>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "conduct-cli"
7
- version = "0.4.58"
7
+ version = "0.4.60"
8
8
  description = "CLI for Conduct AI — install agents, manage projects, run tests"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -241,8 +241,75 @@ def _post_usage(session_id, tool_name, tokens_input, tokens_output, duration_ms)
241
241
  )
242
242
 
243
243
 
244
+ SECRET_PATTERNS = [
245
+ (r"sk-[A-Za-z0-9]{20,}", "secret-leak", "high", "Potential OpenAI/Anthropic API key"),
246
+ (r"ghp_[A-Za-z0-9]{36}", "secret-leak", "high", "GitHub Personal Access Token"),
247
+ (r"AKIA[0-9A-Z]{16}", "secret-leak", "critical", "AWS Access Key ID"),
248
+ (r"Bearer\s+[A-Za-z0-9+/=]{20,}", "secret-leak", "high", "Bearer token in output"),
249
+ (r"""password\s*=\s*['"][^'"]{4,}""","secret-leak", "high", "Hardcoded password"),
250
+ (r"""api[_-]?key\s*=\s*['"][^'"]{4,}""","secret-leak","high", "Hardcoded API key"),
251
+ (r"\.\./\.\./\.\./", "path-traversal", "medium", "Path traversal sequence"),
252
+ (r"file://", "path-traversal", "medium", "File URI scheme in output"),
253
+ (r"\beval\s*\(", "injection", "high", "eval() in output"),
254
+ (r"\bexec\s*\(", "injection", "high", "exec() in output"),
255
+ (r"\b__import__\s*\(", "injection", "high", "__import__() in output"),
256
+ (r"ssl\.CERT_NONE", "crypto", "high", "SSL verification disabled"),
257
+ (r"verify\s*=\s*False", "crypto", "medium", "TLS verification bypassed"),
258
+ ]
259
+ OWASP_KEYWORDS = [
260
+ ("sql injection", "injection", "high", "SQL injection mentioned in AI output"),
261
+ ("cross-site scripting","injection","high", "XSS mentioned in AI output"),
262
+ (" xss ", "injection", "high", "XSS mentioned in AI output"),
263
+ ("idor", "injection", "medium", "IDOR mentioned in AI output"),
264
+ ("ssrf", "injection", "high", "SSRF mentioned in AI output"),
265
+ ("command injection","injection", "high", "Command injection mentioned in AI output"),
266
+ ("auth bypass", "auth-bypass", "high", "Auth bypass mentioned in AI output"),
267
+ ]
268
+
269
+
270
+ def _classify_text(text):
271
+ """Return (finding_type, severity, description, matched_pattern) or (None,...) if clean."""
272
+ import re as _re
273
+ for pattern, ftype, sev, desc in SECRET_PATTERNS:
274
+ if _re.search(pattern, text, _re.IGNORECASE):
275
+ return ftype, sev, desc, pattern
276
+ lower = text.lower()
277
+ for kw, ftype, sev, desc in OWASP_KEYWORDS:
278
+ if kw in lower:
279
+ return ftype, sev, desc, kw
280
+ return None, None, None, None
281
+
282
+
283
+ def _line_number_from_text(text, matched_pattern):
284
+ """Extract line number where pattern matched.
285
+ Uses splitlines() and chr(10) — no backslash-n literals (safe inside _HOOK_SCRIPT).
286
+ """
287
+ import re as _re
288
+ if not matched_pattern:
289
+ return None
290
+ try:
291
+ # Try cat-n format first (Read tool outputs ' N<TAB>code line')
292
+ for raw_line in text.splitlines():
293
+ m = _re.match(r"^\s*(\d+)\t(.*)$", raw_line)
294
+ if m:
295
+ lineno, content = int(m.group(1)), m.group(2)
296
+ try:
297
+ if _re.search(matched_pattern, content, _re.IGNORECASE):
298
+ return lineno
299
+ except Exception:
300
+ if matched_pattern.lower() in content.lower():
301
+ return lineno
302
+ # Fallback: count lines before the first match offset
303
+ m = _re.search(matched_pattern, text, _re.IGNORECASE)
304
+ if m:
305
+ return text[:m.start()].count(chr(10)) + 1
306
+ except Exception:
307
+ pass
308
+ return None
309
+
310
+
244
311
  def _maybe_emit_security_finding(tool_response, session_id, tool_name, tool_input=None):
245
- """Classify tool_response for security findings; POST to /security-findings if flag ON. Never raises."""
312
+ """Classify tool output + input for security findings; POST if flag ON. Never raises."""
246
313
  try:
247
314
  cfg = json.loads(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
248
315
  except Exception:
@@ -255,55 +322,37 @@ def _maybe_emit_security_finding(tool_response, session_id, tool_name, tool_inpu
255
322
  if not workspace_id:
256
323
  return
257
324
 
258
- import re as _re2
259
- text = str(tool_response)
260
-
261
- # Fast-path classifier
262
- SECRET_PATTERNS = [
263
- (r"sk-[A-Za-z0-9]{20,}", "secret-leak", "high", "Potential OpenAI/Anthropic API key"),
264
- (r"ghp_[A-Za-z0-9]{36}", "secret-leak", "high", "GitHub Personal Access Token"),
265
- (r"AKIA[0-9A-Z]{16}", "secret-leak", "critical", "AWS Access Key ID"),
266
- (r"Bearer\s+[A-Za-z0-9+/=]{20,}", "secret-leak", "high", "Bearer token in output"),
267
- (r"""password\s*=\s*['"][^'"]{4,}""", "secret-leak", "high", "Hardcoded password"),
268
- (r"""api[_-]?key\s*=\s*['"][^'"]{4,}""", "secret-leak", "high", "Hardcoded API key"),
269
- (r"\.\./\.\./\.\./", "path-traversal", "medium", "Path traversal sequence"),
270
- (r"file://", "path-traversal", "medium", "File URI scheme in output"),
271
- (r"\beval\s*\(", "injection", "high", "eval() in output"),
272
- (r"\bexec\s*\(", "injection", "high", "exec() in output"),
273
- (r"\b__import__\s*\(", "injection", "high", "__import__() in output"),
274
- (r"ssl\.CERT_NONE", "crypto", "high", "SSL verification disabled"),
275
- (r"verify\s*=\s*False", "crypto", "medium", "TLS verification bypassed"),
276
- ]
277
- OWASP_KEYWORDS = [
278
- ("sql injection", "injection", "high", "SQL injection mentioned in AI output"),
279
- ("cross-site scripting", "injection", "high", "XSS mentioned in AI output"),
280
- (" xss ", "injection", "high", "XSS mentioned in AI output"),
281
- ("idor", "injection", "medium", "IDOR mentioned in AI output"),
282
- ("ssrf", "injection", "high", "SSRF mentioned in AI output"),
283
- ("command injection", "injection", "high", "Command injection mentioned in AI output"),
284
- ("auth bypass", "auth-bypass", "high", "Auth bypass mentioned in AI output"),
285
- ]
325
+ ti = tool_input or {}
286
326
 
287
- finding_type = severity = description = None
288
- for pattern, ftype, sev, desc in SECRET_PATTERNS:
289
- if _re2.search(pattern, text, _re2.IGNORECASE):
290
- finding_type, severity, description = ftype, sev, desc
327
+ # Build scan candidates: (text_to_scan, source_label)
328
+ # Priority: tool_response first (Read), then written content (Edit/Write), then command (Bash)
329
+ candidates = [("response", str(tool_response))]
330
+ if tool_name in ("edit", "multiedit"):
331
+ candidates.append(("input", ti.get("new_string", "")))
332
+ elif tool_name == "write":
333
+ candidates.append(("input", ti.get("content", "")))
334
+ elif tool_name in ("bash", "terminal"):
335
+ candidates.append(("input", ti.get("command", "")))
336
+
337
+ finding_type = severity = description = matched_pattern = scan_text = None
338
+ for _src, text in candidates:
339
+ ft, sv, desc, pat = _classify_text(text)
340
+ if ft:
341
+ finding_type, severity, description, matched_pattern, scan_text = ft, sv, desc, pat, text
291
342
  break
292
- if not finding_type:
293
- lower = text.lower()
294
- for kw, ftype, sev, desc in OWASP_KEYWORDS:
295
- if kw in lower:
296
- finding_type, severity, description = ftype, sev, desc
297
- break
343
+
298
344
  if not finding_type:
299
345
  return
300
346
 
301
- ti = tool_input or {}
347
+ # File path
302
348
  file_path = (
303
349
  ti.get("file_path") or ti.get("path") or
304
350
  (ti.get("command", "")[:120] if tool_name in ("bash", "terminal") else None)
305
351
  ) or None
306
352
 
353
+ # Line number
354
+ line_no = _line_number_from_text(scan_text, matched_pattern) if scan_text else None
355
+
307
356
  payload = json.dumps({
308
357
  "tool": _detect_ai_tool(),
309
358
  "severity": severity,
@@ -312,6 +361,7 @@ def _maybe_emit_security_finding(tool_response, session_id, tool_name, tool_inpu
312
361
  "source_run_id": session_id,
313
362
  "reporter_email": cfg.get("user_email") or "",
314
363
  "file": file_path,
364
+ "line": line_no,
315
365
  })
316
366
  script = (
317
367
  "import urllib.request\\n"
@@ -760,17 +810,25 @@ def _best_python() -> str:
760
810
 
761
811
  def _write_hook(path: Path) -> None:
762
812
  """Write _HOOK_SCRIPT to path, then py_compile-validate it.
763
- Raises RuntimeError if the written file fails to compile prevents
764
- silently deploying a syntactically broken hook."""
813
+ On syntax failure: restores previous hook (or writes a safe stub) so the
814
+ system is never left without a working hook file."""
765
815
  import py_compile, tempfile, os
816
+ # Stash existing hook so we can restore on failure
817
+ backup = None
818
+ if path.exists():
819
+ backup = path.read_text()
820
+ path.parent.mkdir(parents=True, exist_ok=True)
766
821
  path.write_text(_HOOK_SCRIPT)
767
822
  path.chmod(0o755)
768
823
  try:
769
824
  py_compile.compile(str(path), doraise=True)
770
825
  except py_compile.PyCompileError as exc:
771
- path.unlink(missing_ok=True)
826
+ if backup is not None:
827
+ path.write_text(backup)
828
+ else:
829
+ path.write_text("#!/usr/bin/env python3\nimport sys\nsys.exit(0)\n")
772
830
  raise RuntimeError(
773
- f"hook.py failed syntax check after write hook NOT installed.\n{exc}"
831
+ f"hook.py failed syntax check previous hook restored.\n{exc}"
774
832
  ) from exc
775
833
 
776
834
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: conduct-cli
3
- Version: 0.4.58
3
+ Version: 0.4.60
4
4
  Summary: CLI for Conduct AI — install agents, manage projects, run tests
5
5
  Author-email: Conduct AI <hello@conductai.ai>
6
6
  License: MIT
File without changes
File without changes
File without changes