besapi 3.8.2__tar.gz → 3.9.1__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 (27) hide show
  1. {besapi-3.8.2/src/besapi.egg-info → besapi-3.9.1}/PKG-INFO +9 -13
  2. {besapi-3.8.2 → besapi-3.9.1}/README.md +1 -1
  3. besapi-3.9.1/setup.cfg +46 -0
  4. besapi-3.9.1/setup.py +7 -0
  5. {besapi-3.8.2 → besapi-3.9.1}/src/besapi/besapi.py +71 -34
  6. besapi-3.9.1/src/besapi/plugin_utilities_win.py +167 -0
  7. {besapi-3.8.2 → besapi-3.9.1/src/besapi.egg-info}/PKG-INFO +9 -13
  8. {besapi-3.8.2 → besapi-3.9.1}/src/besapi.egg-info/SOURCES.txt +2 -0
  9. {besapi-3.8.2 → besapi-3.9.1}/src/bescli/bescli.py +1 -1
  10. besapi-3.9.1/tests/test_besapi.py +367 -0
  11. {besapi-3.8.2 → besapi-3.9.1}/tests/tests.py +22 -3
  12. besapi-3.8.2/setup.cfg +0 -21
  13. besapi-3.8.2/setup.py +0 -21
  14. {besapi-3.8.2 → besapi-3.9.1}/LICENSE.txt +0 -0
  15. {besapi-3.8.2 → besapi-3.9.1}/MANIFEST.in +0 -0
  16. {besapi-3.8.2 → besapi-3.9.1}/pyproject.toml +0 -0
  17. {besapi-3.8.2 → besapi-3.9.1}/src/besapi/__init__.py +0 -0
  18. {besapi-3.8.2 → besapi-3.9.1}/src/besapi/__main__.py +0 -0
  19. {besapi-3.8.2 → besapi-3.9.1}/src/besapi/plugin_utilities.py +0 -0
  20. {besapi-3.8.2 → besapi-3.9.1}/src/besapi/schemas/BES.xsd +0 -0
  21. {besapi-3.8.2 → besapi-3.9.1}/src/besapi/schemas/BESAPI.xsd +0 -0
  22. {besapi-3.8.2 → besapi-3.9.1}/src/besapi/schemas/BESActionSettings.xsd +0 -0
  23. {besapi-3.8.2 → besapi-3.9.1}/src/besapi.egg-info/dependency_links.txt +0 -0
  24. {besapi-3.8.2 → besapi-3.9.1}/src/besapi.egg-info/requires.txt +2 -2
  25. {besapi-3.8.2 → besapi-3.9.1}/src/besapi.egg-info/top_level.txt +0 -0
  26. {besapi-3.8.2 → besapi-3.9.1}/src/bescli/__init__.py +0 -0
  27. {besapi-3.8.2 → besapi-3.9.1}/src/bescli/__main__.py +0 -0
@@ -1,10 +1,12 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: besapi
3
- Version: 3.8.2
3
+ Version: 3.9.1
4
4
  Summary: Library for working with the BigFix REST API
5
5
  Home-page: https://github.com/jgstew/besapi
6
6
  Author: Matt Hansen, James Stewart
7
7
  Author-email: hansen.m@psu.edu, james@jgstew.com
8
+ Maintainer: James Stewart
9
+ Maintainer-email: james@jgstew.com
8
10
  License: MIT
9
11
  Keywords: bigfix iem tem rest api
10
12
  Classifier: Programming Language :: Python :: 3
@@ -15,20 +17,14 @@ Classifier: Development Status :: 5 - Production/Stable
15
17
  Classifier: Intended Audience :: Developers
16
18
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
19
  Classifier: Topic :: System :: Systems Administration
18
- Requires-Python: >=3.7
20
+ Requires-Python: >=3.9
19
21
  Description-Content-Type: text/markdown
20
22
  License-File: LICENSE.txt
21
- Requires-Dist: requests
22
- Requires-Dist: lxml
23
23
  Requires-Dist: cmd2
24
+ Requires-Dist: lxml
25
+ Requires-Dist: requests
24
26
  Requires-Dist: setuptools
25
- Dynamic: author
26
- Dynamic: author-email
27
- Dynamic: home-page
28
- Dynamic: keywords
29
- Dynamic: license
30
- Dynamic: requires-dist
31
- Dynamic: summary
27
+ Dynamic: license-file
32
28
 
33
29
  # besapi
34
30
 
@@ -42,7 +38,7 @@ Usage:
42
38
 
43
39
  ```
44
40
  import besapi
45
- b = besapi.BESConnection('my_username', 'my_password', 'https://rootserver.domain.org:52311')
41
+ b = besapi.besapi.BESConnection('my_username', 'my_password', 'https://rootserver.domain.org:52311')
46
42
  rr = b.get('sites')
47
43
 
48
44
  # rr.request contains the original request object
@@ -10,7 +10,7 @@ Usage:
10
10
 
11
11
  ```
12
12
  import besapi
13
- b = besapi.BESConnection('my_username', 'my_password', 'https://rootserver.domain.org:52311')
13
+ b = besapi.besapi.BESConnection('my_username', 'my_password', 'https://rootserver.domain.org:52311')
14
14
  rr = b.get('sites')
15
15
 
16
16
  # rr.request contains the original request object
besapi-3.9.1/setup.cfg ADDED
@@ -0,0 +1,46 @@
1
+ [metadata]
2
+ name = besapi
3
+ author = Matt Hansen, James Stewart
4
+ author_email = hansen.m@psu.edu, james@jgstew.com
5
+ maintainer = James Stewart
6
+ maintainer_email = james@jgstew.com
7
+ version = attr: besapi.besapi.__version__
8
+ description = Library for working with the BigFix REST API
9
+ keywords = bigfix iem tem rest api
10
+ long_description = file: README.md
11
+ long_description_content_type = text/markdown
12
+ url = https://github.com/jgstew/besapi
13
+ license = MIT
14
+ license_file = LICENSE.txt
15
+ classifiers =
16
+ Programming Language :: Python :: 3
17
+ Programming Language :: Python :: 3.9
18
+ Operating System :: OS Independent
19
+ License :: OSI Approved :: MIT License
20
+ Development Status :: 5 - Production/Stable
21
+ Intended Audience :: Developers
22
+ Topic :: Software Development :: Libraries :: Python Modules
23
+ Topic :: System :: Systems Administration
24
+ package_dir =
25
+ = src
26
+
27
+ [options]
28
+ python_requires = >=3.9
29
+ include_package_data = True
30
+ packages = find:
31
+ install_requires =
32
+ cmd2
33
+ lxml
34
+ requests
35
+ setuptools
36
+
37
+ [options.package_data]
38
+ besapi = schemas/*.xsd
39
+
40
+ [options.packages.find]
41
+ where = src
42
+
43
+ [egg_info]
44
+ tag_build =
45
+ tag_date = 0
46
+
besapi-3.9.1/setup.py ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python
2
+ """File to configure python build and packaging for pip."""
3
+
4
+ from setuptools import setup
5
+
6
+ if __name__ == "__main__":
7
+ setup()
@@ -12,24 +12,20 @@ Library for communicating with the BES (BigFix) REST API.
12
12
  import configparser
13
13
  import datetime
14
14
  import hashlib
15
+ import importlib.resources
15
16
  import json
16
17
  import logging
17
18
  import os
18
19
  import random
19
20
  import site
20
21
  import string
21
- import typing
22
-
23
- try:
24
- from urllib import parse
25
- except ImportError:
26
- from urlparse import parse_qs as parse # type: ignore[no-redef,import-not-found]
22
+ import urllib.parse
27
23
 
24
+ import lxml.etree
25
+ import lxml.objectify
28
26
  import requests
29
- from lxml import etree, objectify
30
- from pkg_resources import resource_filename
31
27
 
32
- __version__ = "3.8.2"
28
+ __version__ = "3.9.1"
33
29
 
34
30
  besapi_logger = logging.getLogger("besapi")
35
31
 
@@ -48,7 +44,7 @@ def rand_password(length=20):
48
44
 
49
45
  def sanitize_txt(*args):
50
46
  """Clean arbitrary text for safe file system usage."""
51
- valid_chars = "-_.() %s%s" % (string.ascii_letters, string.digits)
47
+ valid_chars = f"-_.() {string.ascii_letters}{string.digits}"
52
48
 
53
49
  sani_args = []
54
50
  for arg in args:
@@ -158,7 +154,7 @@ def parse_bes_modtime(string_datetime):
158
154
  # )
159
155
 
160
156
 
161
- def get_action_combined_relevance(relevances: typing.List[str]):
157
+ def get_action_combined_relevance(relevances: list[str]):
162
158
  """Take array of ordered relevance clauses and return relevance string for
163
159
  action.
164
160
  """
@@ -249,17 +245,19 @@ def get_target_xml(targets=None):
249
245
  def validate_xsd(doc):
250
246
  """Validate results using XML XSDs."""
251
247
  try:
252
- xmldoc = etree.fromstring(doc)
248
+ xmldoc = lxml.etree.fromstring(doc)
253
249
  except BaseException: # pylint: disable=broad-except
254
250
  return False
255
251
 
256
252
  for xsd in ["BES.xsd", "BESAPI.xsd", "BESActionSettings.xsd"]:
257
- xmlschema_doc = etree.parse(resource_filename(__name__, "schemas/%s" % xsd))
253
+ schema_path = importlib.resources.files(__package__) / f"schemas/{xsd}"
254
+ with schema_path.open("r") as xsd_file:
255
+ xmlschema_doc = lxml.etree.parse(xsd_file)
258
256
 
259
257
  # one schema may throw an error while another will validate
260
258
  try:
261
- xmlschema = etree.XMLSchema(xmlschema_doc)
262
- except etree.XMLSchemaParseError as err:
259
+ xmlschema = lxml.etree.XMLSchema(xmlschema_doc)
260
+ except lxml.etree.XMLSchemaParseError as err:
263
261
  # this should only error if the XSD itself is malformed
264
262
  besapi_logger.error("ERROR with `%s`: %s", xsd, err)
265
263
  raise err
@@ -392,7 +390,7 @@ class BESConnection:
392
390
  if path.startswith(self.rootserver):
393
391
  url = path
394
392
  else:
395
- url = "%s/api/%s" % (self.rootserver, path)
393
+ url = f"{self.rootserver}/api/{path}"
396
394
 
397
395
  return url
398
396
 
@@ -414,9 +412,9 @@ class BESConnection:
414
412
  if validate_xml:
415
413
  logging.error(err_msg)
416
414
  raise ValueError(err_msg)
417
- else:
418
- # this is intended it validate_xml is None, but not used currently
419
- logging.warning(err_msg)
415
+
416
+ # this is intended it validate_xml is None, but not used currently
417
+ logging.warning(err_msg)
420
418
 
421
419
  self.last_connected = datetime.datetime.now()
422
420
  return RESTResult(
@@ -435,9 +433,9 @@ class BESConnection:
435
433
  if validate_xml:
436
434
  logging.error(err_msg)
437
435
  raise ValueError(err_msg)
438
- else:
439
- # this is intended it validate_xml is None, but not used currently
440
- logging.warning(err_msg)
436
+
437
+ # this is intended it validate_xml is None, but not used currently
438
+ logging.warning(err_msg)
441
439
 
442
440
  return RESTResult(
443
441
  self.session.put(self.url(path), data=data, verify=self.verify, **kwargs)
@@ -450,13 +448,50 @@ class BESConnection:
450
448
  self.session.delete(self.url(path), verify=self.verify, **kwargs)
451
449
  )
452
450
 
451
+ def session_relevance_json(self, relevance, **kwargs):
452
+ """Get Session Relevance Results in JSON.
453
+
454
+ This will submit the relevance string as json instead of html form data.
455
+ """
456
+ session_relevance = urllib.parse.quote(relevance, safe=":+")
457
+ rel_data = {"output": "json", "relevance": session_relevance}
458
+ self.last_connected = datetime.datetime.now()
459
+ result = RESTResult(
460
+ self.session.post(
461
+ self.url("query"),
462
+ data=rel_data,
463
+ verify=self.verify,
464
+ **kwargs,
465
+ )
466
+ )
467
+ return json.loads(result.text)
468
+
469
+ def session_relevance_json_array(self, relevance, **kwargs):
470
+ """Get Session Relevance Results in an array from the json return.
471
+
472
+ This will submit the relevance string as json instead of html form data.
473
+ """
474
+ result = self.session_relevance_json(relevance, **kwargs)
475
+ return result["result"]
476
+
477
+ def session_relevance_json_string(self, relevance, **kwargs):
478
+ """Get Session Relevance Results in a string from the json return.
479
+
480
+ This will submit the relevance string as json instead of html form data.
481
+ """
482
+ # not sure if the following is needed to handle some cases:
483
+ # relevance = "(it as string) of ( " + relevance + " )"
484
+ rel_result_array = self.session_relevance_json_array(relevance, **kwargs)
485
+ # Ensure each element is converted to a string
486
+ return "\n".join(map(str, rel_result_array))
487
+
453
488
  def session_relevance_xml(self, relevance, **kwargs):
454
489
  """Get Session Relevance Results XML."""
455
490
  self.last_connected = datetime.datetime.now()
456
491
  return RESTResult(
457
492
  self.session.post(
458
493
  self.url("query"),
459
- data=f"relevance={parse.quote(relevance, safe=':+')}",
494
+ data=f"relevance={urllib.parse.quote(relevance, safe=':+')}",
460
495
  verify=self.verify,
461
496
  **kwargs,
462
497
  )
@@ -626,6 +661,8 @@ class BESConnection:
626
661
  self.site_path = site_path
627
662
  return self.site_path
628
663
 
664
+ return None
665
+
629
666
  def import_bes_to_site(self, bes_file_path, site_path=None):
630
667
  """Import bes file to site."""
631
668
 
@@ -651,7 +688,7 @@ class BESConnection:
651
688
 
652
689
  def create_site_from_file(self, bes_file_path, site_type="custom"):
653
690
  """Create new site."""
654
- xml_parsed = etree.parse(bes_file_path)
691
+ xml_parsed = lxml.etree.parse(bes_file_path)
655
692
  new_site_name = xml_parsed.xpath("/BES/CustomSite/Name/text()")[0]
656
693
 
657
694
  result_site_path = self.validate_site_path(
@@ -662,7 +699,7 @@ class BESConnection:
662
699
  besapi_logger.warning("Site `%s` already exists", result_site_path)
663
700
  return None
664
701
 
665
- result_site = self.post("sites", etree.tostring(xml_parsed))
702
+ result_site = self.post("sites", lxml.etree.tostring(xml_parsed))
666
703
 
667
704
  return result_site
668
705
 
@@ -678,7 +715,7 @@ class BESConnection:
678
715
 
679
716
  def create_user_from_file(self, bes_file_path):
680
717
  """Create user from xml."""
681
- xml_parsed = etree.parse(bes_file_path)
718
+ xml_parsed = lxml.etree.parse(bes_file_path)
682
719
  new_user_name = xml_parsed.xpath("/BESAPI/Operator/Name/text()")[0]
683
720
  result_user = self.get_user(new_user_name)
684
721
 
@@ -686,7 +723,7 @@ class BESConnection:
686
723
  besapi_logger.warning("User `%s` Already Exists!", new_user_name)
687
724
  return result_user
688
725
  besapi_logger.info("Creating User `%s`", new_user_name)
689
- _ = self.post("operators", etree.tostring(xml_parsed))
726
+ _ = self.post("operators", lxml.etree.tostring(xml_parsed))
690
727
  # print(user_result)
691
728
  return self.get_user(new_user_name)
692
729
 
@@ -708,7 +745,7 @@ class BESConnection:
708
745
  def create_group_from_file(self, bes_file_path, site_path=None):
709
746
  """Create a new group."""
710
747
  site_path = self.get_current_site_path(site_path)
711
- xml_parsed = etree.parse(bes_file_path)
748
+ xml_parsed = lxml.etree.parse(bes_file_path)
712
749
  new_group_name = xml_parsed.xpath("/BES/ComputerGroup/Title/text()")[0]
713
750
 
714
751
  existing_group = self.get_computergroup(new_group_name, site_path)
@@ -719,7 +756,7 @@ class BESConnection:
719
756
 
720
757
  # print(lxml.etree.tostring(xml_parsed))
721
758
 
722
- _ = self.post(f"computergroups/{site_path}", etree.tostring(xml_parsed))
759
+ _ = self.post(f"computergroups/{site_path}", lxml.etree.tostring(xml_parsed))
723
760
 
724
761
  return self.get_computergroup(site_path, new_group_name)
725
762
 
@@ -844,7 +881,7 @@ class BESConnection:
844
881
  def update_item_from_file(self, file_path, site_path=None):
845
882
  """Update an item by name and last modified."""
846
883
  site_path = self.get_current_site_path(site_path)
847
- bes_tree = etree.parse(file_path)
884
+ bes_tree = lxml.etree.parse(file_path)
848
885
 
849
886
  with open(file_path, "rb") as f:
850
887
  content = f.read()
@@ -1130,7 +1167,7 @@ class RESTResult:
1130
1167
  """Property for python dict representation."""
1131
1168
  if self._besdict is None:
1132
1169
  if self.valid:
1133
- self._besdict = elem2dict(etree.fromstring(self.besxml))
1170
+ self._besdict = elem2dict(lxml.etree.fromstring(self.besxml))
1134
1171
  else:
1135
1172
  self._besdict = {"text": str(self)}
1136
1173
 
@@ -1154,11 +1191,11 @@ class RESTResult:
1154
1191
  def xmlparse_text(self, text):
1155
1192
  """Parse response text as xml."""
1156
1193
  if type(text) is str:
1157
- root_xml = etree.fromstring(text.encode("utf-8"))
1194
+ root_xml = lxml.etree.fromstring(text.encode("utf-8"))
1158
1195
  else:
1159
1196
  root_xml = text
1160
1197
 
1161
- return etree.tostring(root_xml, encoding="utf-8", xml_declaration=True)
1198
+ return lxml.etree.tostring(root_xml, encoding="utf-8", xml_declaration=True)
1162
1199
 
1163
1200
  def objectify_text(self, text):
1164
1201
  """Parse response text as objectified xml."""
@@ -1167,7 +1204,7 @@ class RESTResult:
1167
1204
  else:
1168
1205
  root_xml = text
1169
1206
 
1170
- return objectify.fromstring(root_xml)
1207
+ return lxml.objectify.fromstring(root_xml)
1171
1208
 
1172
1209
 
1173
1210
  def main():
@@ -0,0 +1,167 @@
1
+ """
2
+ Plugin utilities for Windows systems, including DPAPI encryption/decryption
3
+ and Windows Registry access.
4
+ """
5
+
6
+ import base64
7
+ import logging
8
+ import sys
9
+ from typing import Union
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ try:
14
+ import winreg
15
+ except (ImportError, ModuleNotFoundError) as e:
16
+ if not sys.platform.startswith("win"):
17
+ raise RuntimeError(
18
+ "This script requires the 'winreg' module, which is only available on Windows."
19
+ ) from e
20
+ else:
21
+ raise e
22
+
23
+ try:
24
+ import win32crypt
25
+ except (ImportError, ModuleNotFoundError) as e:
26
+ if not sys.platform.startswith("win"):
27
+ raise RuntimeError("This script only works on Windows systems") from e
28
+ raise ImportError(
29
+ "This script requires the pywin32 package. Install it via 'pip install pywin32'."
30
+ ) from e
31
+
32
+
33
+ def win_dpapi_encrypt_str(
34
+ plaintext: str, scope_flags: int = 4, entropy: Union[str, bytes, None] = None
35
+ ) -> Union[str, None]:
36
+ """Encrypt a string using Windows DPAPI and return it as a base64-encoded string.
37
+
38
+ Args:
39
+ plaintext (str): The string to encrypt.
40
+ scope_flags (int): The context scope for encryption (default is 4).
41
+ entropy (bytes | None): Optional entropy for encryption.
42
+
43
+ Returns:
44
+ str: The base64-encoded encrypted string.
45
+ """
46
+ if not plaintext or plaintext.strip() == "":
47
+ logger.warning("No plaintext provided for encryption.")
48
+ return None
49
+
50
+ # 1. Convert the plaintext string to bytes
51
+ plaintext_bytes = plaintext.encode("utf-8")
52
+
53
+ # 2. Call CryptProtectData.
54
+ # The last parameter (flags) is set to 4
55
+ # to indicate that the data should be encrypted in the machine context.
56
+ #
57
+ # The function returns a tuple: (description, encrypted_bytes)
58
+ # We only need the second element.
59
+ encrypted_bytes = win32crypt.CryptProtectData(
60
+ plaintext_bytes,
61
+ None, # Description
62
+ entropy, # Optional entropy
63
+ None, # Reserved
64
+ None, # Prompt Struct
65
+ scope_flags,
66
+ )
67
+
68
+ # 3. Encode the encrypted bytes to a Base64 string
69
+ encrypted_b64 = base64.b64encode(encrypted_bytes).decode("utf-8")
70
+
71
+ return encrypted_b64
72
+
73
+
74
+ def win_dpapi_decrypt_base64(
75
+ encrypted_b64: str, scope_flags: int = 4, entropy: Union[str, bytes, None] = None
76
+ ) -> Union[str, None]:
77
+ """Decrypt a base64-encoded string encrypted with Windows DPAPI.
78
+
79
+ Args:
80
+ encrypted_b64 (str): The base64-encoded encrypted string.
81
+ scope_flags (int): The context scope for decryption (default is 4).
82
+ entropy (bytes | None): Optional entropy for decryption.
83
+
84
+ Returns:
85
+ str: The decrypted string.
86
+ """
87
+ if not encrypted_b64 or encrypted_b64.strip() == "":
88
+ logger.warning("No encrypted data provided for decryption.")
89
+ return None
90
+
91
+ # 1. Decode the Base64 string to get the raw encrypted bytes
92
+ encrypted_bytes = base64.b64decode(encrypted_b64)
93
+
94
+ # 2. Call CryptUnprotectData.
95
+ # The last parameter (flags) is set to 4
96
+ # to indicate that the data was encrypted in the machine context.
97
+ #
98
+ # The function returns a tuple: (description, decrypted_bytes)
99
+ # We only need the second element.
100
+ _, decrypted_bytes = win32crypt.CryptUnprotectData(
101
+ encrypted_bytes,
102
+ entropy, # Optional entropy
103
+ None, # Reserved
104
+ None, # Prompt Struct
105
+ scope_flags,
106
+ )
107
+
108
+ return decrypted_bytes.decode("utf-8") if decrypted_bytes else None
109
+
110
+
111
+ def win_registry_value_read(hive, subkey, value_name):
112
+ """
113
+ Reads a value from the Windows Registry.
114
+
115
+ Args:
116
+ hive: The root hive (e.g., winreg.HKEY_LOCAL_MACHINE, winreg.HKEY_CURRENT_USER).
117
+ subkey: The path to the subkey (e.g., "SOFTWARE\\Microsoft\\Windows\\CurrentVersion").
118
+ value_name: The name of the value to read.
119
+
120
+ Returns:
121
+ The value data if found, otherwise None.
122
+ """
123
+ try:
124
+ # Open the specified registry key
125
+ key = winreg.OpenKey(hive, subkey, 0, winreg.KEY_READ)
126
+
127
+ # Query the value data
128
+ value_data, _ = winreg.QueryValueEx(key, value_name)
129
+
130
+ # Close the key
131
+ winreg.CloseKey(key)
132
+
133
+ return value_data
134
+ except FileNotFoundError:
135
+ logger.error("Registry key or value '%s\\%s' not found.", subkey, value_name)
136
+ return None
137
+ except Exception as e:
138
+ logger.error("An error occurred: %s", e)
139
+ return None
140
+
141
+
142
+ def get_win_registry_rest_pass():
143
+ """
144
+ Retrieves the base64 encrypted REST Password from the Windows Registry.
145
+
146
+ Returns:
147
+ The REST Password if found, otherwise None.
148
+ """
149
+ hive = winreg.HKEY_LOCAL_MACHINE
150
+ # HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\BigFix\Enterprise Server\MFSConfig
151
+ subkey = r"SOFTWARE\Wow6432Node\BigFix\Enterprise Server\MFSConfig"
152
+ value_name = "RESTPassword"
153
+
154
+ reg_value = win_registry_value_read(hive, subkey, value_name)
155
+
156
+ if not reg_value:
157
+ logger.error("No registry value found for RESTPassword.")
158
+ return None
159
+
160
+ # remove {obf} from start of string if present:
161
+ if reg_value and reg_value.startswith("{obf}"):
162
+ reg_value = reg_value[5:]
163
+
164
+ if reg_value:
165
+ return win_dpapi_decrypt_base64(reg_value)
166
+
167
+ return None
@@ -1,10 +1,12 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: besapi
3
- Version: 3.8.2
3
+ Version: 3.9.1
4
4
  Summary: Library for working with the BigFix REST API
5
5
  Home-page: https://github.com/jgstew/besapi
6
6
  Author: Matt Hansen, James Stewart
7
7
  Author-email: hansen.m@psu.edu, james@jgstew.com
8
+ Maintainer: James Stewart
9
+ Maintainer-email: james@jgstew.com
8
10
  License: MIT
9
11
  Keywords: bigfix iem tem rest api
10
12
  Classifier: Programming Language :: Python :: 3
@@ -15,20 +17,14 @@ Classifier: Development Status :: 5 - Production/Stable
15
17
  Classifier: Intended Audience :: Developers
16
18
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
19
  Classifier: Topic :: System :: Systems Administration
18
- Requires-Python: >=3.7
20
+ Requires-Python: >=3.9
19
21
  Description-Content-Type: text/markdown
20
22
  License-File: LICENSE.txt
21
- Requires-Dist: requests
22
- Requires-Dist: lxml
23
23
  Requires-Dist: cmd2
24
+ Requires-Dist: lxml
25
+ Requires-Dist: requests
24
26
  Requires-Dist: setuptools
25
- Dynamic: author
26
- Dynamic: author-email
27
- Dynamic: home-page
28
- Dynamic: keywords
29
- Dynamic: license
30
- Dynamic: requires-dist
31
- Dynamic: summary
27
+ Dynamic: license-file
32
28
 
33
29
  # besapi
34
30
 
@@ -42,7 +38,7 @@ Usage:
42
38
 
43
39
  ```
44
40
  import besapi
45
- b = besapi.BESConnection('my_username', 'my_password', 'https://rootserver.domain.org:52311')
41
+ b = besapi.besapi.BESConnection('my_username', 'my_password', 'https://rootserver.domain.org:52311')
46
42
  rr = b.get('sites')
47
43
 
48
44
  # rr.request contains the original request object
@@ -8,6 +8,7 @@ src/besapi/__init__.py
8
8
  src/besapi/__main__.py
9
9
  src/besapi/besapi.py
10
10
  src/besapi/plugin_utilities.py
11
+ src/besapi/plugin_utilities_win.py
11
12
  src/besapi.egg-info/PKG-INFO
12
13
  src/besapi.egg-info/SOURCES.txt
13
14
  src/besapi.egg-info/dependency_links.txt
@@ -19,4 +20,5 @@ src/besapi/schemas/BESActionSettings.xsd
19
20
  src/bescli/__init__.py
20
21
  src/bescli/__main__.py
21
22
  src/bescli/bescli.py
23
+ tests/test_besapi.py
22
24
  tests/tests.py
@@ -32,7 +32,7 @@ except ImportError:
32
32
  try:
33
33
  from besapi.besapi import __version__
34
34
  except ImportError:
35
- from besapi import __version__ # type: ignore
35
+ from besapi import __version__ # type: ignore[attr-defined, no-redef]
36
36
 
37
37
 
38
38
  class BESCLInterface(Cmd):
@@ -0,0 +1,367 @@
1
+ #!/usr/bin/env python
2
+ """Test besapi with pytest.
3
+
4
+ This was converted from tests/tests.py which was used before this pytest was added.
5
+ """
6
+
7
+ import json
8
+ import os
9
+ import random
10
+ import subprocess
11
+ import sys
12
+
13
+ import pytest
14
+
15
+ # mypy: disable-error-code="arg-type"
16
+
17
+ if not os.getenv("TEST_PIP"):
18
+ # add module folder to import paths for testing local src
19
+ sys.path.append(
20
+ os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "src")
21
+ )
22
+ # reverse the order so we make sure to get the local src module
23
+ sys.path.reverse()
24
+
25
+ import besapi
26
+ import besapi.plugin_utilities
27
+
28
+ if os.name == "nt":
29
+ import besapi.plugin_utilities_win
30
+
31
+
32
+ def test_besapi_version():
33
+ """Test that the besapi version is not None."""
34
+ assert besapi.besapi.__version__ is not None
35
+
36
+
37
+ def test_rand_password():
38
+ """Test that the generated random password has the correct length."""
39
+ assert 15 == len(besapi.besapi.rand_password(15))
40
+
41
+
42
+ def test_sanitize_txt():
43
+ """Test that the sanitize_txt function works correctly."""
44
+ assert ("test--string", "test") == besapi.besapi.sanitize_txt(
45
+ r"test/\string", "test%"
46
+ )
47
+
48
+
49
+ def test_replace_text_between():
50
+ """Test that the replace_text_between function works correctly."""
51
+ assert "http://localhost:52311/file.example" == besapi.besapi.replace_text_between(
52
+ "http://example:52311/file.example", "://", ":52311", "localhost"
53
+ )
54
+
55
+
56
+ def test_validate_site_path():
57
+ """Test the validate_site_path function with various inputs."""
58
+ assert "master" in besapi.besapi.BESConnection.validate_site_path(
59
+ "", "master", False
60
+ )
61
+ assert "custom/" in besapi.besapi.BESConnection.validate_site_path(
62
+ "", "custom/Example", False
63
+ )
64
+ assert "operator/" in besapi.besapi.BESConnection.validate_site_path(
65
+ "", "operator/Example", False
66
+ )
67
+
68
+
69
+ def test_validate_xml_bes_file():
70
+ """Test the validate_xml_bes_file function with good and bad files."""
71
+ assert besapi.besapi.validate_xml_bes_file("tests/good/RelaySelectTask.bes") is True
72
+ assert (
73
+ besapi.besapi.validate_xml_bes_file("tests/good/ComputerGroupsExample.bes")
74
+ is True
75
+ )
76
+ assert (
77
+ besapi.besapi.validate_xml_bes_file("tests/bad/RelaySelectTask_BAD.bes")
78
+ is False
79
+ )
80
+ assert (
81
+ besapi.besapi.validate_xml_bes_file("tests/bad/ComputerGroups_BAD.bes") is False
82
+ )
83
+
84
+
85
+ def test_failing_validate_site_path():
86
+ """Test that validate_site_path raises ValueError for invalid inputs."""
87
+
88
+ with pytest.raises(ValueError):
89
+ besapi.besapi.BESConnection.validate_site_path("", "bad/Example", False)
90
+
91
+ with pytest.raises(ValueError):
92
+ besapi.besapi.BESConnection.validate_site_path("", "bad/master", False)
93
+
94
+ with pytest.raises(ValueError):
95
+ besapi.besapi.BESConnection.validate_site_path("", "", False, True)
96
+
97
+ with pytest.raises(ValueError):
98
+ besapi.besapi.BESConnection.validate_site_path("", None, False, True)
99
+
100
+
101
+ class RequestResult:
102
+ text = "this is just a test"
103
+ headers: list = []
104
+
105
+
106
+ def test_rest_result():
107
+ """Test the RESTResult class."""
108
+ request_result = RequestResult()
109
+ rest_result = besapi.besapi.RESTResult(request_result)
110
+
111
+ assert rest_result.besdict is not None
112
+ assert rest_result.besjson is not None
113
+ assert b"<BES>Example</BES>" in rest_result.xmlparse_text("<BES>Example</BES>")
114
+ assert rest_result.text == "this is just a test"
115
+
116
+
117
+ def test_parse_bes_modtime():
118
+ """Test the parse_bes_modtime function."""
119
+ assert (
120
+ 2017 == besapi.besapi.parse_bes_modtime("Tue, 05 Sep 2017 23:31:48 +0000").year
121
+ )
122
+
123
+
124
+ def test_get_action_combined_relevance():
125
+ """Test the get_action_combined_relevance function."""
126
+ assert (
127
+ "( ( True ) AND ( windows of operating system ) ) AND ( False )"
128
+ == besapi.besapi.get_action_combined_relevance(
129
+ ["True", "windows of operating system", "False"]
130
+ )
131
+ )
132
+
133
+
134
+ def test_get_target_xml():
135
+ """Test the get_target_xml function with various inputs."""
136
+ assert "<CustomRelevance>False</CustomRelevance>" == besapi.besapi.get_target_xml()
137
+ assert "<AllComputers>true</AllComputers>" == besapi.besapi.get_target_xml(
138
+ "<AllComputers>"
139
+ )
140
+ assert "<ComputerID>1</ComputerID>" == besapi.besapi.get_target_xml(1)
141
+ assert (
142
+ "<CustomRelevance><![CDATA[not windows of operating system]]></CustomRelevance>"
143
+ == besapi.besapi.get_target_xml("not windows of operating system")
144
+ )
145
+ assert (
146
+ "<ComputerID>1</ComputerID><ComputerID>2</ComputerID>"
147
+ == besapi.besapi.get_target_xml([1, 2])
148
+ )
149
+ assert (
150
+ "<ComputerName>Computer 1</ComputerName><ComputerName>Another Computer</ComputerName>"
151
+ == besapi.besapi.get_target_xml(["Computer 1", "Another Computer"])
152
+ )
153
+
154
+
155
+ def test_bescli():
156
+ """Test the BESCLInterface class and its methods."""
157
+ import bescli
158
+
159
+ bigfix_cli = bescli.bescli.BESCLInterface()
160
+
161
+ # just make sure these don't throw errors:
162
+ bigfix_cli.do_ls()
163
+ bigfix_cli.do_clear()
164
+ bigfix_cli.do_ls()
165
+ bigfix_cli.do_logout()
166
+ bigfix_cli.do_error_count()
167
+ bigfix_cli.do_version()
168
+ bigfix_cli.do_conf()
169
+
170
+ # this should really only run if the config file is present:
171
+ if bigfix_cli.bes_conn:
172
+ # session relevance tests require functioning web reports server
173
+ assert (
174
+ int(bigfix_cli.bes_conn.session_relevance_string("number of bes computers"))
175
+ > 0
176
+ )
177
+ assert (
178
+ "test session relevance string result"
179
+ in bigfix_cli.bes_conn.session_relevance_string(
180
+ '"test session relevance string result"'
181
+ )
182
+ )
183
+ bigfix_cli.do_set_current_site("master")
184
+
185
+ # set working directory to folder this file is in:
186
+ os.chdir(os.path.dirname(os.path.abspath(__file__)))
187
+
188
+ # set working directory to src folder in parent folder
189
+ os.chdir("../src")
190
+
191
+ # Test file upload:
192
+ upload_result = bigfix_cli.bes_conn.upload(
193
+ "./besapi/__init__.py", "test_besapi_upload.txt"
194
+ )
195
+ # print(upload_result)
196
+ assert upload_result.besobj.FileUpload.Available == 1
197
+ assert upload_result.besdict["FileUpload"]["Available"] == "1"
198
+ assert upload_result.besjson is not None
199
+ upload_result_json = json.loads(upload_result.besjson)
200
+ assert upload_result_json["FileUpload"]["Available"] == "1"
201
+
202
+ assert "test_besapi_upload.txt</URL>" in str(upload_result)
203
+ upload_prefetch = bigfix_cli.bes_conn.parse_upload_result_to_prefetch(
204
+ upload_result
205
+ )
206
+ # print(upload_prefetch)
207
+ assert "prefetch test_besapi_upload.txt sha1:" in upload_prefetch
208
+
209
+ dashboard_name = "_PyBESAPI_tests.py"
210
+ var_name = "TestVarName"
211
+ var_value = "TestVarValue " + str(random.randint(0, 9999))
212
+
213
+ assert var_value in str(
214
+ bigfix_cli.bes_conn.set_dashboard_variable_value(
215
+ dashboard_name, var_name, var_value
216
+ )
217
+ )
218
+
219
+ assert var_value in str(
220
+ bigfix_cli.bes_conn.get_dashboard_variable_value(dashboard_name, var_name)
221
+ )
222
+
223
+ if os.name == "nt":
224
+ subprocess.run(
225
+ 'CMD /C python -m besapi ls clear ls conf "query number of bes computers" version error_count exit',
226
+ check=True,
227
+ )
228
+
229
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
230
+ print("login succeeded:", bes_conn.login())
231
+ assert bes_conn.login()
232
+
233
+
234
+ def test_bes_conn_json():
235
+ """Test the BESConnection class with JSON output."""
236
+ bes_conn = besapi.besapi.get_bes_conn_using_config_file()
237
+ if bes_conn and bes_conn.login():
238
+ print("testing session_relevance_json")
239
+ result = bes_conn.session_relevance_json("number of bes computers")
240
+ assert result is not None
241
+ assert int(result["result"][0]) > 0
242
+ result = bes_conn.session_relevance_json(
243
+ """("[%22" & it & "%22]") of concatenation "%22, %22" of names of bes computers"""
244
+ )
245
+ assert result is not None
246
+ string_first_result_json = result["result"][0]
247
+ print(string_first_result_json)
248
+ assert '", "' in string_first_result_json
249
+ assert '["' in string_first_result_json
250
+ # NOTE: the following check is specific to my env:
251
+ if "10.0." in bes_conn.rootserver:
252
+ print("doing check specific to my env")
253
+ assert '", "BIGFIX", "' in string_first_result_json
254
+
255
+ print("testing session_relevance_json_array")
256
+ result = bes_conn.session_relevance_json_array("number of bes computers")
257
+ print(result)
258
+ assert result is not None
259
+ assert int(result[0]) > 0
260
+ print("testing session_relevance_json_string")
261
+ result = bes_conn.session_relevance_json_string("number of bes computers")
262
+ print(result)
263
+ assert result is not None
264
+ assert int(result) > 0
265
+ print("testing session_relevance_json_string tuple")
266
+ result = bes_conn.session_relevance_json_string(
267
+ '(ids of it, names of it, "TestString") of bes computers'
268
+ )
269
+ print(result)
270
+ assert result is not None
271
+ assert "TestString" in result
272
+ # NOTE: the following check is specific to my env:
273
+ if "10.0." in bes_conn.rootserver:
274
+ print("doing check specific to my env")
275
+ assert "BIGFIX" in result
276
+
277
+
278
+ def test_plugin_utilities_logging():
279
+ """Test the plugin_utilities module."""
280
+ print(besapi.plugin_utilities.get_invoke_folder())
281
+ print(besapi.plugin_utilities.get_invoke_file_name())
282
+
283
+ parser = besapi.plugin_utilities.setup_plugin_argparse(plugin_args_required=False)
284
+ # allow unknown args to be parsed instead of throwing an error:
285
+ args, _unknown = parser.parse_known_args()
286
+
287
+ # test logging plugin_utilities:
288
+ import logging
289
+
290
+ logging_config = besapi.plugin_utilities.get_plugin_logging_config("./tests.log")
291
+
292
+ # this use of logging.basicConfig requires python >= 3.9
293
+ if sys.version_info >= (3, 9):
294
+ logging.basicConfig(**logging_config)
295
+
296
+ logging.warning("Just testing to see if logging is working!")
297
+
298
+ assert os.path.isfile("./tests.log")
299
+
300
+
301
+ def test_plugin_utilities_win_dpapi():
302
+ """Test the Windows DPAPI encryption function, if on Windows."""
303
+ if not os.name == "nt":
304
+ pytest.skip("Skipping Windows Registry test on non-Windows system.")
305
+
306
+ # only run this test if besapi > v3.8.3:
307
+ if besapi.besapi.__version__ <= "3.8.3":
308
+ pytest.skip("Skipping test for besapi <= 3.8.3")
309
+
310
+ test_string = "This is just a test string " + str(random.randint(0, 9999))
311
+ encrypted_str = besapi.plugin_utilities_win.win_dpapi_encrypt_str(test_string)
312
+ print("Encrypted string:", encrypted_str)
313
+ assert encrypted_str != ""
314
+ assert encrypted_str != test_string
315
+ decrypted_str = besapi.plugin_utilities_win.win_dpapi_decrypt_base64(encrypted_str)
316
+ print("Decrypted string:", decrypted_str)
317
+ assert decrypted_str == test_string
318
+
319
+
320
+ def test_plugin_utilities_win_win_registry_value_read():
321
+ """Test reading a Windows registry value."""
322
+ if not os.name == "nt":
323
+ pytest.skip("Skipping Windows Registry test on non-Windows system.")
324
+
325
+ # only run this test if besapi > v3.8.3:
326
+ if besapi.besapi.__version__ <= "3.8.3":
327
+ pytest.skip("Skipping test for besapi <= 3.8.3")
328
+
329
+ import winreg
330
+
331
+ registry_key = r"SOFTWARE\Microsoft\Windows\CurrentVersion"
332
+ registry_value = "ProgramFilesDir"
333
+ result = besapi.plugin_utilities_win.win_registry_value_read(
334
+ winreg.HKEY_LOCAL_MACHINE, registry_key, registry_value
335
+ )
336
+
337
+ assert result is not None
338
+ print("Registry value:", result)
339
+ assert "Program Files" in result
340
+
341
+
342
+ def test_plugin_utilities_win_get_win_registry_rest_pass():
343
+ """Test getting the Windows Registry REST password."""
344
+ if not os.name == "nt":
345
+ pytest.skip("Skipping Windows Registry test on non-Windows system.")
346
+
347
+ # only run this test if besapi > v3.8.3:
348
+ if besapi.besapi.__version__ <= "3.8.3":
349
+ pytest.skip("Skipping test for besapi <= 3.8.3")
350
+
351
+ import winreg
352
+
353
+ test_string = "This is just a test string " + str(random.randint(0, 9999))
354
+ encrypted_str = besapi.plugin_utilities_win.win_dpapi_encrypt_str(test_string)
355
+
356
+ # write encrypted string to registry for testing:
357
+ # HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\BigFix\Enterprise Server\MFSConfig
358
+ subkey = r"SOFTWARE\Wow6432Node\BigFix\Enterprise Server\MFSConfig"
359
+
360
+ key = winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, subkey)
361
+ winreg.SetValueEx(key, "RESTPassword", 0, winreg.REG_SZ, "{obf}" + encrypted_str)
362
+ winreg.CloseKey(key)
363
+
364
+ result = besapi.plugin_utilities_win.get_win_registry_rest_pass()
365
+ assert result is not None
366
+ print("Windows Registry REST password:", result)
367
+ assert result == test_string
@@ -2,6 +2,7 @@
2
2
  """Test besapi."""
3
3
 
4
4
  import argparse
5
+ import json
5
6
  import os
6
7
  import random
7
8
  import subprocess
@@ -46,6 +47,13 @@ assert "operator/" in besapi.besapi.BESConnection.validate_site_path(
46
47
  "", "operator/Example", False
47
48
  )
48
49
 
50
+ assert besapi.besapi.validate_xml_bes_file("tests/good/RelaySelectTask.bes") is True
51
+ assert (
52
+ besapi.besapi.validate_xml_bes_file("tests/good/ComputerGroupsExample.bes") is True
53
+ )
54
+
55
+ assert besapi.besapi.validate_xml_bes_file("tests/bad/RelaySelectTask_BAD.bes") is False
56
+
49
57
  # start failing tests:
50
58
  raised_errors = 0
51
59
 
@@ -77,9 +85,9 @@ assert raised_errors == 4
77
85
  # end failing tests
78
86
 
79
87
 
80
- class RequestResult(object):
88
+ class RequestResult:
81
89
  text = "this is just a test"
82
- headers = [] # type: ignore
90
+ headers: list = []
83
91
 
84
92
 
85
93
  request_result = RequestResult()
@@ -159,9 +167,20 @@ if bigfix_cli.bes_conn:
159
167
  upload_result = bigfix_cli.bes_conn.upload(
160
168
  "./besapi/__init__.py", "test_besapi_upload.txt"
161
169
  )
170
+
162
171
  # print(upload_result)
172
+ # print(upload_result.besobj.FileUpload.Available)
173
+ assert upload_result.besobj.FileUpload.Available == 1
163
174
  assert "test_besapi_upload.txt</URL>" in str(upload_result)
164
- print(bigfix_cli.bes_conn.parse_upload_result_to_prefetch(upload_result))
175
+ upload_result_json = json.loads(upload_result.besjson)
176
+ # print(upload_result_json["FileUpload"]["Available"])
177
+ assert upload_result_json["FileUpload"]["Available"] == "1"
178
+
179
+ upload_prefetch = bigfix_cli.bes_conn.parse_upload_result_to_prefetch(upload_result)
180
+ # print(upload_prefetch)
181
+ assert "prefetch test_besapi_upload.txt sha1:" in upload_prefetch
182
+ assert "test_besapi_upload.txt" in str(upload_result)
183
+ # print(bigfix_cli.bes_conn.parse_upload_result_to_prefetch(upload_result))
165
184
 
166
185
  dashboard_name = "_PyBESAPI_tests.py"
167
186
  var_name = "TestVarName"
besapi-3.8.2/setup.cfg DELETED
@@ -1,21 +0,0 @@
1
- [metadata]
2
- version = attr: besapi.besapi.__version__
3
- long_description = file: README.md
4
- long_description_content_type = text/markdown
5
- classifiers =
6
- Programming Language :: Python :: 3
7
- Programming Language :: Python :: 3.9
8
- Operating System :: OS Independent
9
- License :: OSI Approved :: MIT License
10
- Development Status :: 5 - Production/Stable
11
- Intended Audience :: Developers
12
- Topic :: Software Development :: Libraries :: Python Modules
13
- Topic :: System :: Systems Administration
14
-
15
- [options]
16
- python_requires = >=3.7
17
-
18
- [egg_info]
19
- tag_build =
20
- tag_date = 0
21
-
besapi-3.8.2/setup.py DELETED
@@ -1,21 +0,0 @@
1
- #!/usr/bin/env python
2
- """File to configure python build and packaging for pip."""
3
-
4
- import setuptools
5
-
6
- setuptools.setup(
7
- name="besapi",
8
- # version= moved to setup.cfg
9
- author="Matt Hansen, James Stewart",
10
- author_email="hansen.m@psu.edu, james@jgstew.com",
11
- description="Library for working with the BigFix REST API",
12
- license="MIT",
13
- keywords="bigfix iem tem rest api",
14
- url="https://github.com/jgstew/besapi",
15
- # long_description= moved to setup.cfg
16
- packages=["besapi", "bescli"],
17
- package_data={"besapi": ["schemas/*.xsd"]},
18
- install_requires=["requests", "lxml", "cmd2", "setuptools"],
19
- include_package_data=True,
20
- package_dir={"": "src"},
21
- )
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1,4 +1,4 @@
1
- requests
2
- lxml
3
1
  cmd2
2
+ lxml
3
+ requests
4
4
  setuptools
File without changes
File without changes