ptopenvas 0.0.1__py3-none-any.whl

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.
ptopenvas/__init__.py ADDED
File without changes
ptopenvas/_version.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
File without changes
@@ -0,0 +1,237 @@
1
+ import os
2
+ import secrets
3
+ import string
4
+ import subprocess
5
+ import sys
6
+ import shutil
7
+ import grp
8
+
9
+ from gvm.transforms import EtreeCheckCommandTransform
10
+ from gvm.connections import UnixSocketConnection
11
+ from gvm.protocols.gmp import Gmp
12
+ from gvm.errors import GvmError
13
+
14
+ from ptlibs.app_dirs import AppDirs
15
+
16
+ class GVMSetup:
17
+
18
+ PASSWORD_FILE = os.path.join(AppDirs("ptopenvas").get_base_dir(), "gvm.pass")
19
+
20
+ def __init__(self, args={}) -> None:
21
+ self.args = args
22
+
23
+ def run(self) -> bool:
24
+ """
25
+ Main function to ensure dependencies and GVM setup.
26
+ Returns True if everything is ready.
27
+ """
28
+ # 1. Install GVM and ensure initialization
29
+ self.install_gvm() # ensure gvm is installed
30
+ self.ensure_gvmd_initialized() # run gvm-setup
31
+
32
+ # 2. Ensure GVMD service is running
33
+ self.run_gvmd_daemon()
34
+
35
+ # 3. Finalize permissions
36
+ self.finalize_permissions()
37
+
38
+ # 4. Ensure penterep user exists (creates with random password if needed)
39
+ self.ensure_penterep_user()
40
+
41
+ return True
42
+
43
+
44
+ def install_gvm(self) -> bool:
45
+ """
46
+ Installs missing GVM dependencies and runs gvm-setup.
47
+ Outputs all subprocess logs directly to the terminal.
48
+ """
49
+ required_packages = ["gvmd", "gvm-cli"] # Minimal required packages
50
+
51
+ missing = [pkg for pkg in required_packages if shutil.which(pkg) is None]
52
+
53
+ if missing:
54
+ print(f"Installing missing packages: {', '.join(missing)}")
55
+ try:
56
+ subprocess.run(["sudo", "apt", "update"], check=True)
57
+ subprocess.run(["sudo", "apt", "install", "-y"] + missing, check=True)
58
+ print("Installation complete.")
59
+ except subprocess.CalledProcessError as e:
60
+ print(f"Failed to install packages: {e}")
61
+ return False
62
+ else:
63
+ pass#print("All core GVM packages are already installed.")
64
+
65
+
66
+ def ensure_gvmd_initialized(self) -> bool:
67
+ """
68
+ Ensure GVMD is initialized by running `gvm-setup` if needed.
69
+
70
+ This checks for the GVMD Unix socket at `/run/gvmd/gvmd.sock` and
71
+ runs `gvm-setup` only when initialization appears required.
72
+ """
73
+ # 1) Check if GVMD database exists
74
+ try:
75
+ db_check = subprocess.run(
76
+ ["sudo", "-u", "postgres", "psql", "-tAc", "SELECT 1 FROM pg_database WHERE datname='gvmd'"],
77
+ #["-u", "postgres", "psql", "-tAc", "SELECT 1 FROM pg_database WHERE datname='gvmd'"],
78
+ capture_output=True, text=True, check=True
79
+ )
80
+ if db_check.stdout.strip() == "1":
81
+ #print("GVM setup was already completed previously.")
82
+ return True
83
+ except Exception:
84
+ pass
85
+
86
+ # If database missing -> setup never ran
87
+ try:
88
+ print("Running gvm-setup...")
89
+ subprocess.run(["sudo", "gvm-setup"], check=True)
90
+ print("GVMD initialization completed successfully.")
91
+ return True
92
+ except subprocess.CalledProcessError as e:
93
+ print(f"GVMD initialization failed: {e}")
94
+ return False
95
+
96
+ def run_gvmd_daemon(self):
97
+ """
98
+ Ensure that the GVMD service is running.
99
+ Starts it if it is not active.
100
+ """
101
+ try:
102
+ #status = subprocess.run(["systemctl", "is-active", "--quiet", "gvmd"])
103
+ status = subprocess.run(["systemctl", "is-active", "--quiet", "gvmd"])
104
+ if status.returncode != 0:
105
+ print("GVMD service is not running. Starting with gvm-start...")
106
+ subprocess.run(["sudo", "gvm-start"], check=True)
107
+ print("GVM stack started.")
108
+ else:
109
+ pass
110
+ #print("GVMD service is already running.")
111
+ except subprocess.CalledProcessError as e:
112
+ print(f"Failed to start GVMD service: {e}")
113
+
114
+ def ensure_penterep_user(self):
115
+ """
116
+ Ensure gvmd service is running and the 'penterep' admin user exists.
117
+ If the user does not exist, create it with a random password stored in PASSWORD_FILE.
118
+ If the provided admin password is wrong, it will be reset automatically.
119
+ """
120
+
121
+ # 1. Define admin username and get password
122
+ USERNAME = "penterep"
123
+ PASSWORD = self.get_password() # "admin"
124
+
125
+ self._create_user_if_needed(username=USERNAME, password=PASSWORD)
126
+
127
+ # 2. Determine password for penterep
128
+ if not PASSWORD:
129
+ alphabet = string.ascii_letters + string.digits
130
+ PASSWORD = ''.join(secrets.choice(alphabet) for _ in range(24))
131
+ with open(self.PASSWORD_FILE, "w") as f:
132
+ f.write(PASSWORD)
133
+
134
+ # 3. Connect to gvmd and create user if missing
135
+ conn = UnixSocketConnection(path="/run/gvmd/gvmd.sock")
136
+ try:
137
+ with Gmp(conn, transform=EtreeCheckCommandTransform()) as gmp:
138
+ try:
139
+ gmp.authenticate(USERNAME, PASSWORD)
140
+ except GvmError as e:
141
+ try:
142
+ subprocess.run([
143
+ "sudo", "-u", "_gvm", "gvmd",
144
+ "--user=penterep",
145
+ f"--new-password={PASSWORD}"
146
+ ], check=True, capture_output=True)
147
+ #print(f"Reset penterep password to '{PASSWORD}'")
148
+ except Exception as e:
149
+ print(f"Failed to reset penterep password: {e}")
150
+ return
151
+
152
+ except PermissionError:
153
+ print("Please reset your shell (log out and log back in) before running scripts that access GVMD.")
154
+ except GvmError as e:
155
+ print(f"Failed to create user: {e}")
156
+
157
+ def _create_user_if_needed(self, username, password):
158
+ r = subprocess.run([
159
+ "sudo", "-u", "_gvm", "gvmd",
160
+ "--get-users"
161
+ ], check=True, capture_output=True)
162
+
163
+ available_users = [u for u in r.stdout.decode().split("\n") if u]
164
+ #print("Available_users:\n ", '\n '.join(available_users))
165
+
166
+ if username not in available_users:
167
+ try:
168
+ # Create admin user
169
+ subprocess.run([
170
+ "sudo", "-u", "_gvm", "gvmd",
171
+ "--create-user="+username,
172
+ "--password="+password,
173
+ ], check=True, capture_output=True)
174
+ print(f"Created admin user '{username}' with password '{password}'")
175
+ except Exception as e:
176
+ print(f"Failed to create user '{username}': {e}")
177
+ return
178
+
179
+
180
+ def get_password(self, generate: bool = False) -> str:
181
+ """
182
+ Return the stored password from PASSWORD_FILE.
183
+ If the file is missing or empty, or generate=True, create a new password, save it, and return it.
184
+ """
185
+ def generate_password() -> str:
186
+ charset = string.ascii_letters + string.digits
187
+ new_pass = ''.join(secrets.choice(charset) for _ in range(32))
188
+ with open(self.PASSWORD_FILE, "w") as f:
189
+ f.write(new_pass)
190
+ return new_pass
191
+
192
+ # Generate a new password if requested or if the file is missing/empty
193
+ if generate or not os.path.exists(self.PASSWORD_FILE):
194
+ return generate_password()
195
+
196
+ # Read existing password
197
+ with open(self.PASSWORD_FILE, "r") as f:
198
+ existing_pass = f.read().strip()
199
+
200
+ # If file is empty, generate a new password
201
+ if not existing_pass:
202
+ return generate_password()
203
+
204
+ return existing_pass
205
+
206
+
207
+ def finalize_permissions(self):
208
+ """
209
+ Add the current user to the _gvm group so the Python script can access the GVMD socket.
210
+ The user must reset their shell for the group change to take effect.
211
+ """
212
+ user = os.environ.get("USER")
213
+ if not user:
214
+ print("Could not determine the current user.")
215
+ return
216
+
217
+ # Check membership
218
+ try:
219
+ group = grp.getgrnam("_gvm")
220
+ except KeyError:
221
+ print("Group '_gvm' does not exist.")
222
+ return
223
+
224
+ if user in group.gr_mem:
225
+ #print(f"User '{user}' is already a member of the _gvm group. No action taken.")
226
+ return
227
+
228
+ try:
229
+ subprocess.run(["sudo", "usermod", "-aG", "_gvm", user], check=True)
230
+ print(f"User '{user}' has been added to the _gvm group.")
231
+ except subprocess.CalledProcessError as e:
232
+ print(f"Failed to add user to _gvm group: {e}")
233
+
234
+
235
+ if __name__ == "__main__":
236
+ gvm = GVMSetup(args={})
237
+ gvm.run()
@@ -0,0 +1,220 @@
1
+ from lxml import etree
2
+ import uuid
3
+ from typing import List, Tuple, Union
4
+ import time
5
+
6
+ def _ensure_tree(maybe_xml: Union[str, etree._Element]) -> etree._Element:
7
+ """
8
+ Convert XML string to ElementTree if necessary.
9
+ """
10
+ if isinstance(maybe_xml, str):
11
+ return etree.fromstring(maybe_xml.encode())
12
+ return maybe_xml
13
+
14
+ def _list_port_lists(gmp) -> List[Tuple[str, str, bool]]:
15
+ """
16
+ Return all port lists as a list of tuples: (id, name, predefined_flag)
17
+ """
18
+ root = _ensure_tree(gmp.get_port_lists())
19
+ items = []
20
+ for pl in root.xpath("//port_list"):
21
+ pid = pl.get("id") or pl.findtext("id")
22
+ name = pl.findtext("name") or ""
23
+ predefined_flag = pl.findtext("predefined") == "1"
24
+ items.append((pid, name, predefined_flag))
25
+ return items
26
+
27
+ def _get_default_portlist_id(gmp, proto: str) -> str:
28
+ """
29
+ Return the ID of the default All TCP or All UDP port list.
30
+ Raises RuntimeError if not found.
31
+ """
32
+ proto = proto.lower()
33
+ defaults = {"tcp": "All IANA assigned TCP", "udp": "All UDP"}
34
+ root = _ensure_tree(gmp.get_port_lists())
35
+ node = root.xpath(f"//port_list[name='{defaults.get(proto, '')}']")
36
+ if not node:
37
+ raise RuntimeError(f"Default port list for {proto} not found")
38
+ return node[0].get("id")
39
+
40
+ def _create_temp_portlist(gmp, ports: str) -> Tuple[str, str]:
41
+ """
42
+ Create a temporary port list in GVM for a scan.
43
+ ports: string like "22,80,443" or "1000-2000"
44
+ Returns: (portlist_id, name)
45
+ """
46
+ tmp_name = f"pt-{ports}-{time.strftime('%H:%M:%S')}"
47
+ created_xml = gmp.create_port_list(name=tmp_name, port_range=ports)
48
+ root = _ensure_tree(created_xml)
49
+ pid = root.findtext("id") or (root.xpath("//@id")[0] if root.xpath("//@id") else None)
50
+ if not pid:
51
+ raise RuntimeError("Failed to create temporary port list")
52
+ return pid, tmp_name
53
+
54
+ def _cleanup_portlist(gmp, portlist_id: str) -> None:
55
+ """
56
+ Delete a temporary port list. Ignore errors.
57
+ """
58
+ try:
59
+ gmp.delete_port_list(portlist_id)
60
+ except Exception:
61
+ pass
62
+
63
+ def get_default_scanner_id(gmp):
64
+ """
65
+ Retrieve the ID of the default OpenVAS scanner to assign tasks to.
66
+
67
+ Priority:
68
+ 1. Scanner whose name contains "openvas" and status "Alive"
69
+ 2. Any scanner whose name contains "openvas" (even if not alive)
70
+ 3. Fallback to any alive scanner (if no OpenVAS exists)
71
+ 4. Fallback to the first registered scanner
72
+
73
+ Args:
74
+ gmp: An authenticated GMP connection object.
75
+
76
+ Returns:
77
+ str: The ID of the selected scanner.
78
+
79
+ Raises:
80
+ RuntimeError: If no scanners are registered in GVMD.
81
+ """
82
+ root = _ensure_tree(gmp.get_scanners())
83
+ scanners = root.xpath("//scanner")
84
+ if not scanners:
85
+ raise RuntimeError("No scanners registered in gvmd")
86
+
87
+ # 1) Alive OpenVAS scanner
88
+ for s in scanners:
89
+ name = (s.findtext("name") or "").lower()
90
+ status = (s.findtext("status") or s.get("status") or "").lower()
91
+ if "openvas" in name and status == "alive":
92
+ return s.get("id") or s.findtext("id")
93
+
94
+ # 2) Any OpenVAS scanner (not necessarily alive)
95
+ for s in scanners:
96
+ name = (s.findtext("name") or "").lower()
97
+ if "openvas" in name:
98
+ return s.get("id") or s.findtext("id")
99
+
100
+ # 3) Any alive scanner
101
+ for s in scanners:
102
+ status = (s.findtext("status") or s.get("status") or "").lower()
103
+ if status == "alive":
104
+ return s.get("id") or s.findtext("id")
105
+
106
+ # 4) Fallback: first scanner
107
+ s = scanners[0]
108
+ return s.get("id") or s.findtext("id")
109
+
110
+
111
+ def wait_for_report(gmp, task_id, timeout=300, interval=5, verbose=False):
112
+ """
113
+ Wait until a report is available for the given task.
114
+
115
+ Strategy:
116
+ - Poll gmp.get_task(task_id) and look for report id in common places:
117
+ * .//last_report/id
118
+ * .//report/id
119
+ * .//report (check @id)
120
+ - If task status becomes a finished state (Done/Stopped/Aborted/Failed) but no report id found,
121
+ query gmp.get_reports() and filter for reports whose task/id == task_id and return the newest.
122
+ - After timeout, do a final search in reports and raise RuntimeError if nothing found.
123
+
124
+ Args:
125
+ gmp: Authenticated GMP connection object.
126
+ task_id: ID of the task.
127
+ timeout: Seconds to wait before giving up.
128
+ interval: Poll interval in seconds.
129
+ verbose: If True, prints progress messages.
130
+
131
+ Returns:
132
+ report_id (str)
133
+
134
+ Raises:
135
+ RuntimeError: If no report is found within timeout.
136
+ """
137
+ waited = 0
138
+ finished_states = {"done", "stopped", "aborted", "cancelled", "failed"}
139
+ if verbose:
140
+ print(f"[wait_for_report] waiting for report for task {task_id} (timeout={timeout}s)")
141
+
142
+ while waited < timeout:
143
+ task_xml = gmp.get_task(task_id)
144
+ # check several common locations for report id
145
+ report_id = (
146
+ task_xml.findtext(".//last_report/id")
147
+ or task_xml.findtext(".//report/id")
148
+ )
149
+
150
+ # sometimes report is present as a <report id="..."> element
151
+ if not report_id:
152
+ report_nodes = task_xml.xpath(".//report")
153
+ if report_nodes:
154
+ # try attribute 'id' first, fallback to child <id>
155
+ first = report_nodes[0]
156
+ report_id = first.get("id") or first.findtext("id")
157
+
158
+ status = (task_xml.findtext(".//status") or "").strip().lower()
159
+ if verbose:
160
+ print(f"[wait_for_report] waited={waited}s status='{status}' report_id='{report_id}'")
161
+
162
+ if report_id:
163
+ if verbose:
164
+ print(f"[wait_for_report] found report id on task: {report_id}")
165
+ return report_id
166
+
167
+ # If the task is finished but no report id on task, search reports list
168
+ if status in finished_states:
169
+ if verbose:
170
+ print(f"[wait_for_report] task in finished state '{status}' but no report on task; searching reports table...")
171
+ # search reports for this task
172
+ candidate = _find_latest_report_for_task(gmp, task_id, verbose=verbose)
173
+ if candidate:
174
+ return candidate
175
+ # else continue waiting a little in case the report is still being generated/attached
176
+
177
+ time.sleep(interval)
178
+ waited += interval
179
+
180
+ # Final attempt after timeout
181
+ if verbose:
182
+ print(f"[wait_for_report] timeout reached ({timeout}s). final search in reports...")
183
+ candidate = _find_latest_report_for_task(gmp, task_id, verbose=verbose)
184
+ if candidate:
185
+ return candidate
186
+
187
+ raise RuntimeError(f"No report found for task {task_id} after waiting {timeout} seconds")
188
+
189
+
190
+ def _find_latest_report_for_task(gmp, task_id, verbose=False):
191
+ """
192
+ Search all reports and return the newest report id for the given task_id, or None.
193
+ Uses gmp.get_reports() and XPath; returns the first match or the newest by creation_time if available.
194
+ """
195
+ reports_xml = gmp.get_reports()
196
+ reports = reports_xml.xpath(f"//report[task/id='{task_id}']")
197
+ if not reports:
198
+ if verbose:
199
+ print(f"[_find_latest_report_for_task] no reports found for task {task_id}")
200
+ return None
201
+
202
+ # prefer attribute id, else child <id>; prefer newest by creation_time if present
203
+ def extract_info(rnode):
204
+ rid = rnode.get("id") or rnode.findtext("id")
205
+ # try to get creation time if available (many report formats include it)
206
+ ctime = rnode.findtext("creation_time") or rnode.findtext("report/creation_time") or ""
207
+ return (rid, ctime)
208
+
209
+ infos = [extract_info(r) for r in reports]
210
+ # if any creation times present, sort by them (lexicographic ISO-8601 is fine)
211
+ if any(info[1] for info in infos):
212
+ infos = sorted(infos, key=lambda x: x[1] or "", reverse=True)
213
+ else:
214
+ # fallback: return first report node's id
215
+ infos = infos
216
+
217
+ chosen = infos[0][0] if infos else None
218
+ if verbose:
219
+ print(f"[_find_latest_report_for_task] candidate reports: {[i[0] for i in infos]}; chosen={chosen}")
220
+ return chosen