eva-exploit 3.3.7__tar.gz → 3.4__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 (26) hide show
  1. {eva_exploit-3.3.7 → eva_exploit-3.4}/PKG-INFO +2 -1
  2. {eva_exploit-3.3.7 → eva_exploit-3.4}/README.md +1 -0
  3. {eva_exploit-3.3.7 → eva_exploit-3.4}/config.py +1 -1
  4. {eva_exploit-3.3.7 → eva_exploit-3.4}/eva_exploit.egg-info/PKG-INFO +2 -1
  5. {eva_exploit-3.3.7 → eva_exploit-3.4}/modules/llm.py +147 -10
  6. {eva_exploit-3.3.7 → eva_exploit-3.4}/modules/prompt_builder.py +1 -0
  7. {eva_exploit-3.3.7 → eva_exploit-3.4}/pyproject.toml +1 -1
  8. {eva_exploit-3.3.7 → eva_exploit-3.4}/sessions/eva_session.py +59 -0
  9. {eva_exploit-3.3.7 → eva_exploit-3.4}/utils/system.py +43 -22
  10. {eva_exploit-3.3.7 → eva_exploit-3.4}/eva.py +0 -0
  11. {eva_exploit-3.3.7 → eva_exploit-3.4}/eva_exploit.egg-info/SOURCES.txt +0 -0
  12. {eva_exploit-3.3.7 → eva_exploit-3.4}/eva_exploit.egg-info/dependency_links.txt +0 -0
  13. {eva_exploit-3.3.7 → eva_exploit-3.4}/eva_exploit.egg-info/entry_points.txt +0 -0
  14. {eva_exploit-3.3.7 → eva_exploit-3.4}/eva_exploit.egg-info/requires.txt +0 -0
  15. {eva_exploit-3.3.7 → eva_exploit-3.4}/eva_exploit.egg-info/top_level.txt +0 -0
  16. {eva_exploit-3.3.7 → eva_exploit-3.4}/modules/__init__.py +0 -0
  17. {eva_exploit-3.3.7 → eva_exploit-3.4}/modules/attack_map.py +0 -0
  18. {eva_exploit-3.3.7 → eva_exploit-3.4}/modules/exploit_search.py +0 -0
  19. {eva_exploit-3.3.7 → eva_exploit-3.4}/modules/reporting.py +0 -0
  20. {eva_exploit-3.3.7 → eva_exploit-3.4}/modules/tooling.py +0 -0
  21. {eva_exploit-3.3.7 → eva_exploit-3.4}/modules/vuln_intel.py +0 -0
  22. {eva_exploit-3.3.7 → eva_exploit-3.4}/modules/workflow.py +0 -0
  23. {eva_exploit-3.3.7 → eva_exploit-3.4}/sessions/__init__.py +0 -0
  24. {eva_exploit-3.3.7 → eva_exploit-3.4}/setup.cfg +0 -0
  25. {eva_exploit-3.3.7 → eva_exploit-3.4}/utils/__init__.py +0 -0
  26. {eva_exploit-3.3.7 → eva_exploit-3.4}/utils/ui.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eva-exploit
3
- Version: 3.3.7
3
+ Version: 3.4
4
4
  Summary: Exploit Vector Agent
5
5
  Author: ARCANGEL0
6
6
  License: MIT
@@ -275,6 +275,7 @@ eva
275
275
  | `/exit` / `/quit` | Exit EVA and save session |
276
276
  | `/model` | Change AI backend |
277
277
  | `/rename` | Rename the current session |
278
+ | `/search <query>` or `search <query>` | Run exploit/vulnerability intel search inside current chat session and feed results into next analysis |
278
279
  | `/report` | Generates a PDF/HTML report with latest findings on session |
279
280
  | `/map` | Generates a html file with attack surface map of session |
280
281
  | `/menu` | Return to session menu |
@@ -263,6 +263,7 @@ eva
263
263
  | `/exit` / `/quit` | Exit EVA and save session |
264
264
  | `/model` | Change AI backend |
265
265
  | `/rename` | Rename the current session |
266
+ | `/search <query>` or `search <query>` | Run exploit/vulnerability intel search inside current chat session and feed results into next analysis |
266
267
  | `/report` | Generates a PDF/HTML report with latest findings on session |
267
268
  | `/map` | Generates a html file with attack surface map of session |
268
269
  | `/menu` | Return to session menu |
@@ -10,7 +10,7 @@ from pathlib import Path
10
10
 
11
11
  # ================= CONFIG =================
12
12
  APP_NAME = "EVA"
13
- APP_VERSION = "3.3.7"
13
+ APP_VERSION = "3.4"
14
14
  GITHUB_REPO = "arcangel0/EVA"
15
15
  PYPI_PACKAGE = "eva-exploit"
16
16
  API_ENDPOINT = "NOT_SET"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: eva-exploit
3
- Version: 3.3.7
3
+ Version: 3.4
4
4
  Summary: Exploit Vector Agent
5
5
  Author: ARCANGEL0
6
6
  License: MIT
@@ -275,6 +275,7 @@ eva
275
275
  | `/exit` / `/quit` | Exit EVA and save session |
276
276
  | `/model` | Change AI backend |
277
277
  | `/rename` | Rename the current session |
278
+ | `/search <query>` or `search <query>` | Run exploit/vulnerability intel search inside current chat session and feed results into next analysis |
278
279
  | `/report` | Generates a PDF/HTML report with latest findings on session |
279
280
  | `/map` | Generates a html file with attack surface map of session |
280
281
  | `/menu` | Return to session menu |
@@ -49,6 +49,16 @@ STREAM_HIDE_MARKERS = [
49
49
  "explanation of flags:",
50
50
  "[authorized ctf/lab task]",
51
51
  ]
52
+ COMMAND_START_TOKENS = [
53
+ "nmap", "masscan", "rustscan", "nikto", "whatweb", "curl", "wget", "ffuf",
54
+ "gobuster", "dirsearch", "feroxbuster", "sqlmap", "hydra", "nc", "netcat",
55
+ "ssh", "ftp", "smbclient", "enum4linux", "rpcclient", "crackmapexec", "nuclei",
56
+ "msfconsole", "searchsploit", "command", "test", "find", "ls", "cat", "grep",
57
+ "awk", "sed", "cut", "sort", "uniq", "tr", "head", "tail", "id", "whoami",
58
+ "uname", "ip", "ifconfig", "ss", "netstat", "nslookup", "dig", "host", "ping",
59
+ "traceroute", "openssl", "sslscan", "ldapsearch", "snmpwalk", "python", "python3",
60
+ "bash", "sh",
61
+ ]
52
62
  ## regex for the lame llm model excuses, to guarantee adherence to pentesting scope
53
63
  REFUSAL_PATTERNS = [
54
64
  r"\bi can(?:not|'t)\s+(?:assist|help|fulfill)\b",
@@ -87,6 +97,59 @@ def _build_no_output_analysis(last_output):
87
97
  )
88
98
 
89
99
 
100
+ def _dedupe_keep_order(items):
101
+ out = []
102
+ seen = set()
103
+ for item in items:
104
+ key = str(item).strip()
105
+ if not key or key in seen:
106
+ continue
107
+ seen.add(key)
108
+ out.append(key)
109
+ return out
110
+
111
+
112
+ def _coerce_commands(value):
113
+ collected = []
114
+ token_group = "|".join(sorted((re.escape(t) for t in COMMAND_START_TOKENS), key=len, reverse=True))
115
+ loose_line_re = re.compile(
116
+ rf"^(?:sudo\s+)?(?:{token_group})\b",
117
+ flags=re.IGNORECASE,
118
+ )
119
+
120
+ def _visit(node):
121
+ if node is None:
122
+ return
123
+ if isinstance(node, dict):
124
+ for key in ("cmd", "command", "value", "text", "content"):
125
+ if key in node:
126
+ _visit(node.get(key))
127
+ return
128
+ if isinstance(node, (list, tuple, set)):
129
+ for item in node:
130
+ _visit(item)
131
+ return
132
+
133
+ text = str(node).strip()
134
+ if not text:
135
+ return
136
+
137
+ extracted = extract_commands_anywhere(text)
138
+ if extracted:
139
+ collected.extend(extracted)
140
+ return
141
+
142
+ for raw_line in text.splitlines():
143
+ line = re.sub(r"^\s*(?:[-*]|\d+[.)])\s*", "", raw_line).strip("` ").strip()
144
+ if not line:
145
+ continue
146
+ if loose_line_re.match(line):
147
+ collected.append(line)
148
+
149
+ _visit(value)
150
+ return _dedupe_keep_order(collected)
151
+
152
+
90
153
  ## extractjson
91
154
  def _extract_code_fence_json(raw_str):
92
155
  pattern = r"```(?:json)?\\s*(.*?)\\s*```"
@@ -167,15 +230,24 @@ def normalize_response(resp):
167
230
  if not isinstance(resp, dict):
168
231
  return {"analysis": "[::!] ⚠️ Error on LLM output.", "commands": []}
169
232
 
170
- analysis = resp.get("analysis", "[::!] ⚠️ Error with model response, please ask again.")
171
- commands = resp.get("commands", [])
172
-
173
- if isinstance(commands, str):
174
- commands = [commands]
175
- if not isinstance(commands, list):
176
- commands = []
177
-
178
- commands = [str(c).strip() for c in commands if str(c).strip()]
233
+ analysis = resp.get("analysis")
234
+ if analysis is None:
235
+ for key in ("response", "answer", "text", "content", "message"):
236
+ if key in resp and str(resp.get(key, "")).strip():
237
+ analysis = resp.get(key)
238
+ break
239
+ if analysis is None:
240
+ analysis = "[::!] ⚠️ Error with model response, please ask again."
241
+
242
+ commands = resp.get("commands")
243
+ if commands in (None, "", []):
244
+ for key in ("next_commands", "cmds", "command", "suggested_commands"):
245
+ if key in resp:
246
+ commands = resp.get(key)
247
+ break
248
+ commands = _coerce_commands(commands)
249
+ if not commands:
250
+ commands = extract_commands_anywhere(str(analysis))
179
251
 
180
252
  return {
181
253
  "analysis": str(analysis),
@@ -200,8 +272,9 @@ def extract_commands_anywhere(raw_str):
200
272
  seen.add(candidate)
201
273
  commands.append(candidate)
202
274
 
275
+ token_group = "|".join(sorted((re.escape(t) for t in COMMAND_START_TOKENS), key=len, reverse=True))
203
276
  command_line_re = re.compile(
204
- r"^\s*(?:[-*]\s*)?(?:`)?((?:nmap|masscan|nikto|whatweb|curl|wget|ffuf|gobuster|dirsearch|sqlmap|hydra|nc|netcat|ssh|ftp|smbclient|enum4linux|rpcclient|crackmapexec|nuclei|msfconsole)\b[^\n`]*)`?\s*$",
277
+ rf"^\s*(?:[-*]\s*)?(?:\d+[.)]\s*)?(?:`)?(?:\$+\s*)?((?:sudo\s+)?(?:{token_group})\b[^\n`]*)`?\s*$",
205
278
  flags=re.IGNORECASE | re.MULTILINE,
206
279
  )
207
280
  for match in command_line_re.findall(raw_str):
@@ -210,6 +283,28 @@ def extract_commands_anywhere(raw_str):
210
283
  seen.add(candidate)
211
284
  commands.append(candidate)
212
285
 
286
+ inline_re = re.compile(
287
+ rf"`((?:sudo\s+)?(?:{token_group})\b[^`]+)`",
288
+ flags=re.IGNORECASE,
289
+ )
290
+ for match in inline_re.findall(raw_str):
291
+ candidate = match.strip()
292
+ if candidate and candidate not in seen:
293
+ seen.add(candidate)
294
+ commands.append(candidate)
295
+
296
+ phrase_re = re.compile(
297
+ rf"\b(?:use|run|execute)\s+((?:sudo\s+)?(?:{token_group})\b[^\n`]+)",
298
+ flags=re.IGNORECASE,
299
+ )
300
+ for line in raw_str.splitlines():
301
+ text_line = line.strip()
302
+ for match in phrase_re.findall(text_line):
303
+ candidate = match.strip().rstrip(" .;,")
304
+ if candidate and candidate not in seen:
305
+ seen.add(candidate)
306
+ commands.append(candidate)
307
+
213
308
  return commands
214
309
 
215
310
 
@@ -387,6 +482,41 @@ def _ollama_run_fallback(prompt):
387
482
  return ""
388
483
 
389
484
 
485
+ def _recover_commands_for_ollama(user_msg, last_output, workflow_context, analysis):
486
+ recovery_system = (
487
+ "You are EVA command planner for authorized CTF/lab activity.\n"
488
+ "Return strict JSON only: {\"commands\": [\"cmd1\", \"cmd2\", \"cmd3\"]}\n"
489
+ "Rules:\n"
490
+ "- Return 1-3 executable shell commands.\n"
491
+ "- No placeholders and no markdown.\n"
492
+ "- Use only targets/ports visible in provided evidence.\n"
493
+ "- If evidence is insufficient, return prerequisite evidence-gathering commands."
494
+ )
495
+ recovery_user = (
496
+ f"USER_MSG: {user_msg}\n\n"
497
+ f"ANALYSIS_TEXT:\n{analysis}\n\n"
498
+ f"CONTEXT_DATA:\n{_context_for_system_prompt(last_output)}\n\n"
499
+ f"WORKFLOW_STATE:\n{workflow_context or 'none'}"
500
+ )
501
+ raw, _ = _ollama_chat(
502
+ [
503
+ {"role": "system", "content": recovery_system},
504
+ {"role": "user", "content": recovery_user},
505
+ ]
506
+ )
507
+ if not raw:
508
+ return []
509
+
510
+ parsed = extract_json_anywhere(raw) or {}
511
+ recovered = _coerce_commands(parsed.get("commands"))
512
+ if recovered:
513
+ return recovered[:3]
514
+ recovered = extract_commands_anywhere(raw)
515
+ if recovered:
516
+ return recovered[:3]
517
+ return []
518
+
519
+
390
520
  def _query_g4f(history):
391
521
  raw = ""
392
522
  headers = {"Content-Type": "application/json"}
@@ -688,6 +818,13 @@ class LLM:
688
818
  data["commands"] = inferred_commands
689
819
 
690
820
  data = normalize_response(data)
821
+ if self.backend == "ollama" and not data.get("commands"):
822
+ data["commands"] = _recover_commands_for_ollama(
823
+ user_msg,
824
+ last_output,
825
+ workflow_context,
826
+ str(data.get("analysis", "")),
827
+ )
691
828
  data["analysis"] = _clean_analysis_text(data.get("analysis", ""))
692
829
  data["__streamed"] = streamed
693
830
 
@@ -51,6 +51,7 @@ COMMAND RULES:
51
51
  - Evidence lock: never claim a command result happened unless it appears in CONTEXT_DATA/WORKFLOW_STATE.
52
52
  - If latest output indicates missing evidence/no output, say that explicitly and avoid speculative findings.
53
53
  - For any command requiring local files (wordlists/config/scripts), emit a file-existence check command first.
54
+ - `commands` MUST NOT be empty. If evidence is weak, still output safe prerequisite commands to collect missing evidence.
54
55
 
55
56
  CONTEXT_DATA:
56
57
  {last_output if last_output else "SYSTEM_BOOT: AWAITING_TARGET_PARAMETER"}
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "eva-exploit"
7
- version = "3.3.7"
7
+ version = "3.4"
8
8
  description = "Exploit Vector Agent"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -6,16 +6,20 @@
6
6
  # ---------------------------------------------------------------------
7
7
 
8
8
  import json
9
+ import io
9
10
  import getpass
10
11
  import os
12
+ import re
11
13
  import signal
12
14
  import subprocess
15
+ import contextlib
13
16
  from datetime import datetime, timezone
14
17
 
15
18
  from colorama import Fore,Back,Style
16
19
 
17
20
  from config import API_ENDPOINT, MAPS_DIR, REPORTS_DIR, SESSIONS_DIR, username
18
21
  from modules.attack_map import generate_attack_map_files, open_attack_map
22
+ from modules.exploit_search import run_exploit_search
19
23
  from modules.llm import LLM
20
24
  from modules.reporting import build_html_report, open_report_file, try_generate_pdf
21
25
  from modules.tooling import (
@@ -40,6 +44,8 @@ from utils.system import (
40
44
  )
41
45
  from utils.ui import cyber, menu, raw_input, spinner_start, spinner_stop
42
46
 
47
+ ANSI_ESCAPE_RE = re.compile(r"\x1B\[[0-?]*[ -/]*[@-~]")
48
+
43
49
 
44
50
  # +-------------------------------------------+
45
51
  # | Main core -██ |
@@ -345,6 +351,7 @@ class Eva:
345
351
  print(Fore.GREEN + "⯁⮞ ˹E˼xploit ˹V˼ector ˹A˼gent \n⬢ Current Model: " + Fore.CYAN + self.backend + f"\n{Fore.GREEN}𖨠 Session Name: " + Fore.YELLOW + self.sessionName)
346
352
  print(Fore.CYAN + "/// type /exit to quit the program anytime")
347
353
  print(Fore.CYAN + "/// type /model to change current model")
354
+ print(Fore.CYAN + "/// type /search <query> to run exploit/vuln intel search in-session")
348
355
  print(Fore.CYAN + "/// type /rename to change a session name")
349
356
  print(Fore.CYAN + "/// type /viewmap to open attack map with last findings.")
350
357
  print(Fore.CYAN + "/// type /report to generate a report in PDF and HTML.")
@@ -387,6 +394,7 @@ class Eva:
387
394
  if user.lower() in ("help", "/help"):
388
395
  print(Fore.CYAN + "/// type /exit to quit the program anytime")
389
396
  print(Fore.CYAN + "/// type /model to change current model")
397
+ print(Fore.CYAN + "/// type /search <query> to run exploit/vuln intel search in-session")
390
398
  print(Fore.CYAN + "/// type /rename to change a session name")
391
399
  print(Fore.CYAN + "/// type /viewmap to open attack map with last findings.")
392
400
  print(Fore.CYAN + "/// type /report to generate a report in PDF and HTML.")
@@ -405,6 +413,57 @@ class Eva:
405
413
  if user.lower() in ("report", "/report"):
406
414
  self.generate_report()
407
415
  continue
416
+ low_user = user.strip().lower()
417
+ if (
418
+ low_user == "search"
419
+ or low_user.startswith("search ")
420
+ or low_user == "/search"
421
+ or low_user.startswith("/search ")
422
+ ):
423
+ query = user.strip().split(maxsplit=1)[1].strip() if len(user.strip().split(maxsplit=1)) > 1 else ""
424
+ if not query:
425
+ query = raw_input("EVA search query > ").strip()
426
+ if not query:
427
+ cyber("Search query cannot be empty.", color=Fore.YELLOW)
428
+ continue
429
+
430
+ normalized_user = f"/search {query}"
431
+ self.memory["timeline"].append({
432
+ "type": "user",
433
+ "content": normalized_user
434
+ })
435
+ self.model.history.append({"role": "user", "content": normalized_user})
436
+
437
+ buf = io.StringIO()
438
+ rc = 1
439
+ try:
440
+ with contextlib.redirect_stdout(buf):
441
+ rc = run_exploit_search(query)
442
+ except KeyboardInterrupt:
443
+ if self.memory["timeline"] and self.memory["timeline"][-1].get("type") == "user":
444
+ self.memory["timeline"].pop()
445
+ if self.model.history and self.model.history[-1].get("role") == "user":
446
+ self.model.history.pop()
447
+ print(Fore.YELLOW + "\n// 🜂 Search cancelled")
448
+ continue
449
+ except Exception as exc:
450
+ print(Fore.RED + f"[!] Search failed: {exc}")
451
+ raw_search_output = buf.getvalue()
452
+ if raw_search_output:
453
+ print(raw_search_output, end="" if raw_search_output.endswith("\n") else "\n")
454
+
455
+ clean_output = ANSI_ESCAPE_RE.sub("", raw_search_output or "").strip()
456
+ if not clean_output:
457
+ clean_output = f"[EVA_NOTICE] /search returned no output. exit_code={rc}"
458
+
459
+ self.last_output = clean_output
460
+ self.memory["timeline"].append({
461
+ "type": "analysis",
462
+ "content": clean_output
463
+ })
464
+ self.model.history.append({"role": "assistant", "content": clean_output})
465
+ self.save()
466
+ continue
408
467
 
409
468
  spinner_start()
410
469
  spinner_stopped = False
@@ -370,35 +370,56 @@ def checkupdts():
370
370
 
371
371
  def run_self_update():
372
372
  print(Fore.CYAN + f"\nChecking updates for {APP_NAME}...\n")
373
- updated = False
373
+ status = get_update_status(force=True)
374
+ if not status.get("available"):
375
+ cyber("EVA is already up to date.", color=Fore.CYAN)
376
+ return 0
374
377
 
375
- pip_result = subprocess.run(
376
- [sys.executable, "-m", "pip", "install", "--upgrade", PYPI_PACKAGE,"--break-system-packages"],
377
- text=True
378
- )
379
- if pip_result.returncode == 0:
380
- updated = True
378
+ latest = status.get("latest") or "latest"
379
+ source = status.get("source")
380
+ print(Style.BRIGHT + Fore.MAGENTA + f"Update {latest} found! Installing . . . . " + Style.RESET_ALL)
381
381
 
382
- if Path(".git").exists() and command_exists("git"):
383
- branch = "main"
384
- branch_detect = subprocess.run(
385
- ["git", "rev-parse", "--abbrev-ref", "HEAD"],
386
- capture_output=True,
387
- text=True
388
- )
389
- if branch_detect.returncode == 0 and branch_detect.stdout.strip():
390
- branch = branch_detect.stdout.strip()
391
-
392
- pull_result = subprocess.run(["git", "pull", "--tags", "origin", branch], text=True)
393
- if pull_result.returncode == 0:
394
- updated = True
382
+ updated = False
383
+ with open(os.devnull, "w") as devnull:
384
+ if source == "github" and Path(".git").exists() and command_exists("git"):
385
+ branch = _git_branch()
386
+ pull_result = subprocess.run(
387
+ ["git", "pull", "--tags", "origin", branch],
388
+ stdout=devnull,
389
+ stderr=devnull,
390
+ text=True
391
+ )
392
+ updated = pull_result.returncode == 0
393
+ else:
394
+ pip_result = subprocess.run(
395
+ [
396
+ sys.executable,
397
+ "-m",
398
+ "pip",
399
+ "install",
400
+ "--upgrade",
401
+ PYPI_PACKAGE,
402
+ "--break-system-packages",
403
+ "-q",
404
+ ],
405
+ stdout=devnull,
406
+ stderr=devnull,
407
+ text=True
408
+ )
409
+ updated = pip_result.returncode == 0
395
410
 
411
+ print(Fore.CYAN + "Almost done . . . . ")
396
412
  if updated:
397
- print(Fore.GREEN + "\n✔ Update process finished. Restart EVA to use the latest version.")
413
+ global _UPDATE_STATUS_CACHE
414
+ _UPDATE_STATUS_CACHE = None
415
+ cyber("✔ Update process finished. Restart EVA to use the latest version.", color=Fore.GREEN)
398
416
  return 0
399
417
 
400
418
  print(Fore.RED + "\n[!] Could not auto-update EVA in this environment.")
401
- print(Fore.YELLOW + f"Try manually: {sys.executable} -m pip install --upgrade {PYPI_PACKAGE}")
419
+ if source == "github":
420
+ print(Fore.YELLOW + f"Try manually: git pull --tags origin {_git_branch()}")
421
+ else:
422
+ print(Fore.YELLOW + f"Try manually: {sys.executable} -m pip install --upgrade {PYPI_PACKAGE}")
402
423
  return 1
403
424
 
404
425
 
File without changes
File without changes
File without changes
File without changes