Keine Beschreibung

generator.py 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. # Read a directory
  2. # Find python functions
  3. # Generate yaml
  4. # FIXME:
  5. # Position, default_value and function in params
  6. # TO ADD:
  7. # from walkoff_app_sdk.app_base import AppBase
  8. # class TheHive(AppBase): <-- Add appbase
  9. # __version__ = version within class
  10. # app_name = app_name in class
  11. # if __name__ == "__main__":
  12. # asyncio.run(TheHive.run(), debug=True) <-- APPEND SHIT HERE
  13. # async infront of every function?
  14. # Add async library to imports
  15. # Make wrapper class? <-- within app.py
  16. # 1. Generate app.yaml (functions with returns etc)
  17. # 2. Generate app.py (with imports to the original function etc
  18. # 3. Build requirements.txt based on the items necessary
  19. # 4. Check whether it runs?
  20. import os
  21. import yaml
  22. import jedi
  23. import shutil
  24. # Testing generator
  25. entrypoint_directory = "thehive4py"
  26. include_requirements = False
  27. if not os.path.exists(entrypoint_directory):
  28. include_requirements = True
  29. print("Requires library in requirements")
  30. source = '''
  31. import %s
  32. %s.
  33. ''' % (entrypoint_directory, entrypoint_directory)
  34. splitsource = source.split("\n")
  35. # Find modules AKA files
  36. def get_modules():
  37. curline = splitsource[-2]
  38. print(splitsource, curline)
  39. entrypoint = jedi.Script(source, line=len(splitsource)-1, column=len(curline))
  40. modules = []
  41. completions = entrypoint.completions()
  42. for item in completions:
  43. if item.type != "module":
  44. continue
  45. modules.append(item.name)
  46. return modules
  47. def loop_modules(modules, data):
  48. # Loop modules AKA files - this is garbage but works lmao
  49. for module in modules:
  50. modulesplit = list(splitsource)
  51. modulesplit[2] = "%s%s." % (modulesplit[2], module)
  52. #print(modulesplit)
  53. source = "\n".join(modulesplit)
  54. entrypoint = jedi.Script(source, line=len(modulesplit)-1, column=len(modulesplit[2]))
  55. # Loop classes in the files
  56. for classcompletion in entrypoint.completions():
  57. if classcompletion.type != "class":
  58. continue
  59. if not classcompletion.full_name.startswith(modulesplit[2]):
  60. continue
  61. # Same thing again, but for functions within classes
  62. # CBA with subclasses etc atm
  63. #print(classcompletion.full_name, modulesplit[2])
  64. classplit = list(modulesplit)
  65. classplit[2] = "%s." % (classcompletion.full_name)
  66. #print(modulesplit)
  67. source = "\n".join(classplit)
  68. entrypoint = jedi.Script(source, line=len(classplit)-1, column=len(classplit[2]))
  69. # List of functions sorted by their name
  70. nameinternalfunctions = []
  71. for functioncompletion in entrypoint.completions():
  72. if functioncompletion.type != "function":
  73. continue
  74. if not functioncompletion.full_name.startswith(classplit[2]):
  75. continue
  76. nameinternalfunctions.append(functioncompletion)
  77. #print(nameinternalfunctions)
  78. # List of functions sorted by their line in the file (reversed)
  79. # CODE USED TO ACTUALLY PRINT THE CODE
  80. #prevnumber = 0
  81. #numberinternalfunctions = sorted(nameinternalfunctions, key=lambda k: k.line, reverse=True)
  82. numberinternalfunctions = sorted(nameinternalfunctions, key=lambda k: k.line)
  83. prevnumber = 0
  84. origparent = "TheHiveApi"
  85. # Predefined functions? - maybe skip: __init__
  86. skip_functions = ["__init__"]
  87. skip_parameters = [""]
  88. cnt = 0
  89. for item in numberinternalfunctions:
  90. if item.parent().name != origparent:
  91. continue
  92. # FIXME - prolly wrong
  93. if item.name in skip_functions or (item.name.startswith("__") and item.name.endswith("__")):
  94. continue
  95. # FIXME - remove
  96. #print(item.get_line_code())
  97. #if "=" not in item.get_line_code():
  98. # continue
  99. #if item.docstring() in item.get_line_code():
  100. # print("NO DOCSTRING FOR: %s. Skipping!" % item.name)
  101. # cnt += 1
  102. # continue
  103. curfunction = {
  104. "name": item.name,
  105. "description": "HEY",
  106. }
  107. params = []
  108. curreturn = {}
  109. function = item.docstring().split("\n")[0]
  110. for line in item.docstring().split("\n"):
  111. if not line:
  112. continue
  113. linesplit = line.split(" ")
  114. try:
  115. curname = linesplit[1][:-1]
  116. except IndexError as e:
  117. print("IndexError: %s. Line: %s" % (e, line))
  118. continue
  119. paramfound = False
  120. foundindex = 0
  121. cnt = 0
  122. for param in params:
  123. #print(param["name"], curname)
  124. if param["name"] == curname:
  125. #print("ALREADY EXISTS: %s" % curname)
  126. paramfound = True
  127. foundindex = cnt
  128. break
  129. cnt += 1
  130. # CBA finding a good parser, as that seemed impossible :(
  131. # Skipped :return
  132. if line.startswith(":param"):
  133. if not paramfound:
  134. #print("HERE!: %s" % line)
  135. curparam = {}
  136. #print(line)
  137. curparam["name"] = curname
  138. curparam["description"] = " ".join(linesplit[2:])
  139. #print(curparam["description"])
  140. if "\r\n" in curparam["description"]:
  141. curparam["description"] = " ".join(curparam["description"].split("\r\n"))
  142. if "\n" in curparam["description"]:
  143. curparam["description"] = " ".join(curparam["description"].split("\n"))
  144. curparam["function"] = function
  145. #curparam["docstring"] = item.docstring()
  146. params.append(curparam)
  147. elif line.startswith(":type"):
  148. if paramfound:
  149. params[foundindex]["schema"] = {}
  150. params[foundindex]["schema"]["type"] = " ".join(linesplit[2:])
  151. #print(params)
  152. #print(line)
  153. elif line.startswith(":rtype"):
  154. curreturn["type"] = " ".join(linesplit[1:])
  155. # Check whether param is required
  156. # FIXME - remove
  157. #if len(params) != 0:
  158. # print(params)
  159. # continue
  160. #print(function)
  161. #print(params)
  162. # FIXME - this might crash when missing docstrings
  163. # FIXME - is also bad splitt (can be written without e.g. spaces
  164. # This should maybe be done first? idk
  165. fields = function.split("(")[1][:-1].split(", ")
  166. if len(params) == 0:
  167. # Handle missing docstrings
  168. params = []
  169. for item in fields:
  170. params.append({
  171. "name": item,
  172. "description": "",
  173. "schema": {},
  174. "function": function,
  175. })
  176. cnt = 0
  177. for param in params:
  178. found = False
  179. for field in fields:
  180. if param["name"] in field:
  181. if "=" in field:
  182. param["required"] = False
  183. param["default_value"] = field
  184. else:
  185. param["required"] = True
  186. found = True
  187. break
  188. if not param.get("schema"):
  189. #print("Defining object schema for %s" % param["name"])
  190. param["schema"] = {}
  191. param["schema"]["type"] = "object"
  192. param["position"] = cnt
  193. if not found:
  194. # FIXME - what here?
  195. pass
  196. #print("HANDLE NOT FOUND")
  197. #print(param)
  198. #print(fields)
  199. cnt += 1
  200. if len(params) > 0:
  201. curfunction["parameters"] = params
  202. if not curfunction.get("returns"):
  203. curfunction["returns"] = {}
  204. curfunction["returns"]["schema"] = {}
  205. curfunction["returns"]["schema"]["type"] = "object"
  206. #print(curfunction)
  207. try:
  208. print("Finished prepping %s with %d parameters and return %s" % (item.name, len(curfunction["parameters"]), curfunction["returns"]["schema"]["type"]))
  209. except KeyError as e:
  210. print("Error: %s" % e)
  211. #print("Finished prepping %s with 0 parameters and return %s" % (item.name, curfunction["returns"]["schema"]["type"]))
  212. curfunction["parameters"] = []
  213. except AttributeError as e:
  214. pass
  215. try:
  216. data["actions"].append(curfunction)
  217. except KeyError:
  218. data["actions"] = []
  219. data["actions"].append(curfunction)
  220. #return data
  221. # FIXME
  222. #if cnt == breakcnt:
  223. # break
  224. #cnt += 1
  225. # Check if
  226. # THIS IS TO GET READ THE ACTUAL CODE
  227. #functioncode = item.get_line_code(after=prevnumber-item.line-1)
  228. #prevnumber = item.line
  229. # break
  230. return data
  231. # Generates the base information necessary to make an api.yaml file
  232. def generate_base_yaml(filename, version, appname):
  233. print("Generating base app for library %s with version %s" % (appname, version))
  234. data = {
  235. "walkoff_version": "0.0.1",
  236. "app_version": version,
  237. "name": appname,
  238. "description": "Autogenerated yaml with @Frikkylikeme's generator",
  239. "contact_info": {
  240. "name": "@frikkylikeme",
  241. "url": "https://github.com/frikky",
  242. }
  243. }
  244. return data
  245. def generate_app(filepath, data):
  246. tbd = [
  247. "library_path",
  248. "import_class",
  249. "required_init"
  250. ]
  251. # FIXME - add to data dynamically and remove
  252. data["library_path"] = "thehive4py.api"
  253. data["import_class"] = "TheHiveApi"
  254. data["required_init"] = {"url": "http://localhost:9000", "principal": "asd"}
  255. wrapperstring = ""
  256. cnt = 0
  257. # FIXME - only works for strings currently
  258. for key, value in data["required_init"].items():
  259. if cnt != len(data["required_init"]):
  260. wrapperstring += "%s=\"%s\", " % (key, value)
  261. cnt += 1
  262. wrapperstring = wrapperstring[:-2]
  263. wrapper = "self.wrapper = %s(%s)" % (data["import_class"], wrapperstring)
  264. name = data["name"]
  265. if ":" in data["name"]:
  266. name = data["name"].split(":")[0]
  267. if not data.get("actions"):
  268. print("No actions found for %s in path %s" % (entrypoint_directory, data["library_path"]))
  269. print("Folder might be missing (or unexported (__init__.py), library not installed (pip) or library action missing")
  270. exit()
  271. functions = []
  272. for action in data["actions"]:
  273. internalparamstring = ""
  274. paramstring = ""
  275. try:
  276. for param in action["parameters"]:
  277. if param["required"] == False:
  278. paramstring += "%s, " % (param["default_value"])
  279. else:
  280. paramstring += "%s, " % param["name"]
  281. except KeyError:
  282. action["parameters"] = []
  283. #internalparamstring += "%s, " % param["name"]
  284. paramstring = paramstring[:-2]
  285. #internalparamstring = internalparamstring[:-2]
  286. functionstring = ''' async def %s(%s):
  287. return self.wrapper.%s(%s)
  288. ''' % (action["name"], paramstring, action["name"], paramstring)
  289. functions.append(functionstring)
  290. filedata = '''from walkoff_app_sdk.app_base import AppBase
  291. import asyncio
  292. from %s import %s
  293. class %sWrapper(AppBase):
  294. __version__ = "%s"
  295. app_name = "%s"
  296. def __init__(self, redis, logger, console_logger=None):
  297. """
  298. Each app should have this __init__ to set up Redis and logging.
  299. :param redis:
  300. :param logger:
  301. :param console_logger:
  302. """
  303. super().__init__(redis, logger, console_logger)
  304. %s
  305. %s
  306. if __name__ == "__main__":
  307. asyncio.run(%sWrapper.run(), debug=True)
  308. ''' % ( \
  309. data["library_path"],
  310. data["import_class"],
  311. name,
  312. data["app_version"],
  313. name,
  314. wrapper,
  315. "\n".join(functions),
  316. name
  317. )
  318. # Simple key cleanup
  319. for item in tbd:
  320. try:
  321. del data[item]
  322. except KeyError:
  323. pass
  324. tbd_action = []
  325. tbd_param = [
  326. "position",
  327. "default_value",
  328. "function"
  329. ]
  330. for action in data["actions"]:
  331. for param in action["parameters"]:
  332. for item in tbd_param:
  333. try:
  334. del param[item]
  335. except KeyError:
  336. pass
  337. for item in tbd_action:
  338. try:
  339. del action[item]
  340. except KeyError:
  341. pass
  342. # FIXME - add how to initialize the class
  343. with open(filepath, "w") as tmp:
  344. tmp.write(filedata)
  345. return data
  346. def dump_yaml(filename, data):
  347. with open(filename, 'w') as outfile:
  348. yaml.dump(data, outfile, default_flow_style=False)
  349. def build_base_structure(appname, version):
  350. outputdir = "generated"
  351. app_path = "%s/%s" % (outputdir, appname)
  352. filepath = "%s/%s" % (app_path, version)
  353. srcdir_path = "%s/src" % (filepath)
  354. directories = [
  355. outputdir,
  356. app_path,
  357. filepath,
  358. srcdir_path
  359. ]
  360. for directory in directories:
  361. try:
  362. os.mkdir(directory)
  363. except FileExistsError:
  364. print("%s already exists. Skipping." % directory)
  365. # "docker-compose.yml",
  366. # "env.txt",
  367. filenames = [
  368. "Dockerfile",
  369. "requirements.txt"
  370. ]
  371. #if strings.
  372. # include_requirements = False
  373. for filename in filenames:
  374. ret = shutil.copyfile("baseline/%s" % filename, "%s/%s" % (filepath, filename))
  375. print("Copied baseline/%s." % filename)
  376. def move_files(appname, version):
  377. applocation = "../../functions/apps/%s" % appname
  378. if not os.path.exists("../../functions/apps"):
  379. os.mkdir("../../functions/apps")
  380. if not os.path.exists(applocation):
  381. os.mkdir(applocation)
  382. versionlocation = "%s/%s" % (applocation, version)
  383. if not os.path.exists(versionlocation):
  384. os.mkdir(versionlocation)
  385. shutil.rmtree(versionlocation)
  386. shutil.move("generated/%s/%s" % (appname, version), versionlocation)
  387. print("\nMoved files to %s" % versionlocation)
  388. def main():
  389. appname = entrypoint_directory
  390. version = "0.0.1"
  391. output_path = "generated/%s/%s" % (appname, version)
  392. api_yaml_path = "%s/api.yaml" % (output_path)
  393. app_python_path = "%s/src/app.py" % (output_path)
  394. # Builds the directory structure for the app
  395. build_base_structure(appname, version)
  396. # Generates the yaml based on input library etc
  397. data = generate_base_yaml(api_yaml_path, version, appname)
  398. modules = get_modules()
  399. data = loop_modules(modules, data)
  400. # Generates app file
  401. data = generate_app(app_python_path, data)
  402. # Dumps the yaml to specified directory
  403. dump_yaml(api_yaml_path, data)
  404. # Move the file to functions/apps repository
  405. move_files(appname, version)
  406. if __name__ == "__main__":
  407. main()